Compare commits
1019 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dcbe8ba38 | ||
|
|
360d4dbbe2 | ||
|
|
2f32b53d19 | ||
|
|
6a3e3c3753 | ||
|
|
5ad08d8d36 | ||
|
|
b892d92614 | ||
|
|
b259f083d4 | ||
|
|
38aa9fe2fb | ||
|
|
863dd3a53e | ||
|
|
bea5058df8 | ||
|
|
31c157f58f | ||
|
|
8af6887daa | ||
|
|
eb9b7b4f86 | ||
|
|
cf78766a37 | ||
|
|
944824b552 | ||
|
|
477bb1ac8f | ||
|
|
29df864ecb | ||
|
|
bcf897b7e0 | ||
|
|
e63890c755 | ||
|
|
f3725215bd | ||
|
|
c43e305ea7 | ||
|
|
b9215e2410 | ||
|
|
19d79ab055 | ||
|
|
64d4492806 | ||
|
|
0790eae8c6 | ||
|
|
e10e2c27c1 | ||
|
|
d30b38d4b9 | ||
|
|
f6e4ed7c60 | ||
|
|
7a6bbfac75 | ||
|
|
425d6f9a06 | ||
|
|
58c760bb77 | ||
|
|
3ffdce5e7a | ||
|
|
8e518a044a | ||
|
|
a5adbf80a9 | ||
|
|
0b6c478b4f | ||
|
|
0434bde16f | ||
|
|
0deb3113b5 | ||
|
|
ecb52c76b9 | ||
|
|
69c66053b7 | ||
|
|
892ad395a7 | ||
|
|
e801c09c0f | ||
|
|
ad560a8555 | ||
|
|
e75aa5c2b9 | ||
|
|
e83fd7af1b | ||
|
|
eaec8ef994 | ||
|
|
3339e6b442 | ||
|
|
4c2425c758 | ||
|
|
80d1e6469e | ||
|
|
2fad6394ee | ||
|
|
4bfe33a37f | ||
|
|
130c8bf3b1 | ||
|
|
b9df277104 | ||
|
|
97e1f321ca | ||
|
|
4933930afd | ||
|
|
ab4640382d | ||
|
|
e4cfa4b405 | ||
|
|
789062e85e | ||
|
|
5370bee369 | ||
|
|
2f972488a1 | ||
|
|
6f7656802f | ||
|
|
7d98c5493e | ||
|
|
e0443aa336 | ||
|
|
39ff0d1516 | ||
|
|
1b9c0ee4b8 | ||
|
|
d48287f93a | ||
|
|
717e87cfa9 | ||
|
|
708b488af8 | ||
|
|
d60d3f374b | ||
|
|
8b87a2bc40 | ||
|
|
d0da517503 | ||
|
|
754036c472 | ||
|
|
720438fd91 | ||
|
|
ba76df1b00 | ||
|
|
44d85c2864 | ||
|
|
1c8b73a381 | ||
|
|
b445af932c | ||
|
|
5121739fe4 | ||
|
|
96106498d8 | ||
|
|
0116d92021 | ||
|
|
43746634a5 | ||
|
|
5183786fb0 | ||
|
|
5ba0eed721 | ||
|
|
7d08c735ef | ||
|
|
e3067b685c | ||
|
|
b219ca4c1c | ||
|
|
9e5d16ff16 | ||
|
|
da630458e1 | ||
|
|
ee2eceffb0 | ||
|
|
c8d22e7b5f | ||
|
|
342e8725bd | ||
|
|
3ced00de1f | ||
|
|
7fa075fa75 | ||
|
|
95ca496691 | ||
|
|
50b1d93f08 | ||
|
|
8958f2f234 | ||
|
|
00b4d6a748 | ||
|
|
f4de0d8276 | ||
|
|
cfa749f4f3 | ||
|
|
eeaff08673 | ||
|
|
0475e88dc2 | ||
|
|
e1f73a4639 | ||
|
|
e2296a6f64 | ||
|
|
1a6abf4e1b | ||
|
|
315851eb5f | ||
|
|
0b99b4a9a0 | ||
|
|
66002ff401 | ||
|
|
bdfc8bdd0c | ||
|
|
98e4668969 | ||
|
|
e8235dd0a2 | ||
|
|
123c74de46 | ||
|
|
c92b9bf0fb | ||
|
|
b4cbfd2ae9 | ||
|
|
dd7eee277e | ||
|
|
ab76185434 | ||
|
|
6d97bf1e88 | ||
|
|
49701fcd09 | ||
|
|
40f04d9860 | ||
|
|
d58dd25dbb | ||
|
|
9b2731607b | ||
|
|
a8d2d6f13f | ||
|
|
7c21782ab5 | ||
|
|
f59bdaf5e0 | ||
|
|
9419ddd174 | ||
|
|
0666a8f5c2 | ||
|
|
81c019105c | ||
|
|
6584259454 | ||
|
|
03d0f56095 | ||
|
|
6ffd169784 | ||
|
|
406f64a7c5 | ||
|
|
1353a2c4c9 | ||
|
|
fb88f3bd96 | ||
|
|
22b3598704 | ||
|
|
b62c580d5e | ||
|
|
6a63ceaecc | ||
|
|
591f9e61fb | ||
|
|
7011c81bcd | ||
|
|
3cf7055122 | ||
|
|
aba31eda83 | ||
|
|
1b30582dd9 | ||
|
|
0fb66358cc | ||
|
|
e226f444f7 | ||
|
|
95bf70f568 | ||
|
|
a6597b44c3 | ||
|
|
51c01c5445 | ||
|
|
161bf75a1d | ||
|
|
2f16e2c608 | ||
|
|
aea2e79b37 | ||
|
|
f433d13a2f | ||
|
|
e9abf6ed05 | ||
|
|
0c32b25ddf | ||
|
|
a03dec91c5 | ||
|
|
7c5a966944 | ||
|
|
652dc0953b | ||
|
|
03860a6dce | ||
|
|
c6bee25264 | ||
|
|
068de0fa9f | ||
|
|
4b45d5ca26 | ||
|
|
a268391e68 | ||
|
|
89bdd86f14 | ||
|
|
e533bd7e35 | ||
|
|
09ed978e80 | ||
|
|
4b106e1f41 | ||
|
|
feeeb26d94 | ||
|
|
bef0d2d992 | ||
|
|
6e6bd8a6be | ||
|
|
631fa0db4e | ||
|
|
65d30b7a30 | ||
|
|
5ba5f27ca7 | ||
|
|
acc437bf2d | ||
|
|
5fd2505a33 | ||
|
|
7f6abc331b | ||
|
|
c190aab8b9 | ||
|
|
0f71abdac3 | ||
|
|
8ddc507bd5 | ||
|
|
1c4bae2d91 | ||
|
|
73ca4b1ea5 | ||
|
|
18a922b5cd | ||
|
|
11b98978c1 | ||
|
|
379d3811a8 | ||
|
|
0401b8a6e6 | ||
|
|
6148b996d8 | ||
|
|
39781c9cd7 | ||
|
|
18758a1614 | ||
|
|
b044d8d90e | ||
|
|
02e56f87bc | ||
|
|
d9b9ee221b | ||
|
|
21ec9188ca | ||
|
|
4d768becf5 | ||
|
|
a27049386b | ||
|
|
b23e3d7359 | ||
|
|
7660164583 | ||
|
|
5e1f3c5564 | ||
|
|
197fa9c01c | ||
|
|
554e3d0c2f | ||
|
|
b800cde34d | ||
|
|
775fdd2be0 | ||
|
|
7908ae4258 | ||
|
|
bfbd8229a1 | ||
|
|
afbf8dedbf | ||
|
|
569912abef | ||
|
|
7c94f6bc9a | ||
|
|
b825b5b063 | ||
|
|
50098b5e70 | ||
|
|
e237b4db1c | ||
|
|
2a25cf3bbd | ||
|
|
02275bb417 | ||
|
|
788cae3efe | ||
|
|
0dec70c53a | ||
|
|
f534f012d2 | ||
|
|
bb83875c99 | ||
|
|
d048aa33f7 | ||
|
|
8f3e250073 | ||
|
|
63a792d169 | ||
|
|
eb3524a22d | ||
|
|
f657a24a1a | ||
|
|
a5228448d7 | ||
|
|
1ec4796f72 | ||
|
|
6964158cf6 | ||
|
|
4fc4dd1b16 | ||
|
|
3e851f0c3c | ||
|
|
b8befaa973 | ||
|
|
b05046af29 | ||
|
|
eecc6c9e53 | ||
|
|
d4e754d601 | ||
|
|
a8a54593e2 | ||
|
|
5bbffc4f5c | ||
|
|
0833018399 | ||
|
|
f6850fc795 | ||
|
|
c0b4674568 | ||
|
|
5a8996d90a | ||
|
|
548b30e5b3 | ||
|
|
80f9329004 | ||
|
|
f672280236 | ||
|
|
90a4a01de7 | ||
|
|
09cebb90fe | ||
|
|
70389aab3d | ||
|
|
d9132a722f | ||
|
|
ea7a4e46e0 | ||
|
|
07b91058af | ||
|
|
c27ace6a6a | ||
|
|
1c0a5b17ca | ||
|
|
9b12a829d2 | ||
|
|
0f41172468 | ||
|
|
8597705a77 | ||
|
|
3f733ce857 | ||
|
|
40f8ec77b8 | ||
|
|
0af967d6c9 | ||
|
|
f6d43c9f39 | ||
|
|
70b0538dd5 | ||
|
|
439262b930 | ||
|
|
968b2587ae | ||
|
|
15f471a032 | ||
|
|
5175157ba6 | ||
|
|
e51e8f7196 | ||
|
|
00b34fda42 | ||
|
|
b34fabab54 | ||
|
|
487c7e2824 | ||
|
|
46c79a8772 | ||
|
|
bfb4144e57 | ||
|
|
dc1bb72070 | ||
|
|
8e084d2362 | ||
|
|
d5a75f887d | ||
|
|
710609e98b | ||
|
|
b73ab76bfb | ||
|
|
27b64df870 | ||
|
|
eabb897f96 | ||
|
|
68c5e47bad | ||
|
|
2ae5af7019 | ||
|
|
860d1ca9b3 | ||
|
|
66a9d1f25e | ||
|
|
bbfeedcdb3 | ||
|
|
079e273edb | ||
|
|
393aab4c4b | ||
|
|
4f2bf7549c | ||
|
|
acdf23571c | ||
|
|
62ec634db3 | ||
|
|
c53e978106 | ||
|
|
a7fa757d0d | ||
|
|
5fb1bd4175 | ||
|
|
e792ab7b4d | ||
|
|
02544d29fd | ||
|
|
20acbd0331 | ||
|
|
115b4aacb8 | ||
|
|
8746caab06 | ||
|
|
625648c908 | ||
|
|
734b5f9534 | ||
|
|
a0579318bd | ||
|
|
a437e3cbd3 | ||
|
|
1b242dc04e | ||
|
|
a1a51914a2 | ||
|
|
f6cab9b5a9 | ||
|
|
a3649c04e2 | ||
|
|
a562bfdb08 | ||
|
|
8ffe64ad8e | ||
|
|
a116d06d61 | ||
|
|
c26f73a5a8 | ||
|
|
f5847a57c1 | ||
|
|
fe9d23a0ff | ||
|
|
cdc27004bf | ||
|
|
b914164a77 | ||
|
|
35e58f90bc | ||
|
|
16d360c582 | ||
|
|
4c075b4d11 | ||
|
|
8c9c1c5afa | ||
|
|
d093163cd4 | ||
|
|
9bd6fcefd3 | ||
|
|
5139947643 | ||
|
|
01ff10385a | ||
|
|
9969c3a7ac | ||
|
|
f7ed2ec3e3 | ||
|
|
fedeeb3076 | ||
|
|
8157c27529 | ||
|
|
0eba0c6a4b | ||
|
|
13fb9db52b | ||
|
|
f6818ba880 | ||
|
|
dbf3b3cc79 | ||
|
|
24534069da | ||
|
|
4cec0a7350 | ||
|
|
0b86fa7bee | ||
|
|
b406e22695 | ||
|
|
3fca783dd8 | ||
|
|
6de4865052 | ||
|
|
facc2fab24 | ||
|
|
ddc0931e90 | ||
|
|
d5d32038f5 | ||
|
|
a20edd9bec | ||
|
|
3da90337ef | ||
|
|
9633f7644e | ||
|
|
a19cf0e1c3 | ||
|
|
1a841c4b5d | ||
|
|
937e7ba154 | ||
|
|
4cc0c85a6c | ||
|
|
943941e0f6 | ||
|
|
b160021e67 | ||
|
|
1bcc035979 | ||
|
|
ef67dc144f | ||
|
|
cc96fcd6a0 | ||
|
|
d1f00b2d48 | ||
|
|
2fe28d2335 | ||
|
|
f9276f4d83 | ||
|
|
099004a080 | ||
|
|
cc1df8d7d0 | ||
|
|
686a2e4fff | ||
|
|
e98fe3eec5 | ||
|
|
fc34ff38d3 | ||
|
|
b6b44b3782 | ||
|
|
c906636776 | ||
|
|
db282d1a04 | ||
|
|
898656963d | ||
|
|
6426e0238a | ||
|
|
08e8faf3ff | ||
|
|
d21adf6004 | ||
|
|
c3ac7dd1ab | ||
|
|
f1cfba3ad8 | ||
|
|
1ceed3461c | ||
|
|
d25c26156d | ||
|
|
d75c22114c | ||
|
|
05a754f446 | ||
|
|
b5e6eff65d | ||
|
|
eaa8ae66db | ||
|
|
5e4a08538b | ||
|
|
b01a54437a | ||
|
|
5f0fc78f30 | ||
|
|
325dc5e2fe | ||
|
|
a15b29122e | ||
|
|
074ccc8aab | ||
|
|
3809046fbc | ||
|
|
83ceb3264f | ||
|
|
9055858d55 | ||
|
|
2a465b5f1d | ||
|
|
b4101f856a | ||
|
|
44baa743c0 | ||
|
|
46dd449b2f | ||
|
|
f21d45e697 | ||
|
|
1e0a19ea7a | ||
|
|
dcfa47291e | ||
|
|
bd32c9555e | ||
|
|
f8f612544e | ||
|
|
1c0271f55e | ||
|
|
a10bc74de1 | ||
|
|
958ef80602 | ||
|
|
124b63f325 | ||
|
|
40f5ba5004 | ||
|
|
32f30826b9 | ||
|
|
b4aa8b37ea | ||
|
|
8368bbec47 | ||
|
|
618e482507 | ||
|
|
43711a1a59 | ||
|
|
bbe071545d | ||
|
|
4710479b46 | ||
|
|
c28a375b5d | ||
|
|
750d3429e0 | ||
|
|
4c34fe9b85 | ||
|
|
34c56980d4 | ||
|
|
a5b8609df1 | ||
|
|
1fd7f0314a | ||
|
|
25e82d690e | ||
|
|
31879cb60b | ||
|
|
489e5b551c | ||
|
|
e29bd01f68 | ||
|
|
e5d9140aa0 | ||
|
|
09f5cca948 | ||
|
|
405e09fdf4 | ||
|
|
43624e9b7b | ||
|
|
29ed0ad05a | ||
|
|
0aafe79c65 | ||
|
|
802afd592c | ||
|
|
ac1644e32d | ||
|
|
5a1f130bec | ||
|
|
678868153a | ||
|
|
991f050dbb | ||
|
|
be04355685 | ||
|
|
762b6b3f3b | ||
|
|
71d9cbdce6 | ||
|
|
d995ef19b5 | ||
|
|
bf80e4b02b | ||
|
|
64a047cd7c | ||
|
|
566421003c | ||
|
|
198da7608d | ||
|
|
0a7edb219e | ||
|
|
65c20f2211 | ||
|
|
144767152a | ||
|
|
82be52be52 | ||
|
|
46c8caa627 | ||
|
|
0027e838a0 | ||
|
|
7e846e2a58 | ||
|
|
de7f55bb97 | ||
|
|
2ac9a59469 | ||
|
|
bdb4014b94 | ||
|
|
9509bd1510 | ||
|
|
3f11770baa | ||
|
|
259a040b7e | ||
|
|
ccb14630f7 | ||
|
|
4aa865a60f | ||
|
|
c7ea7b057c | ||
|
|
91dbbd46c0 | ||
|
|
e5146077eb | ||
|
|
76918bf973 | ||
|
|
b7d3e69f87 | ||
|
|
d7bccfd267 | ||
|
|
2a25917e41 | ||
|
|
d73239b274 | ||
|
|
9f6fffbe6b | ||
|
|
72f7ff3df5 | ||
|
|
2af1dba8dc | ||
|
|
ca353d747b | ||
|
|
1589d4df1c | ||
|
|
b3abe8af9c | ||
|
|
c178e36f9b | ||
|
|
d93092de99 | ||
|
|
790b05880a | ||
|
|
f34766ade0 | ||
|
|
0e1d5e802c | ||
|
|
db526dfcc8 | ||
|
|
c18db60e80 | ||
|
|
b579a6ade2 | ||
|
|
9d1d642734 | ||
|
|
261c051052 | ||
|
|
e499e2d0dc | ||
|
|
b860b6f389 | ||
|
|
05d5d5a967 | ||
|
|
74ba1cc723 | ||
|
|
f2042efdc2 | ||
|
|
6b7e7fa1e4 | ||
|
|
6677e6e74f | ||
|
|
c3994ddbea | ||
|
|
a981ff2f6c | ||
|
|
3e0ba398d4 | ||
|
|
aa4f7c8536 | ||
|
|
959f9454d8 | ||
|
|
6b72a309f5 | ||
|
|
c388fe373d | ||
|
|
270ddc6487 | ||
|
|
5ccaa8f106 | ||
|
|
1d92c2668d | ||
|
|
6e03a05e6d | ||
|
|
2905059947 | ||
|
|
1df927f771 | ||
|
|
bd8b6d0319 | ||
|
|
fef39b2720 | ||
|
|
7de433c5fc | ||
|
|
0a711f2656 | ||
|
|
04b6b8aa8a | ||
|
|
58f286efe4 | ||
|
|
a01b292eb3 | ||
|
|
18c608ad7e | ||
|
|
8d144f4e12 | ||
|
|
7f895bfbec | ||
|
|
b0c356fa9b | ||
|
|
79defdc3f3 | ||
|
|
b2d9568deb | ||
|
|
a130c049bf | ||
|
|
8904039515 | ||
|
|
7d28181b16 | ||
|
|
98e4c81b9b | ||
|
|
10f5af5f09 | ||
|
|
18ffad5de5 | ||
|
|
428db42140 | ||
|
|
6e5426764e | ||
|
|
f3cfcc650c | ||
|
|
fc88573c9d | ||
|
|
5cf1c3d14f | ||
|
|
79f15cc34d | ||
|
|
775cab1080 | ||
|
|
3e20e7d0ce | ||
|
|
54407e0a60 | ||
|
|
ef696391d8 | ||
|
|
0c34df290e | ||
|
|
04d31bd814 | ||
|
|
9888d9f59e | ||
|
|
3bb1bf0967 | ||
|
|
dfbcb1f45c | ||
|
|
12ecf4de40 | ||
|
|
7be1171004 | ||
|
|
2bb646d150 | ||
|
|
e7749b2dff | ||
|
|
434d8eabc8 | ||
|
|
0a14219112 | ||
|
|
5b811df8ee | ||
|
|
bc264109f3 | ||
|
|
9c29c1ca9b | ||
|
|
fe4f62ff8d | ||
|
|
35dfb9d1ff | ||
|
|
3809aca09d | ||
|
|
bcd5bb5009 | ||
|
|
0d7cc6a386 | ||
|
|
12265699b3 | ||
|
|
783c0356c7 | ||
|
|
7ef31bc0b5 | ||
|
|
467f2368dd | ||
|
|
2cfcb081a2 | ||
|
|
1829ac851d | ||
|
|
2e715004ae | ||
|
|
45ee092593 | ||
|
|
20e543f721 | ||
|
|
b2148eb656 | ||
|
|
efab4fb41b | ||
|
|
9ac3fd3615 | ||
|
|
14bc7a0f76 | ||
|
|
26727fea17 | ||
|
|
34f8d4c2a6 | ||
|
|
02708807bd | ||
|
|
176e5de531 | ||
|
|
b9180a4426 | ||
|
|
959b30c788 | ||
|
|
df79ef59ea | ||
|
|
4dc1f01cf0 | ||
|
|
cb17110562 | ||
|
|
4424e4f9be | ||
|
|
fb641ac960 | ||
|
|
a5faa378b0 | ||
|
|
66ea277a59 | ||
|
|
006bc6ceda | ||
|
|
aef3e869dc | ||
|
|
9c46d28871 | ||
|
|
1fc141050f | ||
|
|
1e45d199e2 | ||
|
|
3018f3653c | ||
|
|
1c5b856800 | ||
|
|
f53a5ea6c1 | ||
|
|
a608734be9 | ||
|
|
e52523f903 | ||
|
|
82778014b8 | ||
|
|
cb4b9a361f | ||
|
|
3a337940d1 | ||
|
|
9abdafb905 | ||
|
|
cd494b52a4 | ||
|
|
5581d1431b | ||
|
|
6539b14ecf | ||
|
|
45b30e4a33 | ||
|
|
ff48d543e7 | ||
|
|
6562b74130 | ||
|
|
23a68370b4 | ||
|
|
cc9f346d49 | ||
|
|
8c18865138 | ||
|
|
db3a17a2c8 | ||
|
|
5a0d1ac0c0 | ||
|
|
5414accc6c | ||
|
|
c415ace453 | ||
|
|
bf34beec20 | ||
|
|
f4d459af7f | ||
|
|
508cc2bd91 | ||
|
|
35b7e3a289 | ||
|
|
fc907d23a7 | ||
|
|
45e663fcf8 | ||
|
|
b00c6a9268 | ||
|
|
ad7b0cd834 | ||
|
|
eccb3e2825 | ||
|
|
f5a343f358 | ||
|
|
f8b65a5546 | ||
|
|
0f0b9c5551 | ||
|
|
41ab11e7b4 | ||
|
|
9949e7c8d4 | ||
|
|
9e36d84f19 | ||
|
|
ada526fa63 | ||
|
|
ca65eb907d | ||
|
|
7f2a0dd3e8 | ||
|
|
6e34409225 | ||
|
|
4af354e918 | ||
|
|
207d0caf2a | ||
|
|
f7487d22d5 | ||
|
|
3c10976aff | ||
|
|
ec8e144655 | ||
|
|
ae5ccfd775 | ||
|
|
9d9e22451d | ||
|
|
e68daf870f | ||
|
|
534e95f86c | ||
|
|
077713003f | ||
|
|
35fd8e62ac | ||
|
|
b7c2ddd59d | ||
|
|
f931567f56 | ||
|
|
8545e0692c | ||
|
|
19eb05269b | ||
|
|
92d757662a | ||
|
|
1b1406a4d9 | ||
|
|
03fc19f070 | ||
|
|
ffa09f1b29 | ||
|
|
ff9a1523fd | ||
|
|
8a7e5c0830 | ||
|
|
8e90ad69b1 | ||
|
|
52c389edd8 | ||
|
|
f7206c1603 | ||
|
|
86d9baa503 | ||
|
|
92c4565590 | ||
|
|
c51870ff79 | ||
|
|
182f061354 | ||
|
|
80507cab27 | ||
|
|
f801ae9b63 | ||
|
|
977173d987 | ||
|
|
cd0fcb903f | ||
|
|
7bae1d9537 | ||
|
|
b43cbb7afe | ||
|
|
72982387cc | ||
|
|
ff0245840a | ||
|
|
c55f64e209 | ||
|
|
a4cb5d1b14 | ||
|
|
13e1415355 | ||
|
|
7b49d66a8e | ||
|
|
63c6e12aca | ||
|
|
b64d4fd96f | ||
|
|
dda603c7d8 | ||
|
|
e22de5cba1 | ||
|
|
bdabfdcb3d | ||
|
|
00a8514245 | ||
|
|
94fd3197b3 | ||
|
|
7957353c3f | ||
|
|
b3b7589db3 | ||
|
|
5aed9ce29e | ||
|
|
924f0a9f7c | ||
|
|
7a7cd72db9 | ||
|
|
e9e9bab479 | ||
|
|
f258dcfca2 | ||
|
|
fe84f812e7 | ||
|
|
9eba376976 | ||
|
|
462705c4ed | ||
|
|
a2533ce7f9 | ||
|
|
dbf42c51a4 | ||
|
|
f61e7caf2b | ||
|
|
679c2070c1 | ||
|
|
92d46e1da3 | ||
|
|
7ab94205e4 | ||
|
|
ab616c5d32 | ||
|
|
8f2f68dffc | ||
|
|
18b7e17e95 | ||
|
|
cca2298d3a | ||
|
|
f3683bcc84 | ||
|
|
fa2959515e | ||
|
|
7ab5ddc408 | ||
|
|
f273858248 | ||
|
|
ca8f7374da | ||
|
|
ff1dce833a | ||
|
|
d3d5a7bade | ||
|
|
f5d6702472 | ||
|
|
3db687ad40 | ||
|
|
a5cb958398 | ||
|
|
9e522091c6 | ||
|
|
79f256976e | ||
|
|
b560c0bafd | ||
|
|
bd157d305e | ||
|
|
923016197a | ||
|
|
dcf96e2bf5 | ||
|
|
d4697cb203 | ||
|
|
6e6a50b46e | ||
|
|
b9830bc64a | ||
|
|
7c501cec45 | ||
|
|
add23934ca | ||
|
|
a8b798b00c | ||
|
|
b522b1de05 | ||
|
|
3660cbd450 | ||
|
|
50f8bb8721 | ||
|
|
e1a2d90382 | ||
|
|
d8872f215b | ||
|
|
484bc6ea39 | ||
|
|
7d1979f067 | ||
|
|
6927c0b30b | ||
|
|
aa74c5ccaf | ||
|
|
e3d0f571d2 | ||
|
|
d03dabdfdf | ||
|
|
fc339ae55f | ||
|
|
a0aa04fd8c | ||
|
|
6dc5ae1ae4 | ||
|
|
df02a6b50f | ||
|
|
9e4f733c28 | ||
|
|
1175b330f5 | ||
|
|
3cb9fff07f | ||
|
|
bfdb72dd0a | ||
|
|
5489462f90 | ||
|
|
94ac3c3922 | ||
|
|
bf75946e14 | ||
|
|
b2a70e0cce | ||
|
|
83780f9bcd | ||
|
|
bfb9795913 | ||
|
|
208598a6df | ||
|
|
6c79753051 | ||
|
|
095a238fff | ||
|
|
ebbde8afd3 | ||
|
|
bba5bb7411 | ||
|
|
9c155c6cf5 | ||
|
|
19da7a74df | ||
|
|
f5d76fd5ff | ||
|
|
77940c9430 | ||
|
|
54a42a14b6 | ||
|
|
52faf7884b | ||
|
|
841ed4e682 | ||
|
|
bc417b9eea | ||
|
|
da7dc477c8 | ||
|
|
6c519ebd61 | ||
|
|
88e859817b | ||
|
|
f5dd36260f | ||
|
|
a5325b84ae | ||
|
|
7095c9e71e | ||
|
|
fb4373c83a | ||
|
|
dd59a1aebb | ||
|
|
6f7edd0b40 | ||
|
|
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 |
@@ -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
|
||||
|
||||
|
||||
50
.github/workflows/publish.yml
vendored
50
.github/workflows/publish.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "pro"
|
||||
- "dev"
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
@@ -115,9 +114,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 +271,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,7 +298,7 @@ jobs:
|
||||
env:
|
||||
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
|
||||
run: |
|
||||
pushd electron
|
||||
pushd electron || exit
|
||||
npm install
|
||||
popd
|
||||
popd || exit
|
||||
node ./electron/build.js published
|
||||
|
||||
50
.gitignore
vendored
50
.gitignore
vendored
@@ -1,31 +1,61 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
/vendor
|
||||
|
||||
# Build and temporary files
|
||||
/build
|
||||
/public/hot
|
||||
/public/tmp
|
||||
/tmp
|
||||
|
||||
# Uploads and user-generated content
|
||||
/public/summary
|
||||
/public/uploads/*
|
||||
/public/.well-known
|
||||
/public/.user.ini
|
||||
/storage/*.key
|
||||
|
||||
# Storage and configuration
|
||||
/config/LICENSE
|
||||
/vendor
|
||||
/build
|
||||
/tmp
|
||||
._*
|
||||
/storage/*.key
|
||||
|
||||
# Environment and configuration
|
||||
.env
|
||||
vars.yaml
|
||||
|
||||
# IDE and editor files
|
||||
.idea
|
||||
.vscode
|
||||
.vagrant
|
||||
.windsurfrules
|
||||
.phpunit.result.cache
|
||||
|
||||
# Development tools
|
||||
.vagrant
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
|
||||
# Development file
|
||||
/index.html
|
||||
|
||||
# Testing
|
||||
.phpunit.result.cache
|
||||
test.*
|
||||
|
||||
# Logs and debug files
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
test.*
|
||||
|
||||
# Lock files
|
||||
dootask.lock
|
||||
package-lock.json
|
||||
|
||||
# Laravel/Swoole specific
|
||||
laravels-timer-process.pid
|
||||
.DS_Store
|
||||
vars.yaml
|
||||
laravels.conf
|
||||
laravels.pid
|
||||
|
||||
# System files
|
||||
._*
|
||||
.DS_Store
|
||||
|
||||
# Documentation
|
||||
AGENTS.md
|
||||
README_LOCAL.md
|
||||
|
||||
13
.gitpod.yml
13
.gitpod.yml
@@ -1,13 +0,0 @@
|
||||
# This configuration file was automatically generated by Gitpod.
|
||||
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
|
||||
# and commit this file to your remote git repository to share the goodness with others.
|
||||
|
||||
tasks:
|
||||
- init: sudo ./cmd install
|
||||
command: ./cmd dev
|
||||
|
||||
ports:
|
||||
- port: 2222
|
||||
visibility: public
|
||||
- port: 22222
|
||||
visibility: public
|
||||
3989
CHANGELOG.md
3989
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
139
README.md
139
README.md
@@ -1,148 +1,147 @@
|
||||
# Install (Docker)
|
||||
# DooTask - Open Source Task Management System
|
||||
|
||||
English | **[中文文档](./README_CN.md)**
|
||||
|
||||
- [Screenshot preview](./README_PREVIEW.md)
|
||||
- [Demo site](http://www.dootask.com/)
|
||||
- [Screenshot Preview](./README_PREVIEW.md)
|
||||
- [Demo Site](http://www.dootask.com/)
|
||||
|
||||
**QQ Group**
|
||||
|
||||
Group No.: `546574618`
|
||||
- Group Number: `546574618`
|
||||
|
||||
## Setup
|
||||
## 📍 Migration from 0.x to 1.x
|
||||
|
||||
- `Docker v20.10+` & `Docker Compose v2.0+` must be installed
|
||||
- System: `Centos/Debian/Ubuntu/macOS/Windows`
|
||||
- Hardware suggestion: 2 cores and above 4G memory
|
||||
- Special note: Windows users please use `git bash` or `cmder` to run the command
|
||||
- Please ensure to back up your data before upgrading!
|
||||
- If the upgrade fails, try running `./cmd update` multiple times.
|
||||
- If you encounter "Container xxx not found" during upgrade, run `./cmd reup` and then execute `./cmd update`.
|
||||
- If you see a 502 error after upgrading, run `./cmd reup` to restart the services.
|
||||
- If you encounter "Application 'xxx' not installed" after upgrading, log in with the admin account and install the relevant applications from the App Store.
|
||||
|
||||
### Deployment (Pro Edition)
|
||||
## Installation Requirements
|
||||
|
||||
- Required: `Docker v20.10+` and `Docker Compose v2.0+`
|
||||
- Supported Systems: `CentOS/Debian/Ubuntu/macOS` and other Linux/Unix systems
|
||||
- Hardware Recommendation: 2+ cores, 4GB+ memory
|
||||
- Special Note: Windows users can install Linux environment using WSL2 before installing DooTask.
|
||||
|
||||
### Deploy Project
|
||||
|
||||
```bash
|
||||
# 1、Clone the repository
|
||||
# 1、Clone the project to your local machine or server
|
||||
|
||||
# Clone projects on github
|
||||
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
|
||||
# Or you can use gitee
|
||||
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
|
||||
# Clone project from GitHub
|
||||
git clone --depth=1 https://github.com/kuaifan/dootask.git
|
||||
# Or you can use Gitee
|
||||
git clone --depth=1 https://gitee.com/aipaw/dootask.git
|
||||
|
||||
# 2、Enter directory
|
||||
cd dootask
|
||||
|
||||
# 3、Installation(Custom port installation, as: ./cmd install --port 80)
|
||||
# 3、One-click installation (Custom port installation: ./cmd install --port 80)
|
||||
./cmd install
|
||||
```
|
||||
|
||||
### Reset password
|
||||
### Reset Password
|
||||
|
||||
```bash
|
||||
# Reset default account password
|
||||
# Reset default administrator password
|
||||
./cmd repassword
|
||||
```
|
||||
|
||||
### Change port
|
||||
### Change Port
|
||||
|
||||
```bash
|
||||
# This method only replaces the HTTP port. To replace the HTTPS port, please read the SSL configuration below
|
||||
# This method only changes HTTP port. For HTTPS port, please read SSL configuration below
|
||||
./cmd port 80
|
||||
```
|
||||
|
||||
### Stop server
|
||||
### Stop Service
|
||||
|
||||
```bash
|
||||
./cmd stop
|
||||
|
||||
# P.S: Once application is set up, whenever you want to start the server (if it is stopped) run below command
|
||||
./cmd start
|
||||
./cmd down
|
||||
```
|
||||
|
||||
### Development compilation
|
||||
|
||||
- `NodeJs 20+` must be installed
|
||||
### Start Service
|
||||
|
||||
```bash
|
||||
# Development
|
||||
./cmd up
|
||||
```
|
||||
|
||||
### Development & Build
|
||||
|
||||
Please ensure you have installed `NodeJs 20+`
|
||||
|
||||
```bash
|
||||
# Development mode
|
||||
./cmd dev
|
||||
|
||||
# Production (This is web client. For App/PC/Mac clients, Please read README-CLIENT.md)
|
||||
# Build project (This is for web client. For desktop apps, refer to ".github/workflows/publish.yml")
|
||||
./cmd prod
|
||||
```
|
||||
|
||||
### Shortcuts for running command
|
||||
### SSL Configuration
|
||||
|
||||
```bash
|
||||
# You can do this using the following command
|
||||
./cmd artisan "your command" # To run a artisan command
|
||||
./cmd php "your command" # To run a php command
|
||||
./cmd nginx "your command" # To run a nginx command
|
||||
./cmd redis "your command" # To run a redis command
|
||||
./cmd composer "your command" # To run a composer command
|
||||
./cmd supervisorctl "your command" # To run a supervisorctl command
|
||||
./cmd mysql "your command" # To run a mysql command (backup: Backup database, recovery: Restore database, open: Open database external port access, close: Close database external port access)
|
||||
```
|
||||
|
||||
### SSL configuration
|
||||
|
||||
#### Method 1: Automatic configuration
|
||||
#### Method 1: Automatic Configuration
|
||||
|
||||
```bash
|
||||
# Running commands in a project
|
||||
# Run command and follow the prompts
|
||||
./cmd https
|
||||
```
|
||||
|
||||
#### Or Method 2: Nginx Agent Configuration
|
||||
#### Method 2: Nginx Proxy Configuration
|
||||
|
||||
```bash
|
||||
# 1、Nginx config add
|
||||
# 1、Add Nginx proxy configuration
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# 2、Running commands in a project (If you unconfigure the NGINX agent, run: ./cmd https close)
|
||||
# 2、Run command (To cancel Nginx proxy configuration: ./cmd https close)
|
||||
./cmd https agent
|
||||
```
|
||||
|
||||
## Upgrade
|
||||
## Upgrade & Update
|
||||
|
||||
**Note: Please back up your data before upgrading!**
|
||||
**Note: Please backup your data before upgrading!**
|
||||
|
||||
```bash
|
||||
# Method 1: Running commands in a project
|
||||
./cmd update
|
||||
|
||||
# Or method 2: use this method if method 1 fails
|
||||
git pull
|
||||
./cmd mysql backup
|
||||
./cmd uninstall
|
||||
./cmd install
|
||||
./cmd mysql recovery
|
||||
```
|
||||
|
||||
* Please try again if the upgrade fails across a large version.
|
||||
* If 502 after the upgrade please run `./cmd restart` restart the service.
|
||||
* Please retry if upgrade fails across major versions.
|
||||
* If you encounter 502 errors after upgrade, run `./cmd reup` to restart services.
|
||||
|
||||
## Transfer
|
||||
## Project Migration
|
||||
|
||||
Follow these steps to complete the project migration after the new project is installed:
|
||||
After installing the new project, follow these steps to complete migration:
|
||||
|
||||
1. Backup original database
|
||||
1、Backup original database
|
||||
|
||||
```bash
|
||||
# Run command under old project
|
||||
# Run command in the old project
|
||||
./cmd mysql backup
|
||||
```
|
||||
|
||||
2. Copy `database backup file` and `public/uploads` directory to the new project.
|
||||
2、Copy the following files and directories from old project to the same paths in new project
|
||||
|
||||
3. Restore database to new project
|
||||
- `Database backup file`
|
||||
- `docker/appstore`
|
||||
- `public/uploads`
|
||||
|
||||
3、Restore database to new project
|
||||
```bash
|
||||
# Run command under new project
|
||||
# Run command in the new project
|
||||
./cmd mysql recovery
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
## Uninstall Project
|
||||
|
||||
```bash
|
||||
# Running commands in a project
|
||||
./cmd uninstall
|
||||
```
|
||||
|
||||
### More Commands
|
||||
|
||||
```bash
|
||||
./cmd help
|
||||
```
|
||||
|
||||
80
README_CN.md
80
README_CN.md
@@ -1,4 +1,4 @@
|
||||
# Install (Docker)
|
||||
# DooTask - 开源任务管理系统
|
||||
|
||||
**[English](./README.md)** | 中文文档
|
||||
|
||||
@@ -9,22 +9,30 @@
|
||||
|
||||
- QQ群号: `546574618`
|
||||
|
||||
## 📍 0.x 迁移到 1.x
|
||||
|
||||
- 升级时请务必备份好数据!
|
||||
- 如果升级失败请尝试执行 `./cmd update` 重试几次。
|
||||
- 如果升级中出现 `没有找到 xxx 容器` 的提示,请运行 `./cmd reup` 后再执行 `./cmd update`。
|
||||
- 如果升级后出现502错误请运行 `./cmd reup` 重启服务即可。
|
||||
- 如果升级后出现 `应用「xxx」未安装` 的提示,请使用管理员账号进入应用商店安装相关应用。
|
||||
|
||||
## 安装程序
|
||||
|
||||
- 必须安装:`Docker v20.10+` 和 `Docker Compose v2.0+`
|
||||
- 支持环境:`Centos/Debian/Ubuntu/macOS/Windows`
|
||||
- 支持环境:`Centos/Debian/Ubuntu/macOS` 等 linux/unix 系统
|
||||
- 硬件建议:2核4G以上
|
||||
- 特别说明:Windows 用户请使用 `git bash` 或者 `cmder` 运行命令
|
||||
- 特别说明:Windows 可以使用 WSL2 安装 Linux 环境后再安装 DooTask。
|
||||
|
||||
### 部署项目(Pro版)
|
||||
### 部署项目
|
||||
|
||||
```bash
|
||||
# 1、克隆项目到您的本地或服务器
|
||||
|
||||
# 通过github克隆项目
|
||||
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
|
||||
git clone --depth=1 https://github.com/kuaifan/dootask.git
|
||||
# 或者你也可以使用gitee
|
||||
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
|
||||
git clone --depth=1 https://gitee.com/aipaw/dootask.git
|
||||
|
||||
# 2、进入目录
|
||||
cd dootask
|
||||
@@ -50,48 +58,37 @@ cd dootask
|
||||
### 停止服务
|
||||
|
||||
```bash
|
||||
./cmd stop
|
||||
./cmd down
|
||||
```
|
||||
|
||||
# 一旦应用程序被设置,无论何时你想要启动服务器(如果它被停止)运行以下命令
|
||||
./cmd start
|
||||
### 启动服务
|
||||
|
||||
```bash
|
||||
./cmd up
|
||||
```
|
||||
|
||||
### 开发编译
|
||||
|
||||
- 请确保你已经安装了 `NodeJs 20+`
|
||||
请确保你已经安装了 `NodeJs 20+`
|
||||
|
||||
```bash
|
||||
# 开发模式
|
||||
./cmd dev
|
||||
|
||||
# 编译项目(这是网页端的,App/Pc/Mac客户端请查看 README_CLIENT.md)
|
||||
# 编译项目(这是网页端的,客户端请参考“.github/workflows/publish.yml”文件)
|
||||
./cmd prod
|
||||
```
|
||||
|
||||
|
||||
### 运行命令的快捷方式
|
||||
|
||||
```bash
|
||||
# 你可以使用以下命令来执行
|
||||
./cmd artisan "your command" # 运行 artisan 命令
|
||||
./cmd php "your command" # 运行 php 命令
|
||||
./cmd nginx "your command" # 运行 nginx 命令
|
||||
./cmd redis "your command" # 运行 redis 命令
|
||||
./cmd composer "your command" # 运行 composer 命令
|
||||
./cmd supervisorctl "your command" # 运行 supervisorctl 命令
|
||||
./cmd mysql "your command" # 运行 mysql 命令 (backup: 备份数据库,recovery: 还原数据库,open: 开启数据库外部端口访问,close: 关闭数据库外部端口访问)
|
||||
```
|
||||
|
||||
### SSL 配置
|
||||
|
||||
#### 方法1:自动配置
|
||||
|
||||
```bash
|
||||
# 在项目下运行命令,根据提示执行即可
|
||||
# 执行指令,根据提示执行即可
|
||||
./cmd https
|
||||
```
|
||||
|
||||
#### (或者)方法2:Nginx 代理配置
|
||||
#### 方法2:Nginx 代理配置
|
||||
|
||||
```bash
|
||||
# 1、Nginx 代理配置添加
|
||||
@@ -99,7 +96,7 @@ proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# 2、在项目下运行命令(如果取消 Nginx 代理配置请运行:./cmd https close)
|
||||
# 2、执行指令(如果取消 Nginx 代理配置请运行:./cmd https close)
|
||||
./cmd https agent
|
||||
```
|
||||
|
||||
@@ -108,19 +105,11 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
**注意:在升级之前请备份好你的数据!**
|
||||
|
||||
```bash
|
||||
# 方法1:在项目下运行命令
|
||||
./cmd update
|
||||
|
||||
# (或者)方法2:如果方法1失败请使用此方法
|
||||
git pull
|
||||
./cmd mysql backup
|
||||
./cmd uninstall
|
||||
./cmd install
|
||||
./cmd mysql recovery
|
||||
```
|
||||
|
||||
* 跨越大版本升级失败时请重试执行一次。
|
||||
* 如果升级后出现502请运行 `./cmd restart` 重启服务即可。
|
||||
* 如果升级后出现502请运行 `./cmd reup` 重启服务即可。
|
||||
|
||||
## 迁移项目
|
||||
|
||||
@@ -129,21 +118,30 @@ git pull
|
||||
1、备份原数据库
|
||||
|
||||
```bash
|
||||
# 在旧的项目下运行命令
|
||||
# 在旧的项目下执行指令
|
||||
./cmd mysql backup
|
||||
```
|
||||
|
||||
2、将`数据库备份文件`及`public/uploads`目录拷贝至新项目
|
||||
2、将旧项目以下文件和目录拷贝至新项目同路径位置
|
||||
|
||||
- `数据库备份文件`
|
||||
- `docker/appstore`
|
||||
- `public/uploads`
|
||||
|
||||
3、还原数据库至新项目
|
||||
```bash
|
||||
# 在新的项目下运行命令
|
||||
# 在新的项目下执行指令
|
||||
./cmd mysql recovery
|
||||
```
|
||||
|
||||
## 卸载项目
|
||||
|
||||
```bash
|
||||
# 在项目下运行命令
|
||||
./cmd uninstall
|
||||
```
|
||||
|
||||
### 更多指令
|
||||
|
||||
```bash
|
||||
./cmd help
|
||||
```
|
||||
|
||||
175
app/Console/Commands/SyncUserMsgToZincSearch.php
Normal file
175
app/Console/Commands/SyncUserMsgToZincSearch.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Apps;
|
||||
use App\Module\ZincSearch\ZincSearchKeyValue;
|
||||
use App\Module\ZincSearch\ZincSearchDialogMsg;
|
||||
use Cache;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SyncUserMsgToZincSearch extends Command
|
||||
{
|
||||
/**
|
||||
* 更新数据
|
||||
* --f: 全量更新 (默认)
|
||||
* --i: 增量更新(从上次更新的最后一个ID接上)
|
||||
*
|
||||
* 清理数据
|
||||
* --c: 清除索引
|
||||
*/
|
||||
|
||||
protected $signature = 'zinc:sync-user-msg {--f} {--i} {--c} {--batch=1000}';
|
||||
protected $description = '同步聊天会话用户和消息到 ZincSearch';
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
if (!Apps::isInstalled("search")) {
|
||||
$this->error("应用「ZincSearch」未安装");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 注册信号处理器(仅在支持pcntl扩展的环境下)
|
||||
if (extension_loaded('pcntl')) {
|
||||
pcntl_async_signals(true); // 启用异步信号处理
|
||||
pcntl_signal(SIGINT, [$this, 'handleSignal']); // Ctrl+C
|
||||
pcntl_signal(SIGTERM, [$this, 'handleSignal']); // kill
|
||||
}
|
||||
|
||||
// 检查锁,如果已被占用则退出
|
||||
$lockInfo = $this->getLock();
|
||||
if ($lockInfo) {
|
||||
$this->error("命令已在运行中,开始时间: {$lockInfo['started_at']}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 设置锁
|
||||
$this->setLock();
|
||||
|
||||
// 清除索引
|
||||
if ($this->option('c')) {
|
||||
$this->info('清除索引...');
|
||||
ZincSearchKeyValue::clear();
|
||||
ZincSearchDialogMsg::clear();
|
||||
$this->info("索引删除成功");
|
||||
$this->releaseLock();
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info('开始同步聊天数据...');
|
||||
|
||||
// 同步消息数据
|
||||
$this->syncDialogMsgs();
|
||||
|
||||
// 完成
|
||||
$this->info("\n同步完成");
|
||||
$this->releaseLock();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锁信息
|
||||
*
|
||||
* @return array|null 如果锁存在返回锁信息,否则返回null
|
||||
*/
|
||||
private function getLock(): ?array
|
||||
{
|
||||
$lockKey = md5($this->signature);
|
||||
return Cache::has($lockKey) ? Cache::get($lockKey) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置锁
|
||||
*/
|
||||
private function setLock(): void
|
||||
{
|
||||
$lockKey = md5($this->signature);
|
||||
$lockInfo = [
|
||||
'started_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
Cache::put($lockKey, $lockInfo, 300); // 5分钟
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放锁
|
||||
*/
|
||||
private function releaseLock(): void
|
||||
{
|
||||
$lockKey = md5($this->signature);
|
||||
Cache::forget($lockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理终端信号
|
||||
*
|
||||
* @param int $signal
|
||||
* @return void
|
||||
*/
|
||||
public function handleSignal(int $signal): void
|
||||
{
|
||||
// 释放锁
|
||||
$this->releaseLock();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步消息数据
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function syncDialogMsgs(): void
|
||||
{
|
||||
// 获取上次同步的最后ID
|
||||
$lastKey = "sync:dialogUserMsgLastId";
|
||||
$lastId = $this->option('i') ? intval(ZincSearchKeyValue::get($lastKey, 0)) : 0;
|
||||
|
||||
if ($lastId > 0) {
|
||||
$this->info("\n同步消息数据({$lastId})...");
|
||||
} else {
|
||||
$this->info("\n同步消息数据...");
|
||||
}
|
||||
|
||||
$num = 0;
|
||||
$count = WebSocketDialogMsg::where('id', '>', $lastId)->count();
|
||||
$batchSize = $this->option('batch');
|
||||
|
||||
$total = 0;
|
||||
$lastNum = 0;
|
||||
|
||||
do {
|
||||
// 获取一批
|
||||
$dialogMsgs = WebSocketDialogMsg::where('id', '>', $lastId)
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($dialogMsgs->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$num += count($dialogMsgs);
|
||||
$progress = round($num / $count * 100, 2);
|
||||
if ($progress < 100) {
|
||||
$progress = number_format($progress, 2);
|
||||
}
|
||||
$this->info("{$num}/{$count} ({$progress}%) 正在同步消息ID {$dialogMsgs->first()->id} ~ {$dialogMsgs->last()->id} ({$total}|{$lastNum})");
|
||||
|
||||
// 刷新锁
|
||||
$this->setLock();
|
||||
|
||||
// 同步数据
|
||||
$lastNum = ZincSearchDialogMsg::batchSync($dialogMsgs);
|
||||
$total += $lastNum;
|
||||
|
||||
// 更新最后ID
|
||||
$lastId = $dialogMsgs->last()->id;
|
||||
ZincSearchKeyValue::set($lastKey, $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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,18 @@ class ApiException extends RuntimeException
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $writeLog = true;
|
||||
|
||||
/**
|
||||
* ApiException constructor.
|
||||
* @param string $msg
|
||||
* @param string|array $msg
|
||||
* @param array $data
|
||||
* @param int $code
|
||||
*/
|
||||
public function __construct($msg = '', $data = [], $code = 0)
|
||||
public function __construct($msg = '', $data = [], $code = 0, $writeLog = true)
|
||||
{
|
||||
if (is_array($msg) && isset($msg['code'])) {
|
||||
$code = $msg['code'];
|
||||
@@ -24,6 +29,7 @@ class ApiException extends RuntimeException
|
||||
$msg = $msg['msg'];
|
||||
}
|
||||
$this->data = $data;
|
||||
$this->writeLog = $writeLog && $code !== -1;
|
||||
parent::__construct($msg, $code);
|
||||
}
|
||||
|
||||
@@ -34,4 +40,12 @@ class ApiException extends RuntimeException
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isWriteLog(): bool
|
||||
{
|
||||
return $this->writeLog;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class Handler extends ExceptionHandler
|
||||
public function report(Throwable $e)
|
||||
{
|
||||
if ($e instanceof ApiException) {
|
||||
if ($e->getCode() !== -1) {
|
||||
if ($e->isWriteLog()) {
|
||||
Log::error($e->getMessage(), [
|
||||
'code' => $e->getCode(),
|
||||
'data' => $e->getData(),
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Request;
|
||||
use Session;
|
||||
use Response;
|
||||
use Madzipper;
|
||||
use Carbon\Carbon;
|
||||
use App\Module\Down;
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
@@ -20,8 +20,10 @@ use App\Models\ApproveProcInstHistory;
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\UserDepartment;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Apps;
|
||||
use App\Module\BillMultipleExport;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Swoole\Coroutine;
|
||||
|
||||
/**
|
||||
* @apiDefine approve
|
||||
@@ -34,11 +36,12 @@ class ApproveController extends AbstractController
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
Apps::isInstalledThrow('approve');
|
||||
$this->flow_url = env('FLOW_URL') ?: 'http://approve';
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/approve/verifyToken 01. 验证APi登录
|
||||
* @api {get} api/approve/verifyToken 验证APi登录
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -60,7 +63,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/procdef/all 02. 查询流程定义
|
||||
* @api {post} api/approve/procdef/all 查询流程定义
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -87,7 +90,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/approve/procdef/del 03. 删除流程定义
|
||||
* @api {get} api/approve/procdef/del 删除流程定义
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -113,7 +116,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/process/start 04. 启动流程(审批中)
|
||||
* @api {post} api/approve/process/start 启动流程(审批中)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -176,7 +179,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/process/addGlobalComment 05. 添加全局评论
|
||||
* @api {post} api/approve/process/addGlobalComment 添加全局评论
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -221,7 +224,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/task/complete 06. 审批
|
||||
* @api {post} api/approve/task/complete 审批
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -301,7 +304,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/task/withdraw 07. 撤回
|
||||
* @api {post} api/approve/task/withdraw 撤回
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -346,7 +349,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/process/findTask 08. 查询需要我审批的流程(审批中)
|
||||
* @api {post} api/approve/process/findTask 查询需要我审批的流程(审批中)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -389,7 +392,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/process/startByMyselfAll 09. 查询我启动的流程(全部)
|
||||
* @api {post} api/approve/process/startByMyselfAll 查询我启动的流程(全部)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -432,7 +435,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/process/startByMyself 10. 查询我启动的流程(审批中)
|
||||
* @api {post} api/approve/process/startByMyself 查询我启动的流程(审批中)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -470,7 +473,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/process/findProcNotify 11. 查询抄送我的流程(审批中)
|
||||
* @api {post} api/approve/process/findProcNotify 查询抄送我的流程(审批中)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -514,7 +517,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/approve/identitylink/findParticipant 12. 查询流程实例的参与者(审批中)
|
||||
* @api {get} api/approve/identitylink/findParticipant 查询流程实例的参与者(审批中)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -549,7 +552,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/procHistory/findTask 13. 查询需要我审批的流程(已结束)
|
||||
* @api {post} api/approve/procHistory/findTask 查询需要我审批的流程(已结束)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -592,7 +595,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/procHistory/startByMyself 14. 查询我启动的流程(已结束)
|
||||
* @api {post} api/approve/procHistory/startByMyself 查询我启动的流程(已结束)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -630,7 +633,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/procHistory/findProcNotify 15. 查询抄送我的流程(已结束)
|
||||
* @api {post} api/approve/procHistory/findProcNotify 查询抄送我的流程(已结束)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -674,7 +677,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/approve/identitylinkHistory/findParticipant 16. 查询流程实例的参与者(已结束)
|
||||
* @api {get} api/approve/identitylinkHistory/findParticipant 查询流程实例的参与者(已结束)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -709,7 +712,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/approve/process/detail 17. 根据流程ID查询流程详情
|
||||
* @api {get} api/approve/process/detail 根据流程ID查询流程详情
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -731,7 +734,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/approve/export 18. 导出数据
|
||||
* @api {post} api/approve/export 导出数据
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -766,131 +769,192 @@ class ApproveController extends AbstractController
|
||||
if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) {
|
||||
return Base::retError('日期范围限制最大35天');
|
||||
}
|
||||
//
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findAllProcIns', json_encode($data));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '查询失败');
|
||||
$botUser = User::botGetOrCreate('system-msg');
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('系统机器人不存在');
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||
//
|
||||
$res = Base::arrayKeyToUnderline($process['data']);
|
||||
//
|
||||
$headings = [];
|
||||
$headings[] = Doo::translate('申请编号');
|
||||
$headings[] = Doo::translate('标题');
|
||||
$headings[] = Doo::translate('申请状态');
|
||||
$headings[] = Doo::translate('发起时间');
|
||||
$headings[] = Doo::translate('完成时间');
|
||||
$headings[] = Doo::translate('发起人工号');
|
||||
$headings[] = Doo::translate('发起人User ID');
|
||||
$headings[] = Doo::translate('发起人姓名');
|
||||
$headings[] = Doo::translate('发起人部门');
|
||||
$headings[] = Doo::translate('发起人部门ID');
|
||||
$headings[] = Doo::translate('部门负责人');
|
||||
$headings[] = Doo::translate('历史审批人');
|
||||
$headings[] = Doo::translate('历史办理人');
|
||||
$headings[] = Doo::translate('审批记录');
|
||||
$headings[] = Doo::translate('当前处理人');
|
||||
$headings[] = Doo::translate('审批节点');
|
||||
$headings[] = Doo::translate('审批人数');
|
||||
$headings[] = Doo::translate('审批耗时');
|
||||
$headings[] = Doo::translate('假期类型');
|
||||
$headings[] = Doo::translate('开始时间');
|
||||
$headings[] = Doo::translate('结束时间');
|
||||
$headings[] = Doo::translate('时长');
|
||||
$headings[] = Doo::translate('请假事由');
|
||||
$headings[] = Doo::translate('请假单位');
|
||||
//
|
||||
$datas = [];
|
||||
foreach ($res as $val) {
|
||||
$doo = Doo::load();
|
||||
go(function () use ($doo, $data, $user, $botUser, $dialog) {
|
||||
Coroutine::sleep(1);
|
||||
//
|
||||
$nickname = Base::filterEmoji($val['start_user_name']);
|
||||
$participant = $this->getUserProcessParticipantById($val['id']); // 获取参与人
|
||||
$participant = $this->handleParticipant($val, $participant['data']); // 处理参与人返回数据
|
||||
//
|
||||
$job_number = ''; // 发起人工号
|
||||
$department_leader = User::userid2nickname(UserDepartment::find(1, ['owner_userid'])['owner_userid']); // 部门负责人
|
||||
$historical_approver = $participant['historical_approver'] ?? ''; // 历史审批人
|
||||
$historical_agent = ''; // 历史办理人
|
||||
$approval_record = $participant['approval_record'] ?? ''; // 审批记录
|
||||
$current_handler = !$val['is_finished'] ? implode(',', User::whereIn('userid', explode(';', $val['candidate']))->pluck('nickname')->toArray()) : ''; // 当前处理人
|
||||
$approved_node = $participant['approved_node'] ?? 0; // 审批节点
|
||||
$approved_num = $participant['approved_num'] ?? 0; // 审批人数
|
||||
// 计算审批耗时
|
||||
$startTime = Carbon::parse($val['start_time'])->timestamp;
|
||||
$endTime = $val['end_time'] ? Carbon::parse($val['end_time'])->timestamp : time();
|
||||
$approval_time = Doo::translate(Timer::timeDiff($startTime, $endTime)); // 审批耗时
|
||||
// 计算时长
|
||||
$varStartTime = Carbon::parse($val['var']['start_time']);
|
||||
$varEndTime = Carbon::parse($val['var']['end_time']);
|
||||
$duration = $varEndTime->floatDiffInHours($varStartTime);
|
||||
$duration_unit = Doo::translate('小时'); // 时长单位
|
||||
$datas[] = [
|
||||
$val['id'], // 申请编号
|
||||
$val['proc_def_name'], // 标题
|
||||
$this->getStateDescription($val['state']), // 申请状态
|
||||
$val['start_time'], // 发起时间
|
||||
$val['end_time'], // 完成时间
|
||||
$job_number, // 发起人工号
|
||||
$val['start_user_id'], // 发起人User ID
|
||||
$nickname, // 发起人姓名
|
||||
$val['department'], // 发起人部门
|
||||
$val['department_id'], // 发起人部门ID
|
||||
$department_leader, // 部门负责人
|
||||
$historical_approver, // 历史审批人
|
||||
$historical_agent, // 历史办理人
|
||||
$approval_record, // 审批记录
|
||||
$current_handler, // 当前处理人
|
||||
$approved_node, // 审批节点
|
||||
$approved_num, // 审批人数
|
||||
$approval_time, // 审批耗时
|
||||
$val['var']['type'], // 假期类型
|
||||
$val['var']['start_time'], // 开始时间
|
||||
$val['var']['end_time'], // 结束时间
|
||||
$duration, // 时长
|
||||
$val['var']['description'], // 请假事由
|
||||
$duration_unit, // 请假单位
|
||||
$content = [];
|
||||
$content[] = [
|
||||
'content' => '导出审批数据已完成',
|
||||
'style' => 'font-weight: bold;padding-bottom: 4px;',
|
||||
];
|
||||
}
|
||||
if (empty($datas)) {
|
||||
return Base::retError('没有任何数据');
|
||||
}
|
||||
//
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findAllProcIns', json_encode($data));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
$content[] = [
|
||||
'content' => $process['message'] ?? '查询失败',
|
||||
'style' => 'color: #ff0000;',
|
||||
];
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'title' => $content[0]['content'],
|
||||
'content' => $content,
|
||||
], $botUser->userid, true, false, true);
|
||||
return;
|
||||
}
|
||||
//
|
||||
$res = Base::arrayKeyToUnderline($process['data']);
|
||||
//
|
||||
$headings = [];
|
||||
$headings[] = $doo->translate('申请编号');
|
||||
$headings[] = $doo->translate('标题');
|
||||
$headings[] = $doo->translate('申请状态');
|
||||
$headings[] = $doo->translate('发起时间');
|
||||
$headings[] = $doo->translate('完成时间');
|
||||
$headings[] = $doo->translate('发起人工号');
|
||||
$headings[] = $doo->translate('发起人User ID');
|
||||
$headings[] = $doo->translate('发起人姓名');
|
||||
$headings[] = $doo->translate('发起人部门');
|
||||
$headings[] = $doo->translate('发起人部门ID');
|
||||
$headings[] = $doo->translate('部门负责人');
|
||||
$headings[] = $doo->translate('历史审批人');
|
||||
$headings[] = $doo->translate('历史办理人');
|
||||
$headings[] = $doo->translate('审批记录');
|
||||
$headings[] = $doo->translate('当前处理人');
|
||||
$headings[] = $doo->translate('审批节点');
|
||||
$headings[] = $doo->translate('审批人数');
|
||||
$headings[] = $doo->translate('审批耗时');
|
||||
$headings[] = $doo->translate('假期类型');
|
||||
$headings[] = $doo->translate('开始时间');
|
||||
$headings[] = $doo->translate('结束时间');
|
||||
$headings[] = $doo->translate('时长');
|
||||
$headings[] = $doo->translate('请假事由');
|
||||
$headings[] = $doo->translate('请假单位');
|
||||
//
|
||||
$datas = [];
|
||||
foreach ($res as $val) {
|
||||
//
|
||||
$nickname = Base::filterEmoji($val['start_user_name']);
|
||||
$participant = $this->getUserProcessParticipantById($val['id']); // 获取参与人
|
||||
$participant = $this->handleParticipant($val, $participant['data']); // 处理参与人返回数据
|
||||
//
|
||||
$job_number = ''; // 发起人工号
|
||||
$department_leader = User::userid2nickname(UserDepartment::find(1, ['owner_userid'])['owner_userid']); // 部门负责人
|
||||
$historical_approver = $participant['historical_approver'] ?? ''; // 历史审批人
|
||||
$historical_agent = ''; // 历史办理人
|
||||
$approval_record = $participant['approval_record'] ?? ''; // 审批记录
|
||||
$current_handler = !$val['is_finished'] ? implode(',', User::whereIn('userid', explode(';', $val['candidate']))->pluck('nickname')->toArray()) : ''; // 当前处理人
|
||||
$approved_node = $participant['approved_node'] ?? 0; // 审批节点
|
||||
$approved_num = $participant['approved_num'] ?? 0; // 审批人数
|
||||
// 计算审批耗时
|
||||
$startTime = Carbon::parse($val['start_time'])->timestamp;
|
||||
$endTime = $val['end_time'] ? Carbon::parse($val['end_time'])->timestamp : time();
|
||||
$approval_time = $doo->translate(Timer::timeDiff($startTime, $endTime)); // 审批耗时
|
||||
// 计算时长
|
||||
$varStartTime = Carbon::parse($val['var']['start_time']);
|
||||
$varEndTime = Carbon::parse($val['var']['end_time']);
|
||||
$duration = $varEndTime->floatDiffInHours($varStartTime);
|
||||
$duration_unit = $doo->translate('小时'); // 时长单位
|
||||
$datas[] = [
|
||||
$val['id'], // 申请编号
|
||||
$val['proc_def_name'], // 标题
|
||||
$this->getStateDescription($val['state']), // 申请状态
|
||||
$val['start_time'], // 发起时间
|
||||
$val['end_time'], // 完成时间
|
||||
$job_number, // 发起人工号
|
||||
$val['start_user_id'], // 发起人User ID
|
||||
$nickname, // 发起人姓名
|
||||
$val['department'], // 发起人部门
|
||||
$val['department_id'], // 发起人部门ID
|
||||
$department_leader, // 部门负责人
|
||||
$historical_approver, // 历史审批人
|
||||
$historical_agent, // 历史办理人
|
||||
$approval_record, // 审批记录
|
||||
$current_handler, // 当前处理人
|
||||
$approved_node, // 审批节点
|
||||
$approved_num, // 审批人数
|
||||
$approval_time, // 审批耗时
|
||||
$val['var']['type'], // 假期类型
|
||||
$val['var']['start_time'], // 开始时间
|
||||
$val['var']['end_time'], // 结束时间
|
||||
$duration, // 时长
|
||||
$val['var']['description'], // 请假事由
|
||||
$duration_unit, // 请假单位
|
||||
];
|
||||
}
|
||||
if (empty($datas)) {
|
||||
$content[] = [
|
||||
'content' => '没有任何数据',
|
||||
'style' => 'color: #ff0000;',
|
||||
];
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'title' => $content[0]['content'],
|
||||
'content' => $content,
|
||||
], $botUser->userid, true, false, true);
|
||||
return;
|
||||
}
|
||||
//
|
||||
$title = $doo->translate("审批记录");
|
||||
$sheets = [
|
||||
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles(["A1:Y1" => ["font" => ["bold" => true]]])
|
||||
];
|
||||
//
|
||||
$fileName = $title . '_' . Timer::time() . '.xlsx';
|
||||
$filePath = "temp/approve/export/" . date("Ym", Timer::time());
|
||||
$export = new BillMultipleExport($sheets);
|
||||
$res = $export->store($filePath . "/" . $fileName);
|
||||
if ($res != 1) {
|
||||
$content[] = [
|
||||
'content' => "导出失败,{$fileName}!",
|
||||
'style' => 'color: #ff0000;',
|
||||
];
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'title' => $content[0]['content'],
|
||||
'content' => $content,
|
||||
], $botUser->userid, true, false, true);
|
||||
return;
|
||||
}
|
||||
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
|
||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
|
||||
$zipPath = storage_path($zipFile);
|
||||
if (file_exists($zipPath)) {
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
if (file_exists($zipPath)) {
|
||||
$key = Down::cache_encode([
|
||||
'file' => $zipFile,
|
||||
]);
|
||||
$fileUrl = Base::fillUrl('api/approve/down?key=' . $key);
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'file_download',
|
||||
'title' => '导出审批数据已完成',
|
||||
'name' => $fileName,
|
||||
'size' => filesize($zipPath),
|
||||
'url' => $fileUrl,
|
||||
], $botUser->userid, true, false, true);
|
||||
} else {
|
||||
$content[] = [
|
||||
'content' => "打包失败,请稍后再试...",
|
||||
'style' => 'color: #ff0000;',
|
||||
];
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'title' => $content[0]['content'],
|
||||
'content' => $content,
|
||||
], $botUser->userid, true, false, true);
|
||||
}
|
||||
});
|
||||
//
|
||||
$title = Doo::translate("审批记录");
|
||||
$sheets = [
|
||||
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles(["A1:Y1" => ["font" => ["bold" => true]]])
|
||||
];
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'content' => '正在导出审批数据,请稍等...',
|
||||
], $botUser->userid, true, false, true);
|
||||
//
|
||||
$fileName = $title . '_' . Timer::time() . '.xlsx';
|
||||
$filePath = "temp/approve/export/" . date("Ym", Timer::time());
|
||||
$export = new BillMultipleExport($sheets);
|
||||
$res = $export->store($filePath . "/" . $fileName);
|
||||
if ($res != 1) {
|
||||
return Base::retError('导出失败,' . $fileName . '!');
|
||||
}
|
||||
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
|
||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
|
||||
$zipPath = storage_path($zipFile);
|
||||
if (file_exists($zipPath)) {
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
if (file_exists($zipPath)) {
|
||||
$base64 = base64_encode(Base::array2string([
|
||||
'file' => $zipFile,
|
||||
]));
|
||||
Session::put('approve::export:userid', $user->userid);
|
||||
return Base::retSuccess('success', [
|
||||
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
|
||||
'url' => Base::fillUrl('api/approve/down?key=' . urlencode($base64)),
|
||||
]);
|
||||
} else {
|
||||
return Base::retError('打包失败,请稍后再试...');
|
||||
}
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
|
||||
function getStateDescription($state)
|
||||
@@ -906,7 +970,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/approve/down 19. 下载导出的审批数据
|
||||
* @api {get} api/approve/down 下载导出的审批数据
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup approve
|
||||
@@ -918,15 +982,10 @@ class ApproveController extends AbstractController
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
$userid = Session::get('approve::export:userid');
|
||||
if (empty($userid)) {
|
||||
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502);
|
||||
}
|
||||
//
|
||||
$array = Base::string2array(base64_decode(urldecode(Request::input('key'))));
|
||||
$array = Down::cache_decode();
|
||||
$file = $array['file'];
|
||||
if (empty($file) || !file_exists(storage_path($file))) {
|
||||
return Base::ajaxError("文件不存在!", [], 0, 502);
|
||||
return Base::ajaxError("文件不存在!", [], 0, 403);
|
||||
}
|
||||
return Response::download(storage_path($file));
|
||||
}
|
||||
@@ -1133,7 +1192,7 @@ class ApproveController extends AbstractController
|
||||
|
||||
|
||||
/**
|
||||
* @api {get} api/approve/user/status 20. 获取用户审批状态
|
||||
* @api {get} api/approve/user/status 获取用户审批状态
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup approve
|
||||
@@ -1153,7 +1212,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/approve/process/doto 21. 查询需要我审批的流程数量
|
||||
* @api {get} api/approve/process/doto 查询需要我审批的流程数量
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
|
||||
73
app/Http/Controllers/Api/AssistantController.php
Normal file
73
app/Http/Controllers/Api/AssistantController.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Module\AI;
|
||||
use App\Module\Apps;
|
||||
use App\Module\Base;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
* @apiDefine assistant
|
||||
*
|
||||
* 助手
|
||||
*/
|
||||
class AssistantController extends AbstractController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
Apps::isInstalledThrow('ai');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/assistant/auth 生成授权码
|
||||
*
|
||||
* @apiDescription 需要token身份,生成 AI 流式会话的 stream_key
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup assistant
|
||||
* @apiName auth
|
||||
*
|
||||
* @apiParam {String} model_type 模型类型
|
||||
* @apiParam {String} model_name 模型名称
|
||||
* @apiParam {JSON} context 上下文数组
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
* @apiSuccess {String} data.stream_key 流式会话凭证
|
||||
*/
|
||||
public function auth()
|
||||
{
|
||||
$user = User::auth();
|
||||
$user->checkChatInformation();
|
||||
|
||||
$modelType = trim(Request::input('model_type', ''));
|
||||
$modelName = trim(Request::input('model_name', ''));
|
||||
$contextInput = Request::input('context', []);
|
||||
|
||||
return AI::createStreamKey($modelType, $modelName, $contextInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/assistant/models 获取AI模型
|
||||
*
|
||||
* @apiDescription 获取所有AI机器人模型设置
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup assistant
|
||||
* @apiName models
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function 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('{}'));
|
||||
}
|
||||
}
|
||||
@@ -10,18 +10,18 @@ use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
|
||||
/**
|
||||
* @apiDefine dialog
|
||||
* @apiDefine complaint
|
||||
*
|
||||
* 投诉
|
||||
*/
|
||||
class ComplaintController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @api {get} api/complaint/lists 01. 获取举报投诉列表
|
||||
* @api {get} api/complaint/lists 获取举报投诉列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiGroup complaint
|
||||
* @apiName lists
|
||||
*
|
||||
* @apiParam {Number} [type] 类型
|
||||
@@ -33,6 +33,34 @@ class ComplaintController extends AbstractController
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response-Data:
|
||||
* {
|
||||
* "current_page": 1,
|
||||
* "data": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "dialog_id": 100,
|
||||
* "userid": 1,
|
||||
* "type": 1,
|
||||
* "reason": "举报原因",
|
||||
* "imgs": [],
|
||||
* "status": 0,
|
||||
* "created_at": "2025-01-01 00:00:00",
|
||||
* "updated_at": "2025-01-01 00:00:00"
|
||||
* }
|
||||
* ],
|
||||
* "first_page_url": "http://example.com/api/complaint/lists?page=1",
|
||||
* "from": 1,
|
||||
* "last_page": 1,
|
||||
* "last_page_url": "http://example.com/api/complaint/lists?page=1",
|
||||
* "next_page_url": null,
|
||||
* "path": "http://example.com/api/complaint/lists",
|
||||
* "per_page": 50,
|
||||
* "prev_page_url": null,
|
||||
* "to": 1,
|
||||
* "total": 1
|
||||
* }
|
||||
*/
|
||||
public function lists()
|
||||
{
|
||||
@@ -56,21 +84,25 @@ class ComplaintController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/complaint/submit 02. 举报投诉
|
||||
* @api {post} api/complaint/submit 举报投诉
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiGroup complaint
|
||||
* @apiName submit
|
||||
*
|
||||
* @apiParam {Number} dialog_id 对话ID
|
||||
* @apiParam {Number} type 类型
|
||||
* @apiParam {String} reason 原因
|
||||
* @apiParam {String} imgs 图片
|
||||
* @apiBody {Number} dialog_id 对话ID
|
||||
* @apiBody {Number} type 类型
|
||||
* @apiBody {String} reason 原因
|
||||
* @apiBody {Object[]} [imgs] 图片数组(可选)
|
||||
* @apiBody {String} imgs.path 图片路径
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response-Data:
|
||||
* []
|
||||
*/
|
||||
public function submit()
|
||||
{
|
||||
@@ -125,19 +157,22 @@ class ComplaintController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/complaint/action 03. 举报投诉 - 操作
|
||||
* @api {post} api/complaint/action 举报投诉 - 操作
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiDescription 需要token身份(管理员权限)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiGroup complaint
|
||||
* @apiName action
|
||||
*
|
||||
* @apiParam {Number} id ID
|
||||
* @apiParam {Number} type 类型
|
||||
* @apiBody {Number} id 投诉ID
|
||||
* @apiBody {String} type 操作类型:handle=已处理,delete=删除
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response-Data:
|
||||
* []
|
||||
*/
|
||||
public function action()
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,11 +11,12 @@ use App\Models\FileContent;
|
||||
use App\Models\FileLink;
|
||||
use App\Models\FileUser;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRecentItem;
|
||||
use App\Module\Base;
|
||||
use App\Module\Down;
|
||||
use App\Module\Timer;
|
||||
use App\Module\Ihttp;
|
||||
use Response;
|
||||
use Session;
|
||||
use Swoole\Coroutine;
|
||||
use Carbon\Carbon;
|
||||
use Redirect;
|
||||
@@ -30,7 +31,7 @@ use ZipArchive;
|
||||
class FileController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @api {get} api/file/lists 01. 获取文件列表
|
||||
* @api {get} api/file/lists 获取文件列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -53,7 +54,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/one 02. 获取单条数据
|
||||
* @api {get} api/file/one 获取单条数据
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -63,6 +64,9 @@ class FileController extends AbstractController
|
||||
* @apiParam {Number|String} id
|
||||
* - Number 文件ID(需要登录)
|
||||
* - String 链接码(不需要登录,用于预览)
|
||||
* @apiParam {String} [with_url] 是否返回文件访问URL
|
||||
* - no: 不返回(默认)
|
||||
* - yes: 返回content_url字段
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -71,11 +75,12 @@ class FileController extends AbstractController
|
||||
public function one()
|
||||
{
|
||||
$id = Request::input('id');
|
||||
$with_url = Request::input('with_url', 'no');
|
||||
//
|
||||
$permission = 0;
|
||||
if (Base::isNumber($id)) {
|
||||
$user = User::auth();
|
||||
$file = File::permissionFind(intval($id), $user, 0, $permission);
|
||||
$file = File::permissionFind(intval($id), $user, $with_url === 'yes' ? 1 : 0, $permission);
|
||||
} elseif ($id) {
|
||||
$fileLink = FileLink::whereCode($id)->first();
|
||||
$file = $fileLink?->file;
|
||||
@@ -87,17 +92,30 @@ class FileController extends AbstractController
|
||||
}
|
||||
return Base::retError($msg, $data);
|
||||
}
|
||||
|
||||
// 如果文件不允许游客访问,则需要登录
|
||||
if (!$file->guest_access) {
|
||||
User::auth();
|
||||
}
|
||||
|
||||
$fileLink->increment("num");
|
||||
} else {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
//
|
||||
$array = $file->toArray();
|
||||
$array['permission'] = $permission;
|
||||
|
||||
// 如果请求返回文件URL
|
||||
if ($with_url === 'yes') {
|
||||
$array['content_url'] = FileContent::getFileUrl($file->id);
|
||||
}
|
||||
|
||||
return Base::retSuccess('success', $array);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/search 03. 搜索文件列表
|
||||
* @api {get} api/file/search 搜索文件列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -106,6 +124,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 返回信息(错误描述)
|
||||
@@ -118,7 +137,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;
|
||||
@@ -132,7 +151,11 @@ class FileController extends AbstractController
|
||||
$builder->where("id", $id);
|
||||
}
|
||||
if ($key) {
|
||||
$builder->where("name", "like", "%{$key}%");
|
||||
if (!$id && Base::isNumber($key)) {
|
||||
$builder->where("id", $key);
|
||||
} else {
|
||||
$builder->where("name", "like", "%{$key}%");
|
||||
}
|
||||
}
|
||||
$array = $builder->take($take)->get()->toArray();
|
||||
// 搜索共享的
|
||||
@@ -169,7 +192,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/add 04. 添加、修改文件(夹)
|
||||
* @api {get} api/file/add 添加、修改文件(夹)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -195,8 +218,8 @@ class FileController extends AbstractController
|
||||
$pid = intval(Request::input('pid'));
|
||||
if (mb_strlen($name) < 2) {
|
||||
return Base::retError('文件名称不可以少于2个字');
|
||||
} elseif (mb_strlen($name) > 32) {
|
||||
return Base::retError('文件名称最多只能设置32个字');
|
||||
} elseif (mb_strlen($name) > 100) {
|
||||
return Base::retError('文件名称最多只能设置100个字');
|
||||
}
|
||||
$tmpName = preg_replace("/[\\\\\/:*?\"<>|]/", '', $name);
|
||||
if ($tmpName != $name) {
|
||||
@@ -278,7 +301,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/copy 05. 复制文件(夹)
|
||||
* @api {get} api/file/copy 复制文件(夹)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -339,7 +362,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/move 06. 移动文件(夹)
|
||||
* @api {get} api/file/move 移动文件(夹)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -414,7 +437,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/remove 07. 删除文件(夹)
|
||||
* @api {get} api/file/remove 删除文件(夹)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -453,7 +476,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/content 08. 获取文件内容
|
||||
* @api {get} api/file/content 获取文件内容
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -501,6 +524,10 @@ class FileController extends AbstractController
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
//
|
||||
if ($down == 'no') {
|
||||
File::isNeedInstallApp($file->type);
|
||||
}
|
||||
//
|
||||
if ($only_update_at == 'yes') {
|
||||
return Base::retSuccess('success', [
|
||||
'id' => $file->id,
|
||||
@@ -513,6 +540,16 @@ class FileController extends AbstractController
|
||||
$builder->whereId($history_id);
|
||||
}
|
||||
$content = $builder->orderByDesc('id')->first();
|
||||
if (isset($user)) {
|
||||
UserRecentItem::record(
|
||||
$user->userid,
|
||||
UserRecentItem::TYPE_FILE,
|
||||
$file->id,
|
||||
UserRecentItem::SOURCE_FILESYSTEM,
|
||||
intval($file->pid)
|
||||
);
|
||||
}
|
||||
|
||||
if ($down === 'preview') {
|
||||
return Redirect::to(FileContent::formatPreview($file, $content?->content));
|
||||
}
|
||||
@@ -520,7 +557,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/content/save 09. 保存文件内容
|
||||
* @api {get} api/file/content/save 保存文件内容
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -575,10 +612,12 @@ class FileController extends AbstractController
|
||||
$contentArray = Base::json2array($content);
|
||||
$contentString = $contentArray['xml'];
|
||||
$file->ext = 'drawio';
|
||||
File::isNeedInstallApp($file->type);
|
||||
break;
|
||||
case 'mind':
|
||||
$contentString = $content;
|
||||
$file->ext = 'mind';
|
||||
File::isNeedInstallApp($file->type);
|
||||
break;
|
||||
case 'txt':
|
||||
case 'code':
|
||||
@@ -613,9 +652,9 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/office/token 10. 获取token
|
||||
* @api {get} api/file/office/token 获取token
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiDescription 用于生成office在线编辑的token
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup file
|
||||
* @apiName office__token
|
||||
@@ -628,7 +667,7 @@ class FileController extends AbstractController
|
||||
*/
|
||||
public function office__token()
|
||||
{
|
||||
User::auth();
|
||||
File::isNeedInstallApp('office');
|
||||
//
|
||||
$config = Request::input('config');
|
||||
$token = \Firebase\JWT\JWT::encode($config, env('APP_KEY') ,'HS256');
|
||||
@@ -638,7 +677,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/content/office 11. 保存文件内容(office)
|
||||
* @api {get} api/file/content/office 保存文件内容(office)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -655,6 +694,8 @@ class FileController extends AbstractController
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
File::isNeedInstallApp('office');
|
||||
//
|
||||
$id = intval(Request::input('id'));
|
||||
$status = intval(Request::input('status'));
|
||||
$key = Request::input('key');
|
||||
@@ -664,7 +705,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));
|
||||
@@ -692,7 +733,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/content/upload 12. 保存文件内容(上传文件)
|
||||
* @api {get} api/file/content/upload 保存文件内容(上传文件)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -720,7 +761,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/content/history 13. 获取内容历史
|
||||
* @api {get} api/file/content/history 获取内容历史
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -752,7 +793,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/content/restore 14. 恢复文件历史
|
||||
* @api {get} api/file/content/restore 恢复文件历史
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -775,6 +816,8 @@ class FileController extends AbstractController
|
||||
//
|
||||
$file = File::permissionFind($id, $user);
|
||||
//
|
||||
File::isNeedInstallApp($file->type);
|
||||
//
|
||||
$history = FileContent::whereFid($file->id)->whereId($history_id)->first();
|
||||
if (empty($history)) {
|
||||
return Base::retError('历史数据不存在或已被删除');
|
||||
@@ -792,7 +835,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/share 15. 获取共享信息
|
||||
* @api {get} api/file/share 获取共享信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -828,7 +871,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/share/update 16. 设置共享
|
||||
* @api {get} api/file/share/update 设置共享
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -918,7 +961,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/share/out 17. 退出共享
|
||||
* @api {get} api/file/share/out 退出共享
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -952,7 +995,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/link 18. 获取链接
|
||||
* @api {get} api/file/link 获取链接
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -963,6 +1006,9 @@ class FileController extends AbstractController
|
||||
* @apiParam {String} refresh 刷新链接
|
||||
* - no: 只获取(默认)
|
||||
* - yes: 刷新链接,之前的将失效
|
||||
* @apiParam {String} guest_access 是否允许游客访问
|
||||
* - no: 不允许(默认)
|
||||
* - yes: 允许游客访问
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -974,15 +1020,22 @@ class FileController extends AbstractController
|
||||
//
|
||||
$id = intval(Request::input('id'));
|
||||
$refresh = Request::input('refresh', 'no');
|
||||
$guestAccess = Request::input('guest_access', 'no');
|
||||
//
|
||||
$file = File::permissionFind($id, $user);
|
||||
|
||||
// 更新文件的游客访问权限
|
||||
$file->guest_access = $guestAccess === 'yes' ? 1 : 0;
|
||||
$file->save();
|
||||
|
||||
$fileLink = $file->getShareLink($user->userid, $refresh == 'yes');
|
||||
$fileLink['guest_access'] = $file->guest_access;
|
||||
//
|
||||
return Base::retSuccess('success', $fileLink);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/download/pack 19. 打包文件
|
||||
* @api {get} api/file/download/pack 打包文件
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -998,17 +1051,11 @@ class FileController extends AbstractController
|
||||
*/
|
||||
public function download__pack()
|
||||
{
|
||||
$key = Request::input('key');
|
||||
if ($key) {
|
||||
$userid = Session::get('file::pack:userid');
|
||||
if (empty($userid)) {
|
||||
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502);
|
||||
}
|
||||
//
|
||||
$array = Base::string2array(base64_decode(urldecode($key)));
|
||||
if (Request::has('key')) {
|
||||
$array = Down::cache_decode();
|
||||
$file = $array['file'];
|
||||
if (empty($file) || !file_exists(storage_path($file))) {
|
||||
return Base::ajaxError("文件不存在!", [], 0, 502);
|
||||
return Base::ajaxError("文件不存在!", [], 0, 403);
|
||||
}
|
||||
return Response::download(storage_path($file));
|
||||
}
|
||||
@@ -1073,11 +1120,10 @@ class FileController extends AbstractController
|
||||
return Base::retError('文件总大小已超过1GB,请分批下载');
|
||||
}
|
||||
|
||||
$base64 = base64_encode(Base::array2string([
|
||||
$key = Down::cache_encode([
|
||||
'file' => $zipFile,
|
||||
]));
|
||||
$fileUrl = Base::fillUrl('api/file/download/pack?key=' . urlencode($base64));
|
||||
Session::put('file::pack:userid', $user->userid);
|
||||
]);
|
||||
$fileUrl = Base::fillUrl('api/file/download/pack?key=' . $key);
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
Base::makeDir(dirname($zipPath));
|
||||
@@ -1086,17 +1132,18 @@ class FileController extends AbstractController
|
||||
return Base::retError('创建压缩文件失败');
|
||||
}
|
||||
|
||||
go(function () use ($zipPath, $fileUrl, $zip, $files, $fileName, $botUser, $dialog) {
|
||||
$userid = $user->userid;
|
||||
go(function () use ($userid, $zipPath, $fileUrl, $zip, $files, $fileName, $botUser, $dialog) {
|
||||
Coroutine::sleep(0.1);
|
||||
// 压缩进度
|
||||
$progress = 0;
|
||||
$zip->registerProgressCallback(0.05, function ($ratio) use ($fileUrl, $fileName, &$progress) {
|
||||
$zip->registerProgressCallback(0.05, function ($ratio) use ($userid, $fileUrl, $fileName, &$progress) {
|
||||
$progress = round($ratio * 100);
|
||||
File::filePushMsg('compress', [
|
||||
File::pushMsgSimple('compress', [
|
||||
'name' => $fileName,
|
||||
'url' => $fileUrl,
|
||||
'progress' => $progress
|
||||
]);
|
||||
], $userid);
|
||||
});
|
||||
//
|
||||
foreach ($files as $file) {
|
||||
@@ -1105,11 +1152,11 @@ class FileController extends AbstractController
|
||||
$zip->close();
|
||||
//
|
||||
if ($progress < 100) {
|
||||
File::filePushMsg('compress', [
|
||||
File::pushMsgSimple('compress', [
|
||||
'name' => $fileName,
|
||||
'url' => $fileUrl,
|
||||
'progress' => 100
|
||||
]);
|
||||
], $userid);
|
||||
}
|
||||
//
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,11 @@ use App\Exceptions\ApiException;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\Report;
|
||||
use App\Models\ReportAnalysis;
|
||||
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;
|
||||
@@ -26,13 +29,15 @@ use Illuminate\Support\Facades\Validator;
|
||||
class ReportController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @api {get} api/report/my 01. 我发送的汇报
|
||||
* @api {get} api/report/my 我发送的汇报
|
||||
*
|
||||
* @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 +54,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']);
|
||||
}
|
||||
@@ -62,15 +76,18 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/receive 02. 我接收的汇报
|
||||
* @api {get} api/report/receive 我接收的汇报
|
||||
*
|
||||
* @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 +106,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());
|
||||
@@ -113,8 +144,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/store 03. 保存并发送工作汇报
|
||||
* @api {get} api/report/store 保存并发送工作汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName store
|
||||
@@ -190,7 +222,6 @@ class ReportController extends AbstractController
|
||||
$report->updateInstance([
|
||||
"title" => $input["title"],
|
||||
"type" => $input["type"],
|
||||
"content" => htmlspecialchars($input["content"]),
|
||||
]);
|
||||
} else {
|
||||
// 生成唯一标识
|
||||
@@ -204,11 +235,25 @@ class ReportController extends AbstractController
|
||||
"title" => $input["title"],
|
||||
"type" => $input["type"],
|
||||
"userid" => $user->userid,
|
||||
"content" => htmlspecialchars($input["content"]),
|
||||
]);
|
||||
}
|
||||
$report->save();
|
||||
|
||||
// 保存内容
|
||||
$content = $input["content"];
|
||||
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp);base64,(.*?)\"/s", $content, $matchs);
|
||||
foreach ($matchs[2] as $key => $text) {
|
||||
$tmpPath = "uploads/report/" . Carbon::parse($report->created_at)->format("Ym") . "/" . $report->id . "/attached/";
|
||||
Base::makeDir(public_path($tmpPath));
|
||||
$tmpPath .= md5($text) . "." . $matchs[1][$key];
|
||||
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
|
||||
$paramet = getimagesize(public_path($tmpPath));
|
||||
$content = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($tmpPath) . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
|
||||
}
|
||||
}
|
||||
$report->content = htmlspecialchars($content);
|
||||
$report->save();
|
||||
|
||||
// 删除关联
|
||||
$report->Receives()->delete();
|
||||
if ($input["receive_content"]) {
|
||||
@@ -238,8 +283,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/template 04. 生成汇报模板
|
||||
* @api {get} api/report/template 生成汇报模板
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName template
|
||||
@@ -409,13 +455,15 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/detail 05. 报告详情
|
||||
* @api {get} api/report/detail 报告详情
|
||||
*
|
||||
* @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 +472,145 @@ 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");
|
||||
}
|
||||
$analysis = ReportAnalysis::query()
|
||||
->whereRid($one->id)
|
||||
->whereUserid($user->userid)
|
||||
->first();
|
||||
if ($analysis) {
|
||||
$updatedAt = $analysis->updated_at ? $analysis->updated_at->toDateTimeString() : null;
|
||||
$one->setAttribute('ai_analysis', [
|
||||
'id' => $analysis->id,
|
||||
'text' => $analysis->analysis_text,
|
||||
'model' => $analysis->model,
|
||||
'updated_at' => $updatedAt,
|
||||
]);
|
||||
} else {
|
||||
$one->setAttribute('ai_analysis', null);
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", $one);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/mark 06. 标记已读/未读
|
||||
* @api {post} api/report/analysave 保存工作汇报 AI 分析
|
||||
*
|
||||
* @apiDescription 需要token身份,仅支持报告提交人或接收人保存分析
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName analysave
|
||||
*
|
||||
* @apiParam {Number} id 报告ID
|
||||
* @apiParam {String} text 分析内容(Markdown)
|
||||
* @apiParam {String} [model] 分析使用的模型标识(可选)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
* @apiSuccess {Number} data.id 分析记录ID
|
||||
* @apiSuccess {String} data.text 分析内容(Markdown)
|
||||
* @apiSuccess {String} data.updated_at 最近更新时间
|
||||
*/
|
||||
public function analysave(): array
|
||||
{
|
||||
$user = User::auth();
|
||||
$id = intval(Request::input("id"));
|
||||
if ($id <= 0) {
|
||||
return Base::retError("缺少ID参数");
|
||||
}
|
||||
$text = trim((string)Request::input('text', ''));
|
||||
if ($text === '') {
|
||||
return Base::retError("分析内容不能为空");
|
||||
}
|
||||
$model = trim((string)Request::input('model', ''));
|
||||
|
||||
$report = Report::getOne($id);
|
||||
if (!$this->userCanAccessReport($report, $user)) {
|
||||
return Base::retError("无权访问该工作汇报");
|
||||
}
|
||||
|
||||
$analysis = ReportAnalysis::query()
|
||||
->whereRid($report->id)
|
||||
->whereUserid($user->userid)
|
||||
->first();
|
||||
|
||||
if (!$analysis) {
|
||||
$analysis = ReportAnalysis::fillInstance([
|
||||
'rid' => $report->id,
|
||||
'userid' => $user->userid,
|
||||
]);
|
||||
}
|
||||
|
||||
$viewerRole = $user->profession ?: (is_array($user->identity) && !empty($user->identity) ? implode('/', $user->identity) : null);
|
||||
$focusMeta = null;
|
||||
$focus = Request::input('focus');
|
||||
if (is_array($focus)) {
|
||||
$focusMeta = array_filter(array_map('trim', $focus));
|
||||
} elseif (is_string($focus) && trim($focus) !== '') {
|
||||
$focusMeta = [trim($focus)];
|
||||
}
|
||||
|
||||
$meta = array_filter([
|
||||
'viewer_role' => $viewerRole,
|
||||
'viewer_name' => $user->nickname ?? null,
|
||||
'focus' => $focusMeta,
|
||||
], function ($value) {
|
||||
if (is_array($value)) {
|
||||
return !empty($value);
|
||||
}
|
||||
return $value !== null && $value !== '';
|
||||
});
|
||||
|
||||
$analysis->updateInstance([
|
||||
'model' => $model,
|
||||
'analysis_text' => $text,
|
||||
'meta' => $meta,
|
||||
]);
|
||||
$analysis->save();
|
||||
|
||||
$analysis->refresh();
|
||||
|
||||
return Base::retSuccess("success", [
|
||||
'id' => $analysis->id,
|
||||
'text' => $analysis->analysis_text,
|
||||
'model' => $analysis->model,
|
||||
'updated_at' => $analysis->updated_at ? $analysis->updated_at->toDateTimeString() : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/mark 标记已读/未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName mark
|
||||
@@ -488,8 +651,71 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人
|
||||
* @api {get} api/report/share 分享报告到消息
|
||||
*
|
||||
* @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 获取最后一次提交的接收人
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName last_submitter
|
||||
@@ -505,8 +731,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/unread 08. 获取未读
|
||||
* @api {get} api/report/unread 获取未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName unread
|
||||
@@ -529,8 +756,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/read 09. 标记汇报已读,可批量
|
||||
* @api {get} api/report/read 标记汇报已读,可批量
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName read
|
||||
@@ -566,4 +794,22 @@ class ReportController extends AbstractController
|
||||
}
|
||||
return Base::retSuccess("success", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前用户是否有权限查看/分析指定工作汇报
|
||||
* @param Report $report
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
protected function userCanAccessReport(Report $report, User $user): bool
|
||||
{
|
||||
if ($report->userid === $user->userid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ReportReceive::query()
|
||||
->whereRid($report->id)
|
||||
->whereUserid($user->userid)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\UserDevice;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\AI;
|
||||
use App\Module\Down;
|
||||
use Request;
|
||||
use Session;
|
||||
use Response;
|
||||
use Madzipper;
|
||||
use Carbon\Carbon;
|
||||
@@ -14,14 +16,15 @@ use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Module\Timer;
|
||||
use App\Models\Setting;
|
||||
use App\Module\Extranet;
|
||||
use LdapRecord\Container;
|
||||
use App\Module\BillExport;
|
||||
use Guanguans\Notify\Factory;
|
||||
use App\Models\UserCheckinRecord;
|
||||
use App\Module\Apps;
|
||||
use App\Module\BillMultipleExport;
|
||||
use LdapRecord\LdapRecordException;
|
||||
use Guanguans\Notify\Messages\EmailMessage;
|
||||
use Swoole\Coroutine;
|
||||
|
||||
/**
|
||||
* @apiDefine system
|
||||
@@ -32,7 +35,7 @@ class SystemController extends AbstractController
|
||||
{
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting 01. 获取设置、保存设置
|
||||
* @api {get} api/system/setting 获取设置、保存设置
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -41,7 +44,7 @@ class SystemController extends AbstractController
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - all: 获取所有(需要管理员权限)
|
||||
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local', 'start_home'])
|
||||
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'convert_video', 'compress_video', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local'])
|
||||
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -68,9 +71,11 @@ class SystemController extends AbstractController
|
||||
'project_invite',
|
||||
'chat_information',
|
||||
'anon_message',
|
||||
'voice2text',
|
||||
'translation',
|
||||
'convert_video',
|
||||
'compress_video',
|
||||
'e2e_message',
|
||||
'msg_rev_limit',
|
||||
'msg_edit_limit',
|
||||
'auto_archived',
|
||||
'archived_day',
|
||||
'task_visible',
|
||||
@@ -84,7 +89,6 @@ class SystemController extends AbstractController
|
||||
'image_compress',
|
||||
'image_quality',
|
||||
'image_save_local',
|
||||
'start_home',
|
||||
'file_upload_limit',
|
||||
'unclaimed_task_reminder',
|
||||
'unclaimed_task_reminder_time',
|
||||
@@ -100,12 +104,6 @@ class SystemController extends AbstractController
|
||||
return Base::retError('自动归档时间不可大于100天!');
|
||||
}
|
||||
}
|
||||
if ($all['voice2text'] == 'open' && !Setting::AIOpen()) {
|
||||
return Base::retError('开启语音转文字功能需要在应用中开启 ChatGPT AI 机器人。');
|
||||
}
|
||||
if ($all['translation'] == 'open' && !Setting::AIOpen()) {
|
||||
return Base::retError('开启翻译功能需要在应用中开启 ChatGPT AI 机器人。');
|
||||
}
|
||||
if ($all['system_alias'] == env('APP_NAME')) {
|
||||
$all['system_alias'] = '';
|
||||
}
|
||||
@@ -132,9 +130,11 @@ class SystemController extends AbstractController
|
||||
$setting['project_invite'] = $setting['project_invite'] ?: 'open';
|
||||
$setting['chat_information'] = $setting['chat_information'] ?: 'optional';
|
||||
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
|
||||
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
|
||||
$setting['translation'] = $setting['translation'] ?: 'close';
|
||||
$setting['convert_video'] = $setting['convert_video'] ?: 'close';
|
||||
$setting['compress_video'] = $setting['compress_video'] ?: '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';
|
||||
@@ -142,19 +142,17 @@ class SystemController extends AbstractController
|
||||
$setting['all_group_autoin'] = $setting['all_group_autoin'] ?: 'yes';
|
||||
$setting['user_private_chat_mute'] = $setting['user_private_chat_mute'] ?: 'open';
|
||||
$setting['user_group_chat_mute'] = $setting['user_group_chat_mute'] ?: 'open';
|
||||
$setting['start_home'] = $setting['start_home'] ?: 'close';
|
||||
$setting['file_upload_limit'] = $setting['file_upload_limit'] ?: '';
|
||||
$setting['unclaimed_task_reminder'] = $setting['unclaimed_task_reminder'] ?: 'close';
|
||||
$setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: '';
|
||||
$setting['server_closeai'] = env("SERVER_CLOSEAI") ?: 'open';
|
||||
$setting['server_timezone'] = config('app.timezone');
|
||||
$setting['server_version'] = Base::getVersion();
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/email 02. 获取邮箱设置、保存邮箱设置(限管理员)
|
||||
* @api {get} api/system/setting/email 获取邮箱设置、保存邮箱设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -220,11 +218,11 @@ class SystemController extends AbstractController
|
||||
$setting = array_intersect_key($setting, array_flip(['reg_verify']));
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/meeting 03. 获取会议设置、保存会议设置(限管理员)
|
||||
* @api {get} api/system/setting/meeting 获取会议设置、保存会议设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -274,11 +272,21 @@ class SystemController extends AbstractController
|
||||
$setting['api_secret'] = substr($setting['api_secret'], 0, 4) . str_repeat('*', strlen($setting['api_secret']) - 8) . substr($setting['api_secret'], -4);
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/aibot 04. 获取会议设置、保存AI机器人设置(限管理员)
|
||||
* AI助手设置(限管理员)
|
||||
*
|
||||
* @deprecated 已废弃方法,仅保留路由占位,后续版本中移除
|
||||
*/
|
||||
public function setting__ai()
|
||||
{
|
||||
Base::checkClientVersion('1.4.35');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/aibot 获取AI设置、保存AI机器人设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -287,6 +295,8 @@ class SystemController extends AbstractController
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - save: 保存设置(参数:[...])
|
||||
* @apiParam {String} filter 过滤字段(可选)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
@@ -295,7 +305,10 @@ class SystemController extends AbstractController
|
||||
{
|
||||
User::auth('admin');
|
||||
//
|
||||
Apps::isInstalledThrow('ai');
|
||||
//
|
||||
$type = trim(Request::input('type'));
|
||||
$filter = trim(Request::input('filter'));
|
||||
$setting = Base::setting('aibotSetting');
|
||||
if ($type == 'save') {
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
@@ -310,20 +323,48 @@ class SystemController extends AbstractController
|
||||
}
|
||||
$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);
|
||||
}
|
||||
//
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
foreach ($setting as $key => $item) {
|
||||
if (str_contains($key, '_key')) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/checkin 05. 获取签到设置、保存签到设置(限管理员)
|
||||
* 获取AI模型
|
||||
*
|
||||
* @deprecated 已废弃方法,仅保留路由占位,后续版本中移除
|
||||
*/
|
||||
public function setting__aibot_models()
|
||||
{
|
||||
Base::checkClientVersion('1.4.35');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取AI默认模型
|
||||
*
|
||||
* @deprecated 已废弃方法,仅保留路由占位,后续版本中移除
|
||||
*/
|
||||
public function setting__aibot_defmodels()
|
||||
{
|
||||
Base::checkClientVersion('1.4.35');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/checkin 获取签到设置、保存签到设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -359,8 +400,13 @@ class SystemController extends AbstractController
|
||||
'face_remark',
|
||||
'face_retip',
|
||||
'locat_remark',
|
||||
'locat_map_type',
|
||||
'locat_bd_lbs_key',
|
||||
'locat_bd_lbs_point', // 格式:{"lng":116.404, "lat":39.915, "radius":500}
|
||||
'locat_amap_key',
|
||||
'locat_amap_point', // 格式:{"lng":116.404, "lat":39.915, "radius":500}
|
||||
'locat_tencent_key',
|
||||
'locat_tencent_point', // 格式:{"lng":116.404, "lat":39.915, "radius":500}
|
||||
'manual_remark',
|
||||
'modes',
|
||||
'key',
|
||||
@@ -376,16 +422,33 @@ class SystemController extends AbstractController
|
||||
if (!$botUser) {
|
||||
return Base::retError('创建签到机器人失败');
|
||||
}
|
||||
if (in_array('locat', $all['modes'])) {
|
||||
if (empty($all['locat_bd_lbs_key'])) {
|
||||
return Base::retError('请填写百度地图AK');
|
||||
if (is_array($all['modes'])) {
|
||||
if (in_array('locat', $all['modes'])) {
|
||||
$mapTypes = [
|
||||
'baidu' => ['key' => 'locat_bd_lbs_key', 'point' => 'locat_bd_lbs_point', 'msg' => '请填写百度地图AK'],
|
||||
'amap' => ['key' => 'locat_amap_key', 'point' => 'locat_amap_point', 'msg' => '请填写高德地图Key'],
|
||||
'tencent' => ['key' => 'locat_tencent_key', 'point' => 'locat_tencent_point', 'msg' => '请填写腾讯地图Key'],
|
||||
];
|
||||
$type = $all['locat_map_type'];
|
||||
if (!isset($mapTypes[$type])) {
|
||||
return Base::retError('请选择地图类型');
|
||||
}
|
||||
$conf = $mapTypes[$type];
|
||||
if (empty($all[$conf['key']])) {
|
||||
return Base::retError($conf['msg']);
|
||||
}
|
||||
if (!is_array($all[$conf['point']])) {
|
||||
return Base::retError('请选择允许签到位置');
|
||||
}
|
||||
$all[$conf['point']]['radius'] = intval($all[$conf['point']]['radius']);
|
||||
$point = $all[$conf['point']];
|
||||
if (empty($point['lng']) || empty($point['lat']) || empty($point['radius'])) {
|
||||
return Base::retError('请选择有效的签到位置');
|
||||
}
|
||||
}
|
||||
if (!is_array($all['locat_bd_lbs_point'])) {
|
||||
return Base::retError('请选择允许签到位置');
|
||||
}
|
||||
$all['locat_bd_lbs_point']['radius'] = intval($all['locat_bd_lbs_point']['radius']);
|
||||
if (empty($all['locat_bd_lbs_point']['lng']) || empty($all['locat_bd_lbs_point']['lat']) || empty($all['locat_bd_lbs_point']['radius'])) {
|
||||
return Base::retError('请选择有效的签到位置');
|
||||
// 人脸识别
|
||||
if (in_array('face', $all['modes'])) {
|
||||
Apps::isInstalledThrow('face');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,7 +474,10 @@ class SystemController extends AbstractController
|
||||
$setting['face_remark'] = $setting['face_remark'] ?: Doo::translate('考勤机');
|
||||
$setting['face_retip'] = $setting['face_retip'] ?: 'open';
|
||||
$setting['locat_remark'] = $setting['locat_remark'] ?: Doo::translate('定位签到');
|
||||
$setting['locat_map_type'] = $setting['locat_map_type'] ?: 'baidu';
|
||||
$setting['locat_bd_lbs_point'] = is_array($setting['locat_bd_lbs_point']) ? $setting['locat_bd_lbs_point'] : ['radius' => 500];
|
||||
$setting['locat_amap_point'] = is_array($setting['locat_amap_point']) ? $setting['locat_amap_point'] : ['radius' => 500];
|
||||
$setting['locat_tencent_point'] = is_array($setting['locat_tencent_point']) ? $setting['locat_tencent_point'] : ['radius' => 500];
|
||||
$setting['manual_remark'] = $setting['manual_remark'] ?: Doo::translate('手动签到');
|
||||
$setting['time'] = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00'];
|
||||
$setting['advance'] = intval($setting['advance']) ?: 120;
|
||||
@@ -425,11 +491,11 @@ class SystemController extends AbstractController
|
||||
$setting['cmd'] = base64_encode($setting['cmd']);
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/apppush 06. 获取APP推送设置、保存APP推送设置(限管理员)
|
||||
* @api {get} api/system/setting/apppush 获取APP推送设置、保存APP推送设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -470,11 +536,11 @@ class SystemController extends AbstractController
|
||||
//
|
||||
$setting['push'] = $setting['push'] ?: 'close';
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/thirdaccess 07. 第三方帐号(限管理员)
|
||||
* @api {get} api/system/setting/thirdaccess 第三方帐号(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -540,11 +606,11 @@ class SystemController extends AbstractController
|
||||
$setting['ldap_port'] = intval($setting['ldap_port']) ?: 389;
|
||||
$setting['ldap_sync_local'] = $setting['ldap_sync_local'] ?: 'close';
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/file 08. 文件设置(限管理员)
|
||||
* @api {get} api/system/setting/file 文件设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -580,11 +646,11 @@ class SystemController extends AbstractController
|
||||
$setting = Base::setting('fileSetting');
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/demo 09. 获取演示帐号
|
||||
* @api {get} api/system/demo 获取演示帐号
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -608,7 +674,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/priority 10. 任务优先级
|
||||
* @api {post} api/system/priority 任务优先级
|
||||
*
|
||||
* @apiDescription 获取任务优先级、保存任务优先级
|
||||
* @apiVersion 1.0.0
|
||||
@@ -653,11 +719,11 @@ class SystemController extends AbstractController
|
||||
$setting = Base::setting('priority');
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $setting);
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/column/template 11. 创建项目模板
|
||||
* @api {post} api/system/column/template 创建项目模板
|
||||
*
|
||||
* @apiDescription 获取创建项目模板、保存创建项目模板
|
||||
* @apiVersion 1.0.0
|
||||
@@ -700,11 +766,11 @@ class SystemController extends AbstractController
|
||||
$setting = Base::setting('columnTemplate');
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $setting);
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/license 12. License
|
||||
* @api {post} api/system/license License
|
||||
*
|
||||
* @apiDescription 获取License信息、保存License(限管理员)
|
||||
* @apiVersion 1.0.0
|
||||
@@ -735,6 +801,7 @@ class SystemController extends AbstractController
|
||||
'info' => Doo::license(),
|
||||
'macs' => Doo::macs(),
|
||||
'doo_sn' => Doo::dooSN(),
|
||||
'doo_version' => Doo::dooVersion(),
|
||||
'user_count' => User::whereBot(0)->whereNull('disable_at')->count(),
|
||||
'error' => []
|
||||
];
|
||||
@@ -743,7 +810,7 @@ class SystemController extends AbstractController
|
||||
if ($data['info']['sn'] != $data['doo_sn']) {
|
||||
$data['error'][] = '终端SN与License不匹配';
|
||||
}
|
||||
if ($data['info']['mac']) {
|
||||
if ($data['info']['mac'] && $data['macs']) {
|
||||
$approved = false;
|
||||
foreach ($data['info']['mac'] as $mac) {
|
||||
if (in_array($mac, $data['macs'])) {
|
||||
@@ -769,11 +836,11 @@ class SystemController extends AbstractController
|
||||
];
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $data);
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $data ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/info 13. 获取终端详细信息
|
||||
* @api {get} api/system/get/info 获取终端详细信息
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -792,8 +859,6 @@ class SystemController extends AbstractController
|
||||
}
|
||||
return Base::retSuccess('success', [
|
||||
'ip' => Base::getIp(),
|
||||
'ip-info' => Extranet::getIpInfo(Base::getIp()),
|
||||
'ip-gcj02' => Extranet::getIpGcj02(Base::getIp()),
|
||||
'ip-iscn' => Base::isCnIp(Base::getIp()),
|
||||
'header' => Request::header(),
|
||||
'token' => Doo::userToken(),
|
||||
@@ -802,7 +867,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/ip 14. 获取IP地址
|
||||
* @api {get} api/system/get/ip 获取IP地址
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -817,7 +882,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/cnip 15. 是否中国IP地址
|
||||
* @api {get} api/system/get/cnip 是否中国IP地址
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -834,41 +899,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/ipgcj02 16. 获取IP地址经纬度
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName get__ipgcj02
|
||||
*
|
||||
* @apiParam {String} ip IP值
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function get__ipgcj02() {
|
||||
return Extranet::getIpGcj02(Request::input("ip"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/ipinfo 17. 获取IP地址详细信息
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName get__ipinfo
|
||||
*
|
||||
* @apiParam {String} ip IP值
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function get__ipinfo() {
|
||||
return Extranet::getIpInfo(Request::input("ip"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/imgupload 18. 上传图片
|
||||
* @api {post} api/system/imgupload 上传图片
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -877,7 +908,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] 压缩方式(等比缩放)
|
||||
@@ -934,7 +965,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/imgview 19. 浏览图片空间
|
||||
* @api {get} api/system/get/imgview 浏览图片空间
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1031,16 +1062,16 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/fileupload 20. 上传文件
|
||||
* @api {post} api/system/fileupload 上传文件
|
||||
*
|
||||
* @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 返回信息(错误描述)
|
||||
@@ -1075,7 +1106,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/updatelog 21. 获取更新日志
|
||||
* @api {get} api/system/get/updatelog 获取更新日志
|
||||
*
|
||||
* @apiDescription 获取更新日志
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1108,7 +1139,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', [
|
||||
@@ -1118,7 +1149,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/email/check 22. 邮件发送测试(限管理员)
|
||||
* @api {get} api/system/email/check 邮件发送测试(限管理员)
|
||||
*
|
||||
* @apiDescription 测试配置邮箱是否能发送邮件
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1164,7 +1195,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/checkin/export 23. 导出签到数据(限管理员)
|
||||
* @api {get} api/system/checkin/export 导出签到数据(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1180,7 +1211,7 @@ class SystemController extends AbstractController
|
||||
*/
|
||||
public function checkin__export()
|
||||
{
|
||||
User::auth('admin');
|
||||
$user = User::auth('admin');
|
||||
//
|
||||
$setting = Base::setting('checkinSetting');
|
||||
if ($setting['open'] !== 'open') {
|
||||
@@ -1210,130 +1241,183 @@ class SystemController extends AbstractController
|
||||
$secondStart = strtotime("2000-01-01 {$time[0]}") - strtotime("2000-01-01 00:00:00");
|
||||
$secondEnd = strtotime("2000-01-01 {$time[1]}") - strtotime("2000-01-01 00:00:00");
|
||||
//
|
||||
$headings = [];
|
||||
$headings[] = Doo::translate('签到人');
|
||||
$headings[] = Doo::translate('签到日期');
|
||||
$headings[] = Doo::translate('班次时间');
|
||||
$headings[] = Doo::translate('首次签到时间');
|
||||
$headings[] = Doo::translate('首次签到结果');
|
||||
$headings[] = Doo::translate('最后签到时间');
|
||||
$headings[] = Doo::translate('最后签到结果');
|
||||
$headings[] = Doo::translate('参数数据');
|
||||
$botUser = User::botGetOrCreate('system-msg');
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('系统机器人不存在');
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||
//
|
||||
$sheets = [];
|
||||
$startD = Carbon::parse($date[0])->startOfDay();
|
||||
$endD = Carbon::parse($date[1])->endOfDay();
|
||||
$users = User::whereIn('userid', $userid)->take(100)->get();
|
||||
/** @var User $user */
|
||||
foreach ($users as $user) {
|
||||
$recordTimes = UserCheckinRecord::getTimes($user->userid, [$startD, $endD]);
|
||||
$doo = Doo::load();
|
||||
go(function () use ($doo, $secondStart, $secondEnd, $time, $userid, $date, $user, $botUser, $dialog) {
|
||||
Coroutine::sleep(1);
|
||||
//
|
||||
$nickname = Base::filterEmoji($user->nickname);
|
||||
$styles = ["A1:H1" => ["font" => ["bold" => true]]];
|
||||
$datas = [];
|
||||
$startT = $startD->timestamp;
|
||||
$endT = $endD->timestamp;
|
||||
$index = 1;
|
||||
while ($startT < $endT) {
|
||||
$index++;
|
||||
$sameDate = date("Y-m-d", $startT);
|
||||
$sameTimes = $recordTimes[$sameDate] ?? [];
|
||||
$sameCollect = UserCheckinRecord::atCollect($sameDate, $sameTimes);
|
||||
$firstBetween = [Carbon::createFromTimestamp($startT), Carbon::createFromTimestamp($startT + $secondEnd - 1)];
|
||||
$lastBetween = [Carbon::createFromTimestamp($startT + $secondStart + 1), Carbon::createFromTimestamp($startT + 86400)];
|
||||
$firstRecord = $sameCollect?->whereBetween("datetime", $firstBetween)->first();
|
||||
$lastRecord = $sameCollect?->whereBetween("datetime", $lastBetween)->last();
|
||||
$firstTimestamp = $firstRecord['timestamp'] ?: 0;
|
||||
$lastTimestamp = $lastRecord['timestamp'] ?: 0;
|
||||
if (Timer::time() < $startT + $secondStart) {
|
||||
$firstResult = "-";
|
||||
} else {
|
||||
$firstResult = Doo::translate("正常");
|
||||
if (empty($firstTimestamp)) {
|
||||
$firstResult = Doo::translate("缺卡");
|
||||
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
|
||||
} elseif ($firstTimestamp > $startT + $secondStart) {
|
||||
$firstResult = Doo::translate("迟到");
|
||||
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
|
||||
$headings = [];
|
||||
$headings[] = $doo->translate('签到人');
|
||||
$headings[] = $doo->translate('签到日期');
|
||||
$headings[] = $doo->translate('班次时间');
|
||||
$headings[] = $doo->translate('首次签到时间');
|
||||
$headings[] = $doo->translate('首次签到结果');
|
||||
$headings[] = $doo->translate('最后签到时间');
|
||||
$headings[] = $doo->translate('最后签到结果');
|
||||
$headings[] = $doo->translate('参数数据');
|
||||
//
|
||||
$content = [];
|
||||
$content[] = [
|
||||
'content' => '导出签到数据已完成',
|
||||
'style' => 'font-weight: bold;padding-bottom: 4px;',
|
||||
];
|
||||
//
|
||||
$sheets = [];
|
||||
$startD = Carbon::parse($date[0])->startOfDay();
|
||||
$endD = Carbon::parse($date[1])->endOfDay();
|
||||
$users = User::whereIn('userid', $userid)->take(100)->get();
|
||||
/** @var User $user */
|
||||
foreach ($users as $user) {
|
||||
$recordTimes = UserCheckinRecord::getTimes($user->userid, [$startD, $endD]);
|
||||
//
|
||||
$nickname = Base::filterEmoji($user->nickname);
|
||||
$styles = ["A1:H1" => ["font" => ["bold" => true]]];
|
||||
$datas = [];
|
||||
$startT = $startD->timestamp;
|
||||
$endT = $endD->timestamp;
|
||||
$index = 1;
|
||||
while ($startT < $endT) {
|
||||
$index++;
|
||||
$sameDate = date("Y-m-d", $startT);
|
||||
$sameTimes = $recordTimes[$sameDate] ?? [];
|
||||
$sameCollect = UserCheckinRecord::atCollect($sameDate, $sameTimes);
|
||||
$firstBetween = [Carbon::createFromTimestamp($startT), Carbon::createFromTimestamp($startT + $secondEnd - 1)];
|
||||
$lastBetween = [Carbon::createFromTimestamp($startT + $secondStart + 1), Carbon::createFromTimestamp($startT + 86400)];
|
||||
$firstRecord = $sameCollect?->whereBetween("datetime", $firstBetween)->first();
|
||||
$lastRecord = $sameCollect?->whereBetween("datetime", $lastBetween)->last();
|
||||
$firstTimestamp = $firstRecord['timestamp'] ?: 0;
|
||||
$lastTimestamp = $lastRecord['timestamp'] ?: 0;
|
||||
if (Timer::time() < $startT + $secondStart) {
|
||||
$firstResult = "-";
|
||||
} else {
|
||||
$firstResult = $doo->translate("正常");
|
||||
if (empty($firstTimestamp)) {
|
||||
$firstResult = $doo->translate("缺卡");
|
||||
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
|
||||
} elseif ($firstTimestamp > $startT + $secondStart) {
|
||||
$firstResult = $doo->translate("迟到");
|
||||
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Timer::time() < $startT + $secondEnd) {
|
||||
$lastResult = "-";
|
||||
$lastTimestamp = 0;
|
||||
} else {
|
||||
$lastResult = Doo::translate("正常");
|
||||
if (empty($lastTimestamp) || $lastTimestamp === $firstTimestamp) {
|
||||
$lastResult = Doo::translate("缺卡");
|
||||
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
|
||||
} elseif ($lastTimestamp < $startT + $secondEnd) {
|
||||
$lastResult = Doo::translate("早退");
|
||||
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
|
||||
if (Timer::time() < $startT + $secondEnd) {
|
||||
$lastResult = "-";
|
||||
$lastTimestamp = 0;
|
||||
} else {
|
||||
$lastResult = $doo->translate("正常");
|
||||
if (empty($lastTimestamp) || $lastTimestamp === $firstTimestamp) {
|
||||
$lastResult = $doo->translate("缺卡");
|
||||
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
|
||||
} elseif ($lastTimestamp < $startT + $secondEnd) {
|
||||
$lastResult = $doo->translate("早退");
|
||||
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
|
||||
}
|
||||
}
|
||||
$firstTimestamp = $firstTimestamp ? date("H:i", $firstTimestamp) : "-";
|
||||
$lastTimestamp = $lastTimestamp ? date("H:i", $lastTimestamp) : "-";
|
||||
$section = array_map(function($item) {
|
||||
return $item[0] . "-" . ($item[1] ?: "None");
|
||||
}, UserCheckinRecord::atSection($sameTimes));
|
||||
$datas[] = [
|
||||
"{$nickname} (ID: {$user->userid})",
|
||||
$sameDate,
|
||||
implode("-", $time),
|
||||
$firstTimestamp,
|
||||
$firstResult,
|
||||
$lastTimestamp,
|
||||
$lastResult,
|
||||
implode(", ", $section),
|
||||
];
|
||||
$startT += 86400;
|
||||
}
|
||||
$firstTimestamp = $firstTimestamp ? date("H:i", $firstTimestamp) : "-";
|
||||
$lastTimestamp = $lastTimestamp ? date("H:i", $lastTimestamp) : "-";
|
||||
$section = array_map(function($item) {
|
||||
return $item[0] . "-" . ($item[1] ?: "None");
|
||||
}, UserCheckinRecord::atSection($sameTimes));
|
||||
$datas[] = [
|
||||
"{$nickname} (ID: {$user->userid})",
|
||||
$sameDate,
|
||||
implode("-", $time),
|
||||
$firstTimestamp,
|
||||
$firstResult,
|
||||
$lastTimestamp,
|
||||
$lastResult,
|
||||
implode(", ", $section),
|
||||
];
|
||||
$startT += 86400;
|
||||
$title = (count($sheets) + 1) . "." . ($nickname ?: $user->userid);
|
||||
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles($styles);
|
||||
}
|
||||
$title = (count($sheets) + 1) . "." . ($nickname ?: $user->userid);
|
||||
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles($styles);
|
||||
}
|
||||
if (empty($sheets)) {
|
||||
return Base::retError('没有任何数据');
|
||||
}
|
||||
if (empty($sheets)) {
|
||||
$content[] = [
|
||||
'content' => '没有任何数据',
|
||||
'style' => 'color: #ff0000;',
|
||||
];
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'title' => $content[0]['content'],
|
||||
'content' => $content,
|
||||
], $botUser->userid, true, false, true);
|
||||
return;
|
||||
}
|
||||
//
|
||||
$fileName = $users[0]->nickname;
|
||||
if (count($users) > 1) {
|
||||
$fileName .= "等" . count($userid) . "位成员的签到记录";
|
||||
} else {
|
||||
$fileName .= '的签到记录';
|
||||
}
|
||||
$fileName = $doo->translate($fileName) . '_' . Timer::time() . '.xlsx';
|
||||
$filePath = "temp/checkin/export/" . date("Ym", Timer::time());
|
||||
$export = new BillMultipleExport($sheets);
|
||||
$res = $export->store($filePath . "/" . $fileName);
|
||||
if ($res != 1) {
|
||||
$content[] = [
|
||||
'content' => "导出失败,{$fileName}!",
|
||||
'style' => 'color: #ff0000;',
|
||||
];
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'title' => $content[0]['content'],
|
||||
'content' => $content,
|
||||
], $botUser->userid, true, false, true);
|
||||
return;
|
||||
}
|
||||
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
|
||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
|
||||
$zipPath = storage_path($zipFile);
|
||||
if (file_exists($zipPath)) {
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
if (file_exists($zipPath)) {
|
||||
$key = Down::cache_encode([
|
||||
'file' => $zipFile,
|
||||
]);
|
||||
$fileUrl = Base::fillUrl('api/system/checkin/down?key=' . $key);
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'file_download',
|
||||
'title' => '导出签到数据已完成',
|
||||
'name' => $fileName,
|
||||
'size' => filesize($zipPath),
|
||||
'url' => $fileUrl,
|
||||
], $botUser->userid, true, false, true);
|
||||
} else {
|
||||
$content[] = [
|
||||
'content' => "打包失败,请稍后再试...",
|
||||
'style' => 'color: #ff0000;',
|
||||
];
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'title' => $content[0]['content'],
|
||||
'content' => $content,
|
||||
], $botUser->userid, true, false, true);
|
||||
}
|
||||
});
|
||||
//
|
||||
$fileName = $users[0]->nickname;
|
||||
if (count($users) > 1) {
|
||||
$fileName .= "等" . count($userid) . "位成员的签到记录";
|
||||
} else {
|
||||
$fileName .= '的签到记录';
|
||||
}
|
||||
$fileName = Doo::translate($fileName) . '_' . Timer::time() . '.xlsx';
|
||||
$filePath = "temp/checkin/export/" . date("Ym", Timer::time());
|
||||
$export = new BillMultipleExport($sheets);
|
||||
$res = $export->store($filePath . "/" . $fileName);
|
||||
if ($res != 1) {
|
||||
return Base::retError('导出失败,' . $fileName . '!');
|
||||
}
|
||||
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
|
||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
|
||||
$zipPath = storage_path($zipFile);
|
||||
if (file_exists($zipPath)) {
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'content' => '正在导出签到数据,请稍等...',
|
||||
], $botUser->userid, true, false, true);
|
||||
//
|
||||
if (file_exists($zipPath)) {
|
||||
$base64 = base64_encode(Base::array2string([
|
||||
'file' => $zipFile,
|
||||
]));
|
||||
Session::put('checkin::export:userid', $user->userid);
|
||||
return Base::retSuccess('success', [
|
||||
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
|
||||
'url' => Base::fillUrl('api/system/checkin/down?key=' . urlencode($base64)),
|
||||
]);
|
||||
} else {
|
||||
return Base::retError('打包失败,请稍后再试...');
|
||||
}
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/checkin/down 24. 下载导出的签到数据
|
||||
* @api {get} api/system/checkin/down 下载导出的签到数据
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1345,21 +1429,16 @@ class SystemController extends AbstractController
|
||||
*/
|
||||
public function checkin__down()
|
||||
{
|
||||
$userid = Session::get('checkin::export:userid');
|
||||
if (empty($userid)) {
|
||||
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502);
|
||||
}
|
||||
//
|
||||
$array = Base::string2array(base64_decode(urldecode(Request::input('key'))));
|
||||
$array = Down::cache_decode();
|
||||
$file = $array['file'];
|
||||
if (empty($file) || !file_exists(storage_path($file))) {
|
||||
return Base::ajaxError("文件不存在!", [], 0, 502);
|
||||
return Base::ajaxError("文件不存在!", [], 0, 403);
|
||||
}
|
||||
return Response::download(storage_path($file));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/version 25. 获取版本号
|
||||
* @api {get} api/system/version 获取版本号
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1367,23 +1446,29 @@ class SystemController extends AbstractController
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"device_count": 3, // 设备数量
|
||||
"version": "0.0.1", // 服务端版本号
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
"url": ""
|
||||
}
|
||||
}
|
||||
// 如果header请求中存在version字段,则返回数据包裹在 {ret:1,data:{},msg:"success"} 中
|
||||
*/
|
||||
public function version()
|
||||
{
|
||||
$url = url('');
|
||||
$package = Base::getPackage();
|
||||
$array = [
|
||||
'device_count' => 0,
|
||||
'version' => Base::getVersion(),
|
||||
'publish' => [],
|
||||
];
|
||||
if (Doo::userId()) {
|
||||
$array['device_count'] = UserDevice::whereUserid(Doo::userId())->count();
|
||||
}
|
||||
if (is_array($package['app'])) {
|
||||
$i = 0;
|
||||
$url = url('');
|
||||
foreach ($package['app'] as $item) {
|
||||
$urls = $item['urls'] && is_array($item['urls']) ? $item['urls'] : $item['url'];
|
||||
if (is_array($item['publish']) && ($i === 0 || Base::hostContrast($url, $urls))) {
|
||||
@@ -1392,11 +1477,14 @@ class SystemController extends AbstractController
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
if (Request::hasHeader('version')) {
|
||||
return Base::retSuccess('success', $array);
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/prefetch 26. 预加载的资源
|
||||
* @api {get} api/system/prefetch 预加载的资源
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1436,11 +1524,13 @@ class SystemController extends AbstractController
|
||||
}
|
||||
// 添加office资源
|
||||
$officePath = '';
|
||||
$officeApi = 'http://' . env('APP_IPPR') . '.6/web-apps/apps/api/documents/api.js';
|
||||
$content = @file_get_contents($officeApi);
|
||||
if ($content) {
|
||||
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {
|
||||
$officePath = $matches[1];
|
||||
if (Apps::isInstalled('office')) {
|
||||
$officeApi = 'http://office/web-apps/apps/api/documents/api.js';
|
||||
$content = @file_get_contents($officeApi);
|
||||
if ($content) {
|
||||
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {
|
||||
$officePath = $matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($officePath) {
|
||||
@@ -1455,6 +1545,18 @@ class SystemController extends AbstractController
|
||||
return !str_starts_with($item, 'office/{path}/');
|
||||
});
|
||||
}
|
||||
// 添加OKR资源
|
||||
if (Apps::isInstalled('okr')) {
|
||||
$okrContent = @file_get_contents("http://nginx/apps/okr/");
|
||||
preg_match_all('/<script[^>]*src=["\']([^"\']+)["\'][^>]*>/i', $okrContent, $scriptMatches);
|
||||
foreach ($scriptMatches[1] as $src) {
|
||||
$array[] = $src;
|
||||
}
|
||||
preg_match_all('/<link[^>]*rel=["\']stylesheet["\'][^>]*href=["\']([^"\']+)["\'][^>]*>/i', $okrContent, $linkMatches);
|
||||
foreach ($linkMatches[1] as $href) {
|
||||
$array[] = $href;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_map(function($item) use ($version) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
378
app/Http/Controllers/Api/apidoc.md
Normal file
378
app/Http/Controllers/Api/apidoc.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# apiDoc 参数标签说明(完整速查)
|
||||
|
||||
apiDoc 使用内联注释为 RESTful API 自动生成文档。
|
||||
以下为所有官方支持的参数与其说明。
|
||||
|
||||
---
|
||||
|
||||
## @api
|
||||
**定义 API 方法的基本信息**
|
||||
|
||||
```js
|
||||
@api {method} path title
|
||||
```
|
||||
|
||||
- **method**:请求方法,如 `GET`、`POST`、`PUT`、`DELETE` 等
|
||||
- **path**:请求路径,例如 `/user/:id`
|
||||
- **title**:简短标题(显示在文档中)
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@api {get} /user/:id Get user info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiBody
|
||||
**定义请求体参数**
|
||||
|
||||
```js
|
||||
@apiBody [{type}] [field=defaultValue] [description]
|
||||
```
|
||||
|
||||
- `{type}` 参数类型(如 String, Number, Object, String[])
|
||||
- `[field]` 可选字段(方括号表示可选)
|
||||
- `=defaultValue` 默认值
|
||||
- `description` 参数说明
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiBody {String} lastname Mandatory Lastname.
|
||||
@apiBody {Object} [address] Optional address object.
|
||||
@apiBody {String} [address[city]] Optional city.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiDefine
|
||||
**定义可复用的文档块**
|
||||
|
||||
```js
|
||||
@apiDefine name [title] [description]
|
||||
```
|
||||
|
||||
- `name`:唯一标识
|
||||
- `title`:简短标题
|
||||
- `description`:多行描述
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiDefine MyError
|
||||
@apiError UserNotFound The <code>id</code> of the User was not found.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiDeprecated
|
||||
**标记接口为弃用状态**
|
||||
|
||||
```js
|
||||
@apiDeprecated [text]
|
||||
```
|
||||
|
||||
- `text`:提示文本,可带链接到新方法
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiDeprecated use now (#User:GetDetails)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiDescription
|
||||
**描述接口详细说明**
|
||||
|
||||
```js
|
||||
@apiDescription text
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiDescription This is the Description.
|
||||
It is multiline capable.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiError
|
||||
**定义错误返回参数**
|
||||
|
||||
```js
|
||||
@apiError [(group)] [{type}] field [description]
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiError UserNotFound The id of the User was not found.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiErrorExample
|
||||
**定义错误返回示例**
|
||||
|
||||
```js
|
||||
@apiErrorExample [{type}] [title]
|
||||
example
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiErrorExample {json} Error-Response:
|
||||
HTTP/1.1 404 Not Found
|
||||
{ "error": "UserNotFound" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiExample
|
||||
**定义接口使用示例**
|
||||
|
||||
```js
|
||||
@apiExample [{type}] title
|
||||
example
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiExample {curl} Example usage:
|
||||
curl -i http://localhost/user/4711
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiGroup
|
||||
**定义所属分组**
|
||||
|
||||
```js
|
||||
@apiGroup name
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiGroup User
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiHeader
|
||||
**定义请求头参数**
|
||||
|
||||
```js
|
||||
@apiHeader [(group)] [{type}] [field=defaultValue] [description]
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiHeader {String} access-key Users unique access-key.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiHeaderExample
|
||||
**定义请求头示例**
|
||||
|
||||
```js
|
||||
@apiHeaderExample [{type}] [title]
|
||||
example
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiHeaderExample {json} Header-Example:
|
||||
{
|
||||
"Accept-Encoding": "gzip, deflate"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiIgnore
|
||||
**忽略当前文档块**
|
||||
|
||||
```js
|
||||
@apiIgnore [hint]
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiIgnore Not finished method
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiName
|
||||
**定义接口唯一名称**
|
||||
|
||||
```js
|
||||
@apiName name
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiName GetUser
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiParam
|
||||
**定义请求参数**
|
||||
|
||||
```js
|
||||
@apiParam [(group)] [{type}] [field=defaultValue] [description]
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiParam {Number} id Users unique ID.
|
||||
@apiParam {String} [firstname] Optional firstname.
|
||||
@apiParam {String} country="DE" Mandatory with default.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiParamExample
|
||||
**定义参数请求示例**
|
||||
|
||||
```js
|
||||
@apiParamExample [{type}] [title]
|
||||
example
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiParamExample {json} Request-Example:
|
||||
{ "id": 4711 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiPermission
|
||||
**定义权限要求**
|
||||
|
||||
```js
|
||||
@apiPermission name
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiPermission admin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiPrivate
|
||||
**标记接口为私有(可过滤)**
|
||||
|
||||
```js
|
||||
@apiPrivate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiQuery
|
||||
**定义查询参数(?query)**
|
||||
|
||||
```js
|
||||
@apiQuery [{type}] [field=defaultValue] [description]
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiQuery {Number} id Users unique ID.
|
||||
@apiQuery {String} [sort="asc"] Sort order.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiSampleRequest
|
||||
**定义接口测试请求 URL**
|
||||
|
||||
```js
|
||||
@apiSampleRequest url
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiSampleRequest http://test.github.com/some_path/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiSuccess
|
||||
**定义成功返回参数**
|
||||
|
||||
```js
|
||||
@apiSuccess [(group)] [{type}] field [description]
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiSuccess {String} firstname Firstname of the User.
|
||||
@apiSuccess {String} lastname Lastname of the User.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiSuccessExample
|
||||
**定义成功返回示例**
|
||||
|
||||
```js
|
||||
@apiSuccessExample [{type}] [title]
|
||||
example
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiSuccessExample {json} Success-Response:
|
||||
HTTP/1.1 200 OK
|
||||
{ "firstname": "John", "lastname": "Doe" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiUse
|
||||
**引用定义块(@apiDefine)**
|
||||
|
||||
```js
|
||||
@apiUse name
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiDefine MySuccess
|
||||
@apiSuccess {String} firstname User firstname.
|
||||
|
||||
@apiUse MySuccess
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @apiVersion
|
||||
**定义接口版本**
|
||||
|
||||
```js
|
||||
@apiVersion version
|
||||
```
|
||||
|
||||
📘 示例:
|
||||
```js
|
||||
@apiVersion 1.6.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 附录:常用标签速查表
|
||||
|
||||
| 标签 | 作用 | 示例 |
|
||||
|------|------|------|
|
||||
| `@api` | 定义接口 | `@api {get} /user/:id` |
|
||||
| `@apiName` | 唯一名称 | `@apiName GetUser` |
|
||||
| `@apiGroup` | 所属分组 | `@apiGroup User` |
|
||||
| `@apiParam` | 请求参数 | `@apiParam {Number} id Users unique ID.` |
|
||||
| `@apiBody` | 请求体参数 | `@apiBody {String} name Username.` |
|
||||
| `@apiQuery` | 查询参数 | `@apiQuery {String} keyword Search term.` |
|
||||
| `@apiHeader` | Header 参数 | `@apiHeader {String} token Auth token.` |
|
||||
| `@apiSuccess` | 成功返回字段 | `@apiSuccess {String} name Username.` |
|
||||
| `@apiError` | 错误返回字段 | `@apiError NotFound User not found.` |
|
||||
| `@apiVersion` | 版本号 | `@apiVersion 1.0.0` |
|
||||
@@ -1,89 +1,137 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 给apidoc项目增加顺序编号
|
||||
* 给apidoc项目增加顺序编号 / 支持恢复
|
||||
*/
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
$path = dirname(__FILE__). '/';
|
||||
$lists = scandir($path);
|
||||
//
|
||||
foreach ($lists AS $item) {
|
||||
$fillPath = $path . $item;
|
||||
if (str_ends_with($fillPath, 'Controller.php')) {
|
||||
$content = file_get_contents($fillPath);
|
||||
preg_match_all("/\* @api \{(.+?)\} (.*?)\n/i", $content, $matchs);
|
||||
$i = 1;
|
||||
foreach ($matchs[2] AS $key=>$text) {
|
||||
if (in_array(strtolower($matchs[1][$key]), array('get', 'post'))) {
|
||||
$expl = explode(" ", __sRemove($text));
|
||||
$end = $expl[1];
|
||||
if ($expl[2]) {
|
||||
$end = '';
|
||||
foreach ($expl AS $k=>$v) { if ($k >= 2) { $end.= " ".$v; } }
|
||||
}
|
||||
$newtext = "* @api {".$matchs[1][$key]."} ".$expl[0]." ".__zeroFill($i, 2).". ".trim($end);
|
||||
$content = str_replace("* @api {".$matchs[1][$key]."} ".$text, $newtext, $content);
|
||||
$i++;
|
||||
//
|
||||
echo $newtext;
|
||||
echo "\r\n";
|
||||
}
|
||||
}
|
||||
if ($i > 1) {
|
||||
file_put_contents($fillPath, $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
echo "Success \n";
|
||||
const NUMBER_WIDTH = 2;
|
||||
|
||||
/** ************************************************************** */
|
||||
/** ************************************************************** */
|
||||
/** ************************************************************** */
|
||||
$isRestore = isset($argv[1]) && strtolower($argv[1]) === 'restore';
|
||||
|
||||
/**
|
||||
* 替换所有空格
|
||||
* @param $str
|
||||
* @return mixed
|
||||
*/
|
||||
function __sRemove($str) {
|
||||
$str = str_replace(" ", " ", $str);
|
||||
if (__strExists($str, " ")) {
|
||||
return __sRemove($str);
|
||||
}
|
||||
return $str;
|
||||
$basePath = dirname(__FILE__) . '/';
|
||||
$controllerFiles = glob($basePath . '*Controller.php');
|
||||
|
||||
if (!$controllerFiles) {
|
||||
echo "No Controller.php files found\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
foreach ($controllerFiles as $filePath) {
|
||||
$original = file_get_contents($filePath);
|
||||
[$updated, $linesChanged] = processFile($original, $isRestore);
|
||||
|
||||
if (count($linesChanged) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
file_put_contents($filePath, $updated);
|
||||
|
||||
foreach ($linesChanged as $line) {
|
||||
echo $line . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo $isRestore ? "Restore Success \n" : "Success \n";
|
||||
|
||||
/**
|
||||
* 是否包含字符
|
||||
* @param $string
|
||||
* @param $find
|
||||
* @return bool
|
||||
* 处理单个文件内容
|
||||
*
|
||||
* @param string $content
|
||||
* @param bool $restore
|
||||
* @return array{string, array<int, string>}
|
||||
*/
|
||||
function __strExists($string, $find)
|
||||
function processFile(string $content, bool $restore): array
|
||||
{
|
||||
return str_contains($string, $find);
|
||||
$lineChanges = [];
|
||||
$counter = 1;
|
||||
|
||||
$pattern = '/\* @api \{([^\}]+)\}\s+([^\s]+)([^\r\n]*)(\r?\n)/';
|
||||
|
||||
$updated = preg_replace_callback(
|
||||
$pattern,
|
||||
function (array $matches) use ($restore, &$counter, &$lineChanges) {
|
||||
$method = trim($matches[1]);
|
||||
if (!in_array(strtolower($method), ['get', 'post'], true)) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
$endpoint = trim($matches[2]);
|
||||
$suffix = normalizeDescription(stripExistingNumbering($matches[3]));
|
||||
|
||||
if (!$restore) {
|
||||
$numberedSuffix = formatNumber($counter) . '.';
|
||||
if ($suffix !== '') {
|
||||
$numberedSuffix .= ' ' . $suffix;
|
||||
}
|
||||
$counter++;
|
||||
} else {
|
||||
$numberedSuffix = $suffix;
|
||||
}
|
||||
|
||||
$newLine = renderAnnotation($method, $endpoint, $numberedSuffix);
|
||||
|
||||
if ($newLine !== rtrim($matches[0], "\r\n")) {
|
||||
$lineChanges[] = $newLine;
|
||||
}
|
||||
|
||||
return $newLine . $matches[4];
|
||||
},
|
||||
$content
|
||||
);
|
||||
|
||||
if ($updated === null) {
|
||||
return [$content, []];
|
||||
}
|
||||
|
||||
return [$updated, $lineChanges];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $str 补零
|
||||
* @param int $length
|
||||
* @param int $after
|
||||
* @return bool|string
|
||||
* 生成格式化后的注释行
|
||||
*/
|
||||
function __zeroFill($str, $length = 0, $after = 1) {
|
||||
if (strlen($str) >= $length) {
|
||||
return $str;
|
||||
function renderAnnotation(string $method, string $endpoint, string $suffix = ''): string
|
||||
{
|
||||
$line = "* @api {" . $method . "} " . $endpoint;
|
||||
|
||||
if ($suffix !== '') {
|
||||
if ($suffix[0] !== ' ') {
|
||||
$line .= ' ';
|
||||
}
|
||||
$line .= $suffix;
|
||||
}
|
||||
$_str = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$_str .= '0';
|
||||
}
|
||||
if ($after) {
|
||||
$_ret = substr($_str . $str, $length * -1);
|
||||
} else {
|
||||
$_ret = substr($str . $_str, 0, $length);
|
||||
}
|
||||
return $_ret;
|
||||
|
||||
return $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除已有编号部分
|
||||
*/
|
||||
function stripExistingNumbering(string $text): string
|
||||
{
|
||||
$trimmed = ltrim($text);
|
||||
$pattern = '/^\d+\.\s*/';
|
||||
return preg_replace($pattern, '', $trimmed) ?? $trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩多余空格
|
||||
*/
|
||||
function normalizeDescription(string $text): string
|
||||
{
|
||||
$text = trim($text);
|
||||
if ($text === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return preg_replace('/\s+/', ' ', $text) ?? $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成固定宽度的数字
|
||||
*/
|
||||
function formatNumber(int $number): string
|
||||
{
|
||||
return str_pad((string) $number, NUMBER_WIDTH, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use Request;
|
||||
use Redirect;
|
||||
use Response;
|
||||
use App\Models\File;
|
||||
use App\Models\User;
|
||||
use App\Models\UserTransfer;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Base;
|
||||
use App\Module\Extranet;
|
||||
@@ -23,10 +21,10 @@ use App\Tasks\AutoArchivedTask;
|
||||
use App\Tasks\DeleteBotMsgTask;
|
||||
use App\Tasks\CheckinRemindTask;
|
||||
use App\Tasks\CloseMeetingRoomTask;
|
||||
use App\Tasks\ZincSearchSyncTask;
|
||||
use App\Tasks\UnclaimedTaskRemindTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Laravolt\Avatar\Avatar;
|
||||
use Swoole\Coroutine;
|
||||
|
||||
|
||||
/**
|
||||
@@ -63,6 +61,10 @@ class IndexController extends InvokeController
|
||||
$array = Base::json2array(file_get_contents($hotFile));
|
||||
$style = null;
|
||||
$script = preg_replace("/^(\/\/(.*?))(:\d+)?\//i", "$1:" . $array['APP_DEV_PORT'] . "/", asset_main("resources/assets/js/app.js"));
|
||||
$proxyUri = Base::liveEnv('VSCODE_PROXY_URI');
|
||||
if (is_string($proxyUri) && preg_match('/^https?:\/\//i', $proxyUri)) {
|
||||
$script = preg_replace('/^(https?:\/\/|\/\/)[^\/]+/', rtrim($proxyUri, '/'), $script, 1);
|
||||
}
|
||||
} else {
|
||||
$array = Base::json2array(file_get_contents($manifestFile));
|
||||
$style = asset_main($array['resources/assets/js/app.js']['css'][0]);
|
||||
@@ -85,6 +87,15 @@ class IndexController extends InvokeController
|
||||
return Redirect::to(Base::fillUrl('api/system/version'), 301);
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
* @return string
|
||||
*/
|
||||
public function health()
|
||||
{
|
||||
return "ok";
|
||||
}
|
||||
|
||||
/**
|
||||
* 头像
|
||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
@@ -241,11 +252,13 @@ class IndexController extends InvokeController
|
||||
// App推送
|
||||
Task::deliver(new AppPushTask());
|
||||
// 删除过期的临时表数据
|
||||
Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1));
|
||||
Task::deliver(new DeleteTmpTask('task_worker', 12));
|
||||
Task::deliver(new DeleteTmpTask('tmp_msgs', 1));
|
||||
Task::deliver(new DeleteTmpTask('tmp'));
|
||||
Task::deliver(new DeleteTmpTask('task_worker', 12));
|
||||
Task::deliver(new DeleteTmpTask('file'));
|
||||
Task::deliver(new DeleteTmpTask('tmp_file', 24));
|
||||
Task::deliver(new DeleteTmpTask('user_device', 24));
|
||||
Task::deliver(new DeleteTmpTask('umeng_log', 24 * 3));
|
||||
// 删除机器人消息
|
||||
Task::deliver(new DeleteBotMsgTask());
|
||||
// 周期任务
|
||||
@@ -258,6 +271,8 @@ class IndexController extends InvokeController
|
||||
Task::deliver(new UnclaimedTaskRemindTask());
|
||||
// 关闭会议室
|
||||
Task::deliver(new CloseMeetingRoomTask());
|
||||
// ZincSearch 同步
|
||||
Task::deliver(new ZincSearchSyncTask());
|
||||
|
||||
return "success";
|
||||
}
|
||||
@@ -321,7 +336,7 @@ class IndexController extends InvokeController
|
||||
"file" => Request::file('file'),
|
||||
"type" => 'publish',
|
||||
"path" => $draftPath,
|
||||
"fileName" => true,
|
||||
"saveName" => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -342,9 +357,7 @@ class IndexController extends InvokeController
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (empty($avaiPath)) {
|
||||
abort(404);
|
||||
}
|
||||
abort_if(empty($avaiPath), 404);
|
||||
$lists = Base::recursiveFiles($dirPath, false);
|
||||
$files = [];
|
||||
foreach ($lists as $file) {
|
||||
@@ -422,13 +435,9 @@ class IndexController extends InvokeController
|
||||
$path = Arr::get($data, 'path');
|
||||
$file = public_path($path);
|
||||
// 防止 ../ 穿越获取到系统文件
|
||||
if (!str_starts_with(realpath($file), public_path())) {
|
||||
abort(404);
|
||||
}
|
||||
//
|
||||
if (!file_exists($file)) {
|
||||
abort(404);
|
||||
}
|
||||
abort_if(!str_starts_with(realpath($file), public_path()), 404);
|
||||
// 如果文件不存在,直接返回 404
|
||||
abort_if(!file_exists($file), 404);
|
||||
//
|
||||
parse_str($data['query'], $query);
|
||||
$name = Arr::get($query, 'name');
|
||||
@@ -468,7 +477,7 @@ class IndexController extends InvokeController
|
||||
action: "eeuiAppSendMessage",
|
||||
data: [
|
||||
{
|
||||
action: 'setPageData',
|
||||
action: 'setPageData', // 设置页面数据
|
||||
data: {
|
||||
showProgress: true,
|
||||
titleFixed: true,
|
||||
@@ -476,7 +485,7 @@ class IndexController extends InvokeController
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'createTarget',
|
||||
action: 'createTarget', // 创建目标(访问新地址)
|
||||
url: "{$redirectUrl}",
|
||||
}
|
||||
]
|
||||
@@ -490,7 +499,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
|
||||
@@ -498,43 +507,4 @@ class IndexController extends InvokeController
|
||||
$redirectUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
|
||||
return Redirect::to($redirectUrl, 301);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复操作离职后续操作(todo 临时,后期删除)
|
||||
* @return array
|
||||
*/
|
||||
public function migration__userdialog()
|
||||
{
|
||||
if (Request::header('app-key') !== env('APP_KEY')) {
|
||||
return Base::retError("key error");
|
||||
}
|
||||
go(function() {
|
||||
Coroutine::sleep(3);
|
||||
$handled = [];
|
||||
UserTransfer::orderBy('id')->chunkById(10, function ($transfers) use ($handled) {
|
||||
/** @var UserTransfer $transfer */
|
||||
foreach ($transfers as $transfer) {
|
||||
if (in_array($transfer->original_userid, $handled)) {
|
||||
continue;
|
||||
}
|
||||
$handled[] = $transfer->original_userid;
|
||||
//
|
||||
$user = User::find($transfer->original_userid);
|
||||
if ($user?->isDisable()) {
|
||||
$transfer->exitDialog();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置 (todo 已废弃)
|
||||
* @return string
|
||||
*/
|
||||
public function storage__synch()
|
||||
{
|
||||
return '<!-- Deprecated -->';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,13 @@ class WebApi
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
// 为每个请求生成唯一ID
|
||||
$request->requestId = RequestContext::generateRequestId();
|
||||
// 记录请求信息
|
||||
RequestContext::set('start_time', microtime(true));
|
||||
RequestContext::set('header_language', $request->header('language'));
|
||||
|
||||
// 更新请求的基本URL
|
||||
RequestContext::updateBaseUrl($request);
|
||||
|
||||
// 加载Doo类
|
||||
Doo::load();
|
||||
|
||||
@@ -73,6 +75,6 @@ class WebApi
|
||||
public function terminate()
|
||||
{
|
||||
// 请求结束后清理上下文
|
||||
RequestContext::clear();
|
||||
RequestContext::clean();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,25 @@ class AbstractModel extends Model
|
||||
return $date->format($this->dateFormat ?: 'Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过模型创建实例
|
||||
* @param array $param
|
||||
* @param bool $force
|
||||
* @return static
|
||||
*/
|
||||
public static function fillInstance(array $param = [], bool $force = true)
|
||||
{
|
||||
$instance = new static;
|
||||
if ($param) {
|
||||
if ($force) {
|
||||
$instance->forceFill($param);
|
||||
} else {
|
||||
$instance->fill($param);
|
||||
}
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建/更新数据
|
||||
* @param array $param
|
||||
@@ -209,24 +228,35 @@ class AbstractModel extends Model
|
||||
|
||||
/**
|
||||
* 数据库更新或插入
|
||||
* @param $where
|
||||
* @param array|\Closure $update 存在时更新的内容
|
||||
* @param array|\Closure $insert 不存在时插入的内容,如果没有则插入更新内容
|
||||
* @param bool $isInsert 是否是插入数据
|
||||
* @param array $where 查询条件
|
||||
* @param array|\Closure $update 存在时更新的内容
|
||||
* @param array|\Closure $insert 不存在时插入的内容,如果没有则插入更新内容
|
||||
* @param bool $isInsert 是否是插入数据
|
||||
* @param bool|null $lockForUpdate 是否加锁(true:加锁,false:不加锁,null:在事务中会自动加锁)
|
||||
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
|
||||
*/
|
||||
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true)
|
||||
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true, $lockForUpdate = null)
|
||||
{
|
||||
$row = static::where($where)->first();
|
||||
$query = static::where($where);
|
||||
if ($lockForUpdate === null) {
|
||||
$lockForUpdate = \DB::transactionLevel() > 0;
|
||||
}
|
||||
if ($lockForUpdate) {
|
||||
$query->lockForUpdate();
|
||||
}
|
||||
$row = $query->first();
|
||||
if (empty($row)) {
|
||||
$row = new static;
|
||||
if ($update instanceof \Closure) {
|
||||
$update = $update();
|
||||
}
|
||||
if ($insert instanceof \Closure) {
|
||||
$insert = $insert();
|
||||
}
|
||||
$array = array_merge($where, $insert ?: $update);
|
||||
if (empty($insert)) {
|
||||
if ($update instanceof \Closure) {
|
||||
$update = $update();
|
||||
}
|
||||
$insert = $update;
|
||||
}
|
||||
$array = array_merge($where, $insert);
|
||||
if (isset($array[$row->primaryKey])) {
|
||||
unset($array[$row->primaryKey]);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,60 @@ 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';
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Request;
|
||||
use App\Module\Apps;
|
||||
use App\Module\Base;
|
||||
use App\Tasks\PushTask;
|
||||
use App\Exceptions\ApiException;
|
||||
@@ -23,6 +24,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property int|null $size 大小(B)
|
||||
* @property int|null $userid 拥有者ID
|
||||
* @property int|null $share 是否共享
|
||||
* @property int|null $guest_access 是否允许游客访问
|
||||
* @property int|null $pshare 所属分享ID
|
||||
* @property int|null $created_id 创建者
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
@@ -43,6 +45,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereExt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereGuestAccess($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File wherePid($value)
|
||||
@@ -117,7 +120,7 @@ class File extends AbstractModel
|
||||
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw',
|
||||
'tif', 'tiff',
|
||||
'mp3', 'wav', 'mp4', 'flv',
|
||||
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
|
||||
// 'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm', // 这一排是要转换的,无法使用本地播放
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -641,6 +644,29 @@ class File extends AbstractModel
|
||||
Task::deliver($task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件推送消息
|
||||
* @param $action
|
||||
* @param array|null $data 发送内容
|
||||
* @param int $userid 会员ID
|
||||
*/
|
||||
public static function pushMsgSimple($action, $data, $userid)
|
||||
{
|
||||
if (empty($data) || empty($userid)) {
|
||||
return;
|
||||
}
|
||||
$msg = [
|
||||
'type' => 'file',
|
||||
'action' => $action,
|
||||
'data' => $data,
|
||||
];
|
||||
$params = [
|
||||
'userid' => $userid,
|
||||
'msg' => $msg
|
||||
];
|
||||
Task::deliver(new PushTask($params));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推送会员
|
||||
* @param $action
|
||||
@@ -956,26 +982,39 @@ class File extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件推送消息
|
||||
* @param $action
|
||||
* @param array|null $data 发送内容
|
||||
* @param array $userid 会员ID
|
||||
* 根据文件类型判断是否需要安装应用
|
||||
* @param $type
|
||||
* @return void
|
||||
*/
|
||||
public static function filePushMsg($action, $data = null, $userid = null)
|
||||
public static function isNeedInstallApp($type): void
|
||||
{
|
||||
$userid = User::userid();
|
||||
if (empty($userid)) {
|
||||
return;
|
||||
// 文件类型与应用的映射配置
|
||||
$fileTypeAppMapping = [
|
||||
// Office 应用映射
|
||||
[
|
||||
'types' => ['word', 'excel', 'ppt', 'docx', 'xlsx', 'pptx'],
|
||||
'app_id' => 'office',
|
||||
'app_name' => 'OnlyOffice'
|
||||
],
|
||||
// Drawio 应用映射
|
||||
[
|
||||
'types' => ['drawio'],
|
||||
'app_id' => 'drawio',
|
||||
'app_name' => 'Drawio'
|
||||
],
|
||||
// Minder 应用映射
|
||||
[
|
||||
'types' => ['mind'],
|
||||
'app_id' => 'minder',
|
||||
'app_name' => 'Minder'
|
||||
]
|
||||
];
|
||||
|
||||
// 遍历配置检查是否需要安装应用
|
||||
foreach ($fileTypeAppMapping as $config) {
|
||||
if (in_array($type, $config['types'])) {
|
||||
Apps::isInstalledThrow($config['app_id']);
|
||||
}
|
||||
}
|
||||
$msg = [
|
||||
'type' => 'file',
|
||||
'action' => $action,
|
||||
'data' => $data,
|
||||
];
|
||||
$params = [
|
||||
'userid' => $userid,
|
||||
'msg' => $msg
|
||||
];
|
||||
Task::deliver(new PushTask($params));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Timer;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -130,9 +129,7 @@ class FileContent extends AbstractModel
|
||||
],
|
||||
default => json_decode('{}'),
|
||||
};
|
||||
if ($download) {
|
||||
abort(403, "This file is empty.");
|
||||
}
|
||||
abort_if($download, 403, "This file is empty.");
|
||||
} else {
|
||||
$path = $content['url'];
|
||||
if ($file->ext) {
|
||||
@@ -148,13 +145,51 @@ class FileContent extends AbstractModel
|
||||
}
|
||||
if ($download) {
|
||||
$filePath = public_path($path);
|
||||
if (isset($filePath)) {
|
||||
return Base::DownloadFileResponse($filePath, $name);
|
||||
} else {
|
||||
abort(403, "This file not support download.");
|
||||
}
|
||||
abort_if(!isset($filePath),403, "This file not support download.");
|
||||
return Base::DownloadFileResponse($filePath, $name);
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('success', [ 'content' => $content ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件访问URL
|
||||
* @param int $fileId 文件ID
|
||||
* @return string|null 返回完整的文件URL,如果文件无内容则返回null
|
||||
*/
|
||||
public static function getFileUrl($fileId)
|
||||
{
|
||||
$content = self::whereFid($fileId)->orderByDesc('id')->first();
|
||||
if ($content) {
|
||||
$contentData = Base::json2array($content->content ?: []);
|
||||
if (!empty($contentData['url'])) {
|
||||
return Base::fillUrl($contentData['url']);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件内容
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ class Project extends AbstractModel
|
||||
'projects.*',
|
||||
'project_users.owner',
|
||||
'project_users.top_at',
|
||||
'project_users.sort',
|
||||
])
|
||||
->leftJoin('project_users', function ($leftJoin) use ($userid) {
|
||||
$leftJoin
|
||||
@@ -153,6 +154,7 @@ class Project extends AbstractModel
|
||||
'projects.*',
|
||||
'project_users.owner',
|
||||
'project_users.top_at',
|
||||
'project_users.sort',
|
||||
])
|
||||
->join('project_users', 'projects.id', '=', 'project_users.project_id')
|
||||
->where('project_users.userid', $userid);
|
||||
@@ -221,6 +223,7 @@ class Project extends AbstractModel
|
||||
'important' => 1
|
||||
], function () use ($userid) {
|
||||
return [
|
||||
'important' => 1,
|
||||
'bot' => User::isBot($userid) ? 1 : 0,
|
||||
];
|
||||
});
|
||||
@@ -419,29 +422,38 @@ class Project extends AbstractModel
|
||||
$hasStart = false;
|
||||
$hasEnd = false;
|
||||
$upTaskList = [];
|
||||
$projectUserids = $this->relationUserids();
|
||||
foreach ($flows as $item) {
|
||||
$id = intval($item['id']);
|
||||
$name = trim(str_replace('|', '·', $item['name']));
|
||||
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
|
||||
$userids = Base::arrayRetainInt($item['userids'] ?: [], true);
|
||||
$usertype = trim($item['usertype']);
|
||||
$userlimit = intval($item['userlimit']);
|
||||
$columnid = intval($item['columnid']);
|
||||
if ($usertype == 'replace' && empty($userids)) {
|
||||
throw new ApiException("状态[{$item['name']}]设置错误,设置流转模式时必须填写状态负责人");
|
||||
throw new ApiException("状态[{$name}]设置错误,设置流转模式时必须填写状态负责人");
|
||||
}
|
||||
if ($usertype == 'merge' && empty($userids)) {
|
||||
throw new ApiException("状态[{$item['name']}]设置错误,设置剔除模式时必须填写状态负责人");
|
||||
throw new ApiException("状态[{$name}]设置错误,设置剔除模式时必须填写状态负责人");
|
||||
}
|
||||
if ($userlimit && empty($userids)) {
|
||||
throw new ApiException("状态[{$item['name']}]设置错误,设置限制负责人时必须填写状态负责人");
|
||||
throw new ApiException("状态[{$name}]设置错误,设置限制负责人时必须填写状态负责人");
|
||||
}
|
||||
foreach ($userids as $userid) {
|
||||
if (!in_array($userid, $projectUserids)) {
|
||||
$nickname = User::userid2nickname($userid);
|
||||
throw new ApiException("状态[{$name}]设置错误,状态负责人[{$nickname}]不在项目成员内");
|
||||
}
|
||||
}
|
||||
$flow = ProjectFlowItem::updateInsert([
|
||||
'id' => $id,
|
||||
'project_id' => $this->id,
|
||||
'flow_id' => $projectFlow->id,
|
||||
], [
|
||||
'name' => trim($item['name']),
|
||||
'name' => $name,
|
||||
'status' => trim($item['status']),
|
||||
'color' => trim($item['color']),
|
||||
'sort' => intval($item['sort']),
|
||||
'turns' => $turns,
|
||||
'userids' => $userids,
|
||||
@@ -461,7 +473,7 @@ class Project extends AbstractModel
|
||||
$hasEnd = true;
|
||||
}
|
||||
if (!$isInsert) {
|
||||
$upTaskList[$flow->id] = $flow->status . "|" . $flow->name;
|
||||
$upTaskList[$flow->id] = $flow->status . "|" . $flow->name . "|" . $flow->color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -582,7 +594,7 @@ class Project extends AbstractModel
|
||||
$project->save();
|
||||
//
|
||||
if ($flow == 'open') {
|
||||
$project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-14,"name":"已取消","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0}]'));
|
||||
$project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-14,"name":"已取消","status":"end","color":"#999999","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0}]'));
|
||||
}
|
||||
});
|
||||
//
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Module\Base;
|
||||
* @property int|null $flow_id 流程ID
|
||||
* @property string|null $name 名称
|
||||
* @property string|null $status 状态
|
||||
* @property string|null $color 自定义颜色
|
||||
* @property array $turns 可流转
|
||||
* @property array $userids 状态负责人ID
|
||||
* @property string|null $usertype 流转模式
|
||||
@@ -30,6 +31,7 @@ use App\Module\Base;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereColor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereColumnid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereFlowId($value)
|
||||
|
||||
@@ -128,8 +128,8 @@ class ProjectPermission extends AbstractModel
|
||||
/**
|
||||
* 更新项目权限
|
||||
*
|
||||
* @param int $projectId
|
||||
* @param array $permissions
|
||||
* @param int $projectId
|
||||
* @param $newPermissions
|
||||
* @return ProjectPermission
|
||||
*/
|
||||
public static function updatePermissions($projectId, $newPermissions)
|
||||
@@ -146,9 +146,9 @@ class ProjectPermission extends AbstractModel
|
||||
|
||||
/**
|
||||
* 检查用户是否有执行特定动作的权限
|
||||
* @param string $action 动作名称
|
||||
* @param Project $project 项目实例
|
||||
* @param ProjectTask $task 任务实例
|
||||
* @param string $action 动作名称
|
||||
* @param ProjectTask|null $task 任务实例
|
||||
* @return bool
|
||||
*/
|
||||
public static function userTaskPermission(Project $project, $action, ProjectTask $task = null)
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace App\Models;
|
||||
* @property string $name 标签名称
|
||||
* @property string|null $desc 标签描述
|
||||
* @property string|null $color 颜色
|
||||
* @property int $sort 排序
|
||||
* @property int $userid 创建人
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
@@ -29,6 +30,7 @@ namespace App\Models;
|
||||
* @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 whereSort($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
@@ -36,7 +38,6 @@ namespace App\Models;
|
||||
class ProjectTag extends AbstractModel
|
||||
{
|
||||
protected $hidden = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
@@ -50,6 +51,7 @@ class ProjectTag extends AbstractModel
|
||||
'name',
|
||||
'desc',
|
||||
'color',
|
||||
'sort',
|
||||
'userid'
|
||||
];
|
||||
|
||||
|
||||
@@ -485,7 +485,7 @@ class ProjectTask extends AbstractModel
|
||||
foreach ($projectFlowItem as $item) {
|
||||
if ($item->status == 'start') {
|
||||
$task->flow_item_id = $item->id;
|
||||
$task->flow_item_name = $item->status . "|" . $item->name;
|
||||
$task->flow_item_name = $item->status . "|" . $item->name . "|" . $item->color;
|
||||
$owner = array_merge($owner, $item->userids);
|
||||
break;
|
||||
}
|
||||
@@ -617,7 +617,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 +631,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'])) {
|
||||
@@ -644,7 +649,7 @@ class ProjectTask extends AbstractModel
|
||||
$data['column_id'] = $newFlowItem->columnid;
|
||||
}
|
||||
$this->flow_item_id = $newFlowItem->id;
|
||||
$this->flow_item_name = $newFlowItem->status . "|" . $newFlowItem->name;
|
||||
$this->flow_item_name = $newFlowItem->status . "|" . $newFlowItem->name . "|" . $newFlowItem->color;
|
||||
$this->addLog("修改{任务}状态", [
|
||||
'flow' => $flowData,
|
||||
'change' => [$currentFlowItem?->name, $newFlowItem->name]
|
||||
@@ -729,7 +734,9 @@ class ProjectTask extends AbstractModel
|
||||
if (count($older) == 0 && count($array) == 1 && $array[0] == User::userid()) {
|
||||
$this->addLog("认领{任务}");
|
||||
} else {
|
||||
$this->addLog("修改{任务}负责人", ['userid' => $array]);
|
||||
if (array_merge(array_diff($array, $older), array_diff($older, $array))) {
|
||||
$this->addLog("修改{任务}负责人", ['userid' => $array]);
|
||||
}
|
||||
}
|
||||
$this->taskPush(array_values(array_diff($array, $older)), 0);
|
||||
}
|
||||
@@ -770,6 +777,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 +843,20 @@ 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) {
|
||||
$effectiveEndTime = $existAt ? Carbon::parse($this->end_at)->min(Carbon::now()) : Carbon::now();
|
||||
$this->addLog("{任务}超期未完成", [
|
||||
'cache' => [
|
||||
'task_at' => $oldStringAt,
|
||||
'change_at' => $newStringAt,
|
||||
'over_sec' => $effectiveEndTime->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]
|
||||
@@ -870,6 +891,7 @@ class ProjectTask extends AbstractModel
|
||||
}
|
||||
// 协助人员
|
||||
if (Arr::exists($data, 'assist')) {
|
||||
$older = $this->taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||
$array = [];
|
||||
$assist = is_array($data['assist']) ? $data['assist'] : [$data['assist']];
|
||||
if (count($assist) > 10) {
|
||||
@@ -890,7 +912,9 @@ class ProjectTask extends AbstractModel
|
||||
$array[] = $uid;
|
||||
}
|
||||
if ($array) {
|
||||
$this->addLog("修改{任务}协助人员", ['userid' => $array]);
|
||||
if (array_merge(array_diff($array, $older), array_diff($older, $array))) {
|
||||
$this->addLog("修改{任务}协助人员", ['userid' => $array]);
|
||||
}
|
||||
}
|
||||
$rows = ProjectTaskUser::whereTaskId($this->id)->whereOwner(0)->whereNotIn('userid', $array)->get();
|
||||
if ($rows->isNotEmpty()) {
|
||||
@@ -1119,12 +1143,14 @@ class ProjectTask extends AbstractModel
|
||||
*/
|
||||
public function copyTask()
|
||||
{
|
||||
if ($this->parent_id > 0) {
|
||||
throw new ApiException('子任务禁止复制');
|
||||
$source = $this->fresh(['content', 'taskFile', 'taskUser']);
|
||||
if (!$source) {
|
||||
throw new ApiException('任务不存在');
|
||||
}
|
||||
return AbstractModel::transaction(function() {
|
||||
// 复制任务
|
||||
$task = $this->replicate();
|
||||
|
||||
return AbstractModel::transaction(function () use ($source) {
|
||||
// 复制任务(使用最新数据,避免复制临时字段)
|
||||
$task = $source->replicate();
|
||||
$task->dialog_id = 0;
|
||||
$task->archived_at = null;
|
||||
$task->archived_userid = 0;
|
||||
@@ -1133,21 +1159,21 @@ class ProjectTask extends AbstractModel
|
||||
$task->created_at = Carbon::now();
|
||||
$task->save();
|
||||
// 复制任务内容
|
||||
if ($this->content) {
|
||||
$tmp = $this->content->replicate();
|
||||
if ($source->content) {
|
||||
$tmp = $source->content->replicate();
|
||||
$tmp->task_id = $task->id;
|
||||
$tmp->created_at = Carbon::now();
|
||||
$tmp->save();
|
||||
}
|
||||
// 复制任务附件
|
||||
foreach ($this->taskFile as $taskFile) {
|
||||
foreach ($source->taskFile as $taskFile) {
|
||||
$tmp = $taskFile->replicate();
|
||||
$tmp->task_id = $task->id;
|
||||
$tmp->created_at = Carbon::now();
|
||||
$tmp->save();
|
||||
}
|
||||
// 复制任务成员
|
||||
foreach ($this->taskUser as $taskUser) {
|
||||
foreach ($source->taskUser as $taskUser) {
|
||||
$tmp = $taskUser->replicate();
|
||||
$tmp->task_id = $task->id;
|
||||
$tmp->task_pid = $task->id;
|
||||
@@ -1182,6 +1208,7 @@ class ProjectTask extends AbstractModel
|
||||
'important' => 1
|
||||
], function () use ($userid) {
|
||||
return [
|
||||
'important' => 1,
|
||||
'bot' => User::isBot($userid) ? 1 : 0,
|
||||
];
|
||||
});
|
||||
@@ -1328,6 +1355,9 @@ class ProjectTask extends AbstractModel
|
||||
$addMsg = $this->parent_id == 0 && $this->dialog_id > 0;
|
||||
if ($complete_at === null) {
|
||||
// 标记未完成
|
||||
if (!$this->complete_at) {
|
||||
return; // 本来就未完成
|
||||
}
|
||||
$this->complete_at = null;
|
||||
$this->addLog("标记{任务}未完成");
|
||||
if ($addMsg) {
|
||||
@@ -1337,6 +1367,9 @@ class ProjectTask extends AbstractModel
|
||||
}
|
||||
} else {
|
||||
// 标记已完成
|
||||
if ($this->complete_at) {
|
||||
return; // 本来就已完成
|
||||
}
|
||||
if ($this->parent_id == 0) {
|
||||
if (self::whereParentId($this->id)->whereCompleteAt(null)->exists()) {
|
||||
throw new ApiException('子任务未完成', [
|
||||
@@ -1347,6 +1380,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 = '已完成';
|
||||
}
|
||||
@@ -1393,11 +1436,12 @@ class ProjectTask extends AbstractModel
|
||||
$this->archived_at = null;
|
||||
$this->archived_userid = User::userid();
|
||||
$this->archived_follow = 0;
|
||||
$this->addLog("任务取消归档");
|
||||
$logText = "任务取消归档";
|
||||
$userid = 0;
|
||||
} else {
|
||||
// 归档任务
|
||||
if ($isAuto === true) {
|
||||
$logText = "自动任务归档";
|
||||
$logText = "任务自动归档";
|
||||
$userid = 0;
|
||||
} else {
|
||||
$logText = "任务归档";
|
||||
@@ -1406,13 +1450,20 @@ class ProjectTask extends AbstractModel
|
||||
$this->archived_at = $archived_at;
|
||||
$this->archived_userid = $userid;
|
||||
$this->archived_follow = 0;
|
||||
$this->addLog($logText, [], $userid);
|
||||
}
|
||||
// 添加日志
|
||||
$this->addLog($logText, [], $userid);
|
||||
// 推送状态
|
||||
$this->pushMsg($archived_at === null ? 'recovery' : 'archived', [
|
||||
'id' => $this->id,
|
||||
'archived_at' => $this->archived_at,
|
||||
'archived_userid' => $this->archived_userid,
|
||||
]);
|
||||
// 更新对话时间
|
||||
if ($this->dialog_id > 0) {
|
||||
WebSocketDialogUser::whereDialogId($this->dialog_id)->update(['updated_at' => Carbon::now()]); // 因为是若提醒,可以直接使用 update 更新
|
||||
}
|
||||
// 更新保存
|
||||
self::whereParentId($this->id)->change([
|
||||
'archived_at' => $this->archived_at,
|
||||
'archived_userid' => $this->archived_userid,
|
||||
@@ -1509,8 +1560,9 @@ class ProjectTask extends AbstractModel
|
||||
* @param string $action
|
||||
* @param array|self $data 发送内容,默认为[id, parent_id, project_id, column_id, dialog_id]
|
||||
* @param array $userid 指定会员,默认为项目所有成员
|
||||
* @param bool $ignoreSelf 是否忽略当前连接
|
||||
*/
|
||||
public function pushMsg($action, $data = null, $userid = null)
|
||||
public function pushMsg($action, $data = null, $userid = null, $ignoreSelf = true)
|
||||
{
|
||||
if (!$this->project) {
|
||||
return;
|
||||
@@ -1522,77 +1574,91 @@ class ProjectTask extends AbstractModel
|
||||
'project_id' => $this->project_id,
|
||||
'column_id' => $this->column_id,
|
||||
'dialog_id' => $this->dialog_id,
|
||||
'visibility' => $this->visibility,
|
||||
];
|
||||
} elseif ($data instanceof self) {
|
||||
$data = $data->toArray();
|
||||
}
|
||||
//
|
||||
|
||||
// 获取接收会员
|
||||
if ($userid === null) {
|
||||
$userids = $this->project->relationUserids();
|
||||
} else {
|
||||
$userids = is_array($userid) ? $userid : [$userid];
|
||||
}
|
||||
//
|
||||
$array = [];
|
||||
if (Arr::exists($data, 'owner') || Arr::exists($data, 'assist')) {
|
||||
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get();
|
||||
// 负责人
|
||||
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
$owners = array_intersect($userids, $owners);
|
||||
if ($owners) {
|
||||
$array[] = [
|
||||
'userid' => array_values($owners),
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 1,
|
||||
'assist' => 1,
|
||||
])
|
||||
];
|
||||
}
|
||||
// 协助人
|
||||
$assists = $taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||
$assists = array_intersect($userids, $assists);
|
||||
if ($assists) {
|
||||
$array[] = [
|
||||
'userid' => array_values($assists),
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 0,
|
||||
'assist' => 1,
|
||||
])
|
||||
];
|
||||
}
|
||||
// 其他人
|
||||
switch ($data['visibility']) {
|
||||
case 1:
|
||||
// 项目人员,除了负责人、协助人项目其他人
|
||||
$userids = array_diff($userids, $owners, $assists);
|
||||
break;
|
||||
case 2:
|
||||
// 任务人员,除了负责人、协助人
|
||||
$userids = [];
|
||||
break;
|
||||
case 3:
|
||||
// 指定成员
|
||||
$specifys = ProjectTaskVisibilityUser::select(['userid'])->whereTaskId($data['id'])->pluck('userid')->toArray();
|
||||
$userids = array_diff($specifys, $owners, $assists);
|
||||
break;
|
||||
default:
|
||||
$userids = [];
|
||||
break;
|
||||
}
|
||||
if ($userids) {
|
||||
$array[] = [
|
||||
'userid' => array_values($userids),
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 0,
|
||||
'assist' => 0,
|
||||
])
|
||||
];
|
||||
}
|
||||
$userids = array_values(array_unique(array_map('intval', $userids)));
|
||||
if (empty($userids)) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
|
||||
// 按可见性分组推送
|
||||
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get();
|
||||
$ownerList = $taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
$assistList = $taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||
|
||||
$ownerUsers = array_values(array_intersect($userids, $ownerList));
|
||||
$assistUsers = array_values(array_diff(array_intersect($userids, $assistList), $ownerUsers));
|
||||
|
||||
$array = [];
|
||||
|
||||
// 负责人
|
||||
if ($ownerUsers) {
|
||||
$array[] = [
|
||||
'userid' => $ownerUsers,
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 1,
|
||||
'assist' => 0,
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
// 协助人
|
||||
if ($assistUsers) {
|
||||
$array[] = [
|
||||
'userid' => $assistUsers,
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 0,
|
||||
'assist' => 1,
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
// 其他人
|
||||
$otherUsers = [];
|
||||
switch (intval($data['visibility'])) {
|
||||
case 1:
|
||||
// 项目人员:除了负责人、协助人项目其他人
|
||||
$otherUsers = array_diff($userids, $ownerUsers, $assistUsers);
|
||||
break;
|
||||
case 2:
|
||||
// 任务人员:除了负责人、协助人
|
||||
// $otherUsers = [];
|
||||
break;
|
||||
case 3:
|
||||
// 指定成员
|
||||
$specifys = ProjectTaskVisibilityUser::select(['userid'])->whereTaskId($data['id'])->pluck('userid')->toArray();
|
||||
$otherUsers = array_diff(array_intersect($userids, $specifys), $ownerUsers, $assistUsers);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($otherUsers) {
|
||||
$array[] = [
|
||||
'userid' => array_values($otherUsers),
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 0,
|
||||
'assist' => 0,
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($array)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 推送
|
||||
foreach ($array as $item) {
|
||||
$params = [
|
||||
'ignoreFd' => Request::header('fd'),
|
||||
'ignoreFd' => $ignoreSelf ? Request::header('fd') : null,
|
||||
'userid' => $item['userid'],
|
||||
'msg' => [
|
||||
'type' => 'projectTask',
|
||||
@@ -1801,16 +1867,30 @@ class ProjectTask extends AbstractModel
|
||||
* @param int $flowItemId
|
||||
* @param array $owner
|
||||
* @param array $assist
|
||||
* @param string $completeAt
|
||||
* @param string|null $completed
|
||||
* @return bool
|
||||
*/
|
||||
public function moveTask(int $projectId, int $columnId,int $flowItemId = 0,array $owner = [], array $assist = [], string $completeAt='')
|
||||
public function moveTask(int $projectId, int $columnId, int $flowItemId = 0, array $owner = [], array $assist = [], ?string $completed = null)
|
||||
{
|
||||
AbstractModel::transaction(function () use ($projectId, $columnId, $flowItemId, $owner, $assist, $completeAt) {
|
||||
AbstractModel::transaction(function () use ($projectId, $columnId, $flowItemId, $owner, $assist, $completed) {
|
||||
$newTaskUser = array_merge($owner, $assist);
|
||||
//
|
||||
$oldProject = Project::find($this->project_id);
|
||||
$newProject = $this->project_id != $projectId ? Project::find($projectId) : $oldProject;
|
||||
if (!$oldProject || !$newProject) {
|
||||
throw new ApiException('项目不存在');
|
||||
}
|
||||
//
|
||||
$this->project_id = $projectId;
|
||||
$this->column_id = $columnId;
|
||||
// 日志
|
||||
$log = $this->addLog("移动{任务}", [
|
||||
'change' => [$oldProject->name, $newProject->name]
|
||||
]);
|
||||
if ($this->dialog_id) {
|
||||
$notice = $oldProject->id != $newProject->id ? "「{$oldProject->name}」移动至「{$newProject->name}」" : $log->detail;
|
||||
WebSocketDialogMsg::sendMsg(null, $this->dialog_id, 'notice', ['notice' => $notice], User::userid(), true, true);
|
||||
}
|
||||
// 任务内容
|
||||
if ($this->content) {
|
||||
$this->content->project_id = $projectId;
|
||||
@@ -1844,26 +1924,25 @@ class ProjectTask extends AbstractModel
|
||||
]);
|
||||
//
|
||||
if ($flowItemId) {
|
||||
// 更新任务流程
|
||||
$flowItem = projectFlowItem::whereProjectId($projectId)->whereId($flowItemId)->first();
|
||||
$this->flow_item_id = $flowItemId;
|
||||
$this->flow_item_name = $flowItem->status . "|" . $flowItem->name;
|
||||
$this->flow_item_name = $flowItem->status . "|" . $flowItem->name . "|" . $flowItem->color;
|
||||
if ($flowItem->status == 'end') {
|
||||
$this->completeTask(Carbon::now(), $flowItem->name);
|
||||
} else {
|
||||
$this->completeTask(null);
|
||||
}
|
||||
} else {
|
||||
// 没有流程只更新状态
|
||||
$this->flow_item_id = 0;
|
||||
$this->flow_item_name = '';
|
||||
}
|
||||
//
|
||||
if ($completeAt) {
|
||||
$this->complete_at = $completeAt;
|
||||
if ($completed !== null) {
|
||||
$this->completeTask($completed ? Carbon::now(): null);
|
||||
}
|
||||
}
|
||||
//
|
||||
$this->save();
|
||||
//
|
||||
$this->addLog("移动{任务}");
|
||||
});
|
||||
$this->pushMsg('update');
|
||||
return true;
|
||||
|
||||
154
app/Models/ProjectTaskRelation.php
Normal file
154
app/Models/ProjectTaskRelation.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTaskRelation
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $task_id 任务ID
|
||||
* @property int $related_task_id 关联任务ID
|
||||
* @property string $direction 关系方向: mention/mentioned_by
|
||||
* @property int|null $dialog_id 来源会话ID
|
||||
* @property int|null $msg_id 来源消息ID
|
||||
* @property int|null $userid 提及人
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\ProjectTask|null $relatedTask
|
||||
* @property-read \App\Models\ProjectTask|null $task
|
||||
* @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|ProjectTaskRelation newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereDirection($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereMsgId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereRelatedTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTaskRelation extends AbstractModel
|
||||
{
|
||||
public const DIRECTION_MENTION = 'mention';
|
||||
public const DIRECTION_MENTIONED_BY = 'mentioned_by';
|
||||
|
||||
protected $fillable = [
|
||||
'task_id',
|
||||
'related_task_id',
|
||||
'direction',
|
||||
'dialog_id',
|
||||
'msg_id',
|
||||
'userid',
|
||||
];
|
||||
|
||||
public function task(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ProjectTask::class, 'task_id');
|
||||
}
|
||||
|
||||
public function relatedTask(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ProjectTask::class, 'related_task_id');
|
||||
}
|
||||
|
||||
public static function recordMentionsFromMessage(WebSocketDialogMsg $msg): void
|
||||
{
|
||||
if ($msg->type !== 'text') {
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = $msg->msg;
|
||||
if (!is_array($payload)) {
|
||||
$payload = Base::json2array($msg->getRawOriginal('msg'));
|
||||
}
|
||||
|
||||
$text = $payload['text'] ?? '';
|
||||
if (!$text || !preg_match_all('/<span class="mention task" data-id="(\d+)">#?(.*?)<\/span>/i', $text, $matches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$targetIds = array_values(array_unique(array_filter(array_map('intval', $matches[1] ?? []))));
|
||||
if (empty($targetIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sourceTasks = ProjectTask::with('project')->whereDialogId($msg->dialog_id)->get();
|
||||
if ($sourceTasks->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$targetTasks = ProjectTask::with('project')->whereIn('id', $targetIds)->get()->keyBy('id');
|
||||
if ($targetTasks->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pushTasks = [];
|
||||
foreach ($sourceTasks as $sourceTask) {
|
||||
foreach ($targetIds as $targetId) {
|
||||
if ($targetId === $sourceTask->id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetTask = $targetTasks->get($targetId);
|
||||
if (!$targetTask) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mentionRelation = static::updateOrCreate(
|
||||
[
|
||||
'task_id' => $sourceTask->id,
|
||||
'related_task_id' => $targetTask->id,
|
||||
'direction' => self::DIRECTION_MENTION,
|
||||
],
|
||||
[
|
||||
'dialog_id' => $msg->dialog_id,
|
||||
'msg_id' => $msg->id,
|
||||
'userid' => $msg->userid,
|
||||
]
|
||||
);
|
||||
|
||||
if ($mentionRelation->wasRecentlyCreated || $mentionRelation->wasChanged()) {
|
||||
$pushTasks[$sourceTask->id] = $sourceTask;
|
||||
}
|
||||
|
||||
$reverseRelation = static::updateOrCreate(
|
||||
[
|
||||
'task_id' => $targetTask->id,
|
||||
'related_task_id' => $sourceTask->id,
|
||||
'direction' => self::DIRECTION_MENTIONED_BY,
|
||||
],
|
||||
[
|
||||
'dialog_id' => $msg->dialog_id,
|
||||
'msg_id' => $msg->id,
|
||||
'userid' => $msg->userid,
|
||||
]
|
||||
);
|
||||
|
||||
if ($reverseRelation->wasRecentlyCreated || $reverseRelation->wasChanged()) {
|
||||
$pushTasks[$targetTask->id] = $targetTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pushTasks as $task) {
|
||||
$task->loadMissing('project');
|
||||
if (!$task->project) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$task->pushMsg('relation', null, null, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace App\Models;
|
||||
* @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|null $user
|
||||
* @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)
|
||||
|
||||
@@ -68,7 +68,18 @@ class ProjectTaskUser extends AbstractModel
|
||||
$item->save();
|
||||
}
|
||||
if ($item->projectTask) {
|
||||
$item->projectTask->addLog("移交{任务}身份", ['userid' => [$originalUserid, ' => ', $newUserid]], 0, 1);
|
||||
$item->projectTask->addLog("移交{任务}身份", [
|
||||
'change' => [
|
||||
[
|
||||
'type' => 'user',
|
||||
'data' => $originalUserid,
|
||||
],
|
||||
[
|
||||
'type' => 'user',
|
||||
'data' => $newUserid,
|
||||
]
|
||||
],
|
||||
], 0, 1);
|
||||
if (!in_array($item->task_pid, $tastIds)) {
|
||||
$tastIds[] = $item->task_pid;
|
||||
$item->projectTask->syncDialogUser();
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Module\Base;
|
||||
* @property int|null $userid 成员ID
|
||||
* @property int|null $owner 是否负责人
|
||||
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
|
||||
* @property int|null $sort 排序(ASC)
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Project|null $project
|
||||
@@ -28,6 +29,7 @@ use App\Module\Base;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereOwner($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereSort($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereTopAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUserid($value)
|
||||
@@ -74,7 +76,18 @@ class ProjectUser extends AbstractModel
|
||||
$item->project->name = "【{$name}】{$item->project->name}";
|
||||
$item->project->save();
|
||||
}
|
||||
$item->project->addLog("移交项目身份", ['userid' => [$originalUserid, ' => ', $newUserid]]);
|
||||
$item->project->addLog("移交项目身份", [
|
||||
'change' => [
|
||||
[
|
||||
'type' => 'user',
|
||||
'data' => $originalUserid
|
||||
],
|
||||
[
|
||||
'type' => 'user',
|
||||
'data' => $newUserid
|
||||
],
|
||||
],
|
||||
]);
|
||||
$item->project->syncDialogUser();
|
||||
$projectIds[] = $item->project_id;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use JetBrains\PhpStorm\Pure;
|
||||
|
||||
/**
|
||||
@@ -77,6 +79,16 @@ class Report extends AbstractModel
|
||||
->withPivot("receive_at", "read");
|
||||
}
|
||||
|
||||
public function aiAnalyses(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReportAnalysis::class, 'rid');
|
||||
}
|
||||
|
||||
public function aiAnalysis(): HasOne
|
||||
{
|
||||
return $this->hasOne(ReportAnalysis::class, 'rid');
|
||||
}
|
||||
|
||||
public function sendUser()
|
||||
{
|
||||
return $this->hasOne(User::class, "userid", "userid");
|
||||
@@ -95,6 +107,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
|
||||
|
||||
27
app/Models/ReportAnalysis.php
Normal file
27
app/Models/ReportAnalysis.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReportAnalysis extends AbstractModel
|
||||
{
|
||||
protected $table = 'report_ai_analyses';
|
||||
|
||||
protected $fillable = [
|
||||
'rid',
|
||||
'userid',
|
||||
'model',
|
||||
'analysis_text',
|
||||
'meta',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'meta' => 'array',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Report::class, 'rid');
|
||||
}
|
||||
}
|
||||
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,12 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Timer;
|
||||
use App\Module\AI;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\Setting
|
||||
@@ -45,6 +49,7 @@ class Setting extends AbstractModel
|
||||
}
|
||||
$value = Base::json2array($value);
|
||||
switch ($this->name) {
|
||||
// 系统设置
|
||||
case 'system':
|
||||
$value['system_alias'] = $value['system_alias'] ?: env('APP_NAME');
|
||||
$value['image_compress'] = $value['image_compress'] ?: 'open';
|
||||
@@ -55,35 +60,43 @@ class Setting extends AbstractModel
|
||||
}
|
||||
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;
|
||||
|
||||
// AI 机器人设置
|
||||
case 'aibotSetting':
|
||||
if ($value['claude_token'] && empty($value['claude_key'])) {
|
||||
$value['claude_key'] = $value['claude_token'];
|
||||
}
|
||||
$array = [];
|
||||
$aiList = ['openai', 'claude', 'deepseek', 'gemini', 'zhipu', 'qianwen', 'wenxin'];
|
||||
$fieldList = ['key', 'model', 'base_url', 'agency', 'temperature', 'system', 'secret'];
|
||||
$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;
|
||||
if ($fieldName == 'temperature' && $value[$key]) {
|
||||
$array[$key] = floatval(min(1, max(0, floatval($value[$key]) ?: 0.7)));
|
||||
continue;
|
||||
$content = $value[$key] ? trim($value[$key]) : '';
|
||||
switch ($fieldName) {
|
||||
case 'models':
|
||||
if ($content) {
|
||||
$content = explode("\n", $content);
|
||||
$content = array_filter($content);
|
||||
}
|
||||
$content = is_array($content) ? implode("\n", $content) : '';
|
||||
break;
|
||||
case 'model':
|
||||
$models = Setting::AIBotModels2Array($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] = $value[$key] ?: match ($key) {
|
||||
'openai_model' => 'gpt-4o-mini',
|
||||
'claude_model' => 'claude-3-5-sonnet-latest',
|
||||
'deepseek_model' => 'deepseek-chat',
|
||||
'gemini_model' => 'gemini-1.5-flash',
|
||||
'zhipu_model' => 'glm-4',
|
||||
'qianwen_model' => 'qwen-turbo',
|
||||
'wenxin_model' => 'ernie-4.0-8k',
|
||||
default => '',
|
||||
};
|
||||
$array[$key] = $content;
|
||||
}
|
||||
}
|
||||
$value = $array;
|
||||
@@ -93,14 +106,62 @@ class Setting extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否开启AI
|
||||
* @param $ai
|
||||
* 是否开启 AI 助手
|
||||
* @return bool
|
||||
*/
|
||||
public static function AIOpen($ai = 'openai')
|
||||
public static function AIOpen()
|
||||
{
|
||||
$array = Base::setting('aibotSetting');
|
||||
return !!$array[$ai . '_key'];
|
||||
$setting = Base::setting('aibotSetting');
|
||||
if (!is_array($setting) || empty($setting)) {
|
||||
return false;
|
||||
}
|
||||
foreach (AI::TEXT_MODEL_PRIORITY as $vendor) {
|
||||
if (self::isAIBotVendorEnabled($setting, $vendor)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 AI 机器人厂商是否启用
|
||||
* @param array $setting
|
||||
* @param string $vendor
|
||||
* @return bool
|
||||
*/
|
||||
protected static function isAIBotVendorEnabled(array $setting, string $vendor): bool
|
||||
{
|
||||
$key = trim((string)($setting[$vendor . '_key'] ?? ''));
|
||||
return match ($vendor) {
|
||||
'ollama' => $key !== '' || !empty($setting['ollama_base_url']),
|
||||
'wenxin' => $key !== '' && !empty($setting['wenxin_secret']),
|
||||
default => $key !== '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 机器人模型转数组
|
||||
* @param $models
|
||||
* @param bool $retValue
|
||||
* @return array
|
||||
*/
|
||||
public static function AIBotModels2Array($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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,4 +200,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,6 +15,7 @@ use Hedeqiang\UMeng\IOS;
|
||||
* @property string|null $alias 别名
|
||||
* @property string|null $platform 平台类型
|
||||
* @property string|null $device 设备类型
|
||||
* @property string|null $device_hash 设备哈希值,用于关联UserDevice表
|
||||
* @property string|null $version 应用版本号
|
||||
* @property string|null $ua userAgent
|
||||
* @property int|null $is_notified 通知权限
|
||||
@@ -32,19 +33,80 @@ use Hedeqiang\UMeng\IOS;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereAlias($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereDevice($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereVersion($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereDeviceHash($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
|
||||
{
|
||||
protected $table = 'umeng_alias';
|
||||
|
||||
private static $waitSend = [];
|
||||
|
||||
|
||||
/**
|
||||
* 推送消息
|
||||
* @param $push
|
||||
* @return void
|
||||
*/
|
||||
private static function sendTask($push = null)
|
||||
{
|
||||
if ($push) {
|
||||
self::$waitSend[] = $push;
|
||||
}
|
||||
|
||||
if (!self::$waitSend) {
|
||||
return;
|
||||
}
|
||||
|
||||
$first = array_shift(self::$waitSend);
|
||||
if (empty($first)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$instance = null;
|
||||
$responsePayload = null;
|
||||
|
||||
try {
|
||||
switch ($first['platform']) {
|
||||
case 'ios':
|
||||
$instance = new IOS($first['config']);
|
||||
break;
|
||||
case 'android':
|
||||
$instance = new Android($first['config']);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
$responsePayload = $instance->send($first['data']);
|
||||
} catch (\Exception $e) {
|
||||
$responsePayload = [
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
$first['retry'] = intval($first['retry'] ?? 0) + 1;
|
||||
if ($first['retry'] > 3) {
|
||||
info("[PushMsg] fail: " . $e->getMessage());
|
||||
} else {
|
||||
info("[PushMsg] retry ({$first['retry']}): " . $e->getMessage());
|
||||
self::$waitSend[] = $first;
|
||||
}
|
||||
} finally {
|
||||
if ($instance !== null) {
|
||||
UmengLog::create([
|
||||
'request' => Base::array2json($first['data']),
|
||||
'response' => Base::array2json($responsePayload),
|
||||
]);
|
||||
}
|
||||
self::sendTask();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送内容处理
|
||||
* @param $string
|
||||
@@ -88,13 +150,13 @@ class UmengAlias extends AbstractModel
|
||||
* @param string $alias
|
||||
* @param string $platform
|
||||
* @param array $array [title, subtitle, body, description, extra, seconds, badge]
|
||||
* @return array|false
|
||||
* @return void
|
||||
*/
|
||||
public static function pushMsgToAlias($alias, $platform, $array)
|
||||
private static function pushMsgToAlias($alias, $platform, $array)
|
||||
{
|
||||
$config = self::getPushConfig();
|
||||
if ($config === false) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
//
|
||||
$title = self::specialCharacters($array['title'] ?: ''); // 标题
|
||||
@@ -103,70 +165,83 @@ class UmengAlias extends AbstractModel
|
||||
$description = $array['description'] ?: 'no description'; // 描述
|
||||
$extra = is_array($array['extra']) ? $array['extra'] : []; // 额外参数
|
||||
$seconds = intval($array['seconds']) ?: 86400; // 有效时间(单位:秒)
|
||||
$badge = intval($array['badge']) ?: 0; // 角标数(iOS)
|
||||
$badge = intval($array['badge']) ?: 0; // 角标数
|
||||
//
|
||||
switch ($platform) {
|
||||
case 'ios':
|
||||
if (!isset($config['iOS'])) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
$ios = new IOS($config);
|
||||
return $ios->send([
|
||||
'description' => $description,
|
||||
'payload' => array_merge([
|
||||
'aps' => [
|
||||
'alert' => [
|
||||
'title' => $title,
|
||||
'subtitle' => $subtitle,
|
||||
'body' => $body,
|
||||
self::sendTask([
|
||||
'platform' => $platform,
|
||||
'config' => $config,
|
||||
'data' => [
|
||||
'description' => $description,
|
||||
'payload' => array_merge([
|
||||
'aps' => [
|
||||
'alert' => [
|
||||
'title' => $title,
|
||||
'subtitle' => $subtitle,
|
||||
'body' => $body,
|
||||
],
|
||||
'sound' => 'default',
|
||||
'badge' => $badge,
|
||||
],
|
||||
'sound' => 'default',
|
||||
'badge' => $badge,
|
||||
], $extra),
|
||||
'type' => 'customizedcast',
|
||||
'alias_type' => 'userid',
|
||||
'alias' => $alias,
|
||||
'policy' => [
|
||||
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
|
||||
],
|
||||
], $extra),
|
||||
'type' => 'customizedcast',
|
||||
'alias_type' => 'userid',
|
||||
'alias' => $alias,
|
||||
'policy' => [
|
||||
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
|
||||
],
|
||||
]
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'android':
|
||||
if (!isset($config['Android'])) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
$android = new Android($config);
|
||||
return $android->send([
|
||||
'description' => $description,
|
||||
'payload' => array_merge([
|
||||
'display_type' => 'notification',
|
||||
'body' => [
|
||||
'ticker' => $title,
|
||||
'text' => $body,
|
||||
'title' => $title,
|
||||
'after_open' => 'go_app',
|
||||
'play_sound' => true,
|
||||
self::sendTask([
|
||||
'platform' => $platform,
|
||||
'config' => $config,
|
||||
'data' => [
|
||||
'description' => $description,
|
||||
'payload' => array_merge([
|
||||
'display_type' => 'notification',
|
||||
'body' => [
|
||||
'ticker' => $title,
|
||||
'text' => $body,
|
||||
'title' => $title,
|
||||
'after_open' => 'go_app',
|
||||
'play_sound' => true,
|
||||
'set_badge' => min(99, $badge),
|
||||
],
|
||||
], $extra),
|
||||
'type' => 'customizedcast',
|
||||
'alias_type' => 'userid',
|
||||
'alias' => $alias,
|
||||
'mipush' => true,
|
||||
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
|
||||
'policy' => [
|
||||
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
|
||||
],
|
||||
], $extra),
|
||||
'type' => 'customizedcast',
|
||||
'alias_type' => 'userid',
|
||||
'alias' => $alias,
|
||||
'mipush' => true,
|
||||
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
|
||||
'policy' => [
|
||||
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
|
||||
],
|
||||
'channel_properties' => [
|
||||
'vivo_category' => 'IM',
|
||||
'huawei_channel_importance' => 'NORMAL',
|
||||
'huawei_channel_category' => 'IM',
|
||||
'channel_fcm' => 0,
|
||||
],
|
||||
'category' => 1,
|
||||
'channel_properties' => [
|
||||
'main_activity' => 'com.dootask.task.WelcomeActivity',
|
||||
'oppo_channel_id' => 'dootask',
|
||||
'vivo_category' => 'IM',
|
||||
'huawei_channel_importance' => 'NORMAL',
|
||||
'huawei_channel_category' => 'IM',
|
||||
'channel_fcm' => 0,
|
||||
],
|
||||
'local_properties' => [
|
||||
'importance' => 'IMPORTANCE_DEFAULT',
|
||||
'category' => 'CATEGORY_MESSAGE',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
32
app/Models/UmengLog.php
Normal file
32
app/Models/UmengLog.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\UmengLog
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $request 请求参数
|
||||
* @property string|null $response 推送返回
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @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|UmengLog newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereRequest($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereResponse($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UmengLog extends AbstractModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
@@ -15,15 +14,18 @@ use Carbon\Carbon;
|
||||
* App\Models\User
|
||||
*
|
||||
* @property int $userid
|
||||
* @property array $identity
|
||||
* @property array $department
|
||||
* @property array $identity 身份
|
||||
* @property array $department 所属部门
|
||||
* @property string|null $az A-Z
|
||||
* @property string|null $pinyin 拼音(主要用于搜索)
|
||||
* @property string|null $email
|
||||
* @property string|null $email 邮箱
|
||||
* @property string|null $tel 联系电话
|
||||
* @property string $nickname
|
||||
* @property string|null $profession
|
||||
* @property string $userimg
|
||||
* @property string $nickname 昵称
|
||||
* @property string|null $profession 职位/职称
|
||||
* @property \Illuminate\Support\Carbon|null $birthday 生日
|
||||
* @property string|null $address 地址
|
||||
* @property string|null $introduction 个人简介
|
||||
* @property string $userimg 头像
|
||||
* @property string|null $encrypt
|
||||
* @property string|null $password 登录密码
|
||||
* @property int|null $changepass 登录需要修改密码
|
||||
@@ -34,7 +36,7 @@ use Carbon\Carbon;
|
||||
* @property \Illuminate\Support\Carbon|null $line_at 最后在线时间(接口)
|
||||
* @property int|null $task_dialog_id 最后打开的任务会话ID
|
||||
* @property string|null $created_ip 注册IP
|
||||
* @property \Illuminate\Support\Carbon|null $disable_at
|
||||
* @property \Illuminate\Support\Carbon|null $disable_at 禁用时间(离职时间)
|
||||
* @property int|null $email_verity 邮箱是否已验证
|
||||
* @property int|null $bot 是否机器人
|
||||
* @property string|null $lang 语言首选项
|
||||
@@ -173,10 +175,9 @@ class User extends AbstractModel
|
||||
return UserDepartment::where('owner_userid', $this->userid)->exists();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取机器人所有者
|
||||
* @return int|mixed
|
||||
* @return int
|
||||
*/
|
||||
public function getBotOwner()
|
||||
{
|
||||
@@ -184,9 +185,9 @@ class User extends AbstractModel
|
||||
return 0;
|
||||
}
|
||||
$key = "userBotOwner::" . $this->userid;
|
||||
return Cache::remember($key, now()->addMonth(), function() {
|
||||
return intval(Cache::remember($key, now()->addMonth(), function() {
|
||||
return intval(UserBot::whereBotId($this->userid)->value('userid')) ?: $this->userid;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,6 +243,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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否管理员
|
||||
*/
|
||||
@@ -419,7 +445,9 @@ class User extends AbstractModel
|
||||
{
|
||||
$user = self::authInfo();
|
||||
if (!$user) {
|
||||
if (Base::token()) {
|
||||
$token = Base::token();
|
||||
if ($token) {
|
||||
UserDevice::forget($token);
|
||||
throw new ApiException('身份已失效,请重新登录', [], -1);
|
||||
} else {
|
||||
throw new ApiException('请登录后继续...', [], -1);
|
||||
@@ -441,31 +469,46 @@ class User extends AbstractModel
|
||||
private static function authInfo()
|
||||
{
|
||||
if (RequestContext::has('auth')) {
|
||||
// 缓存
|
||||
return RequestContext::get('auth');
|
||||
}
|
||||
if (Doo::userId() > 0
|
||||
&& !Doo::userExpired()
|
||||
&& $user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first()) {
|
||||
$upArray = [];
|
||||
if (Base::getIp() && $user->line_ip != Base::getIp()) {
|
||||
$upArray['line_ip'] = Base::getIp();
|
||||
}
|
||||
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
|
||||
$upArray['line_at'] = Carbon::now();
|
||||
}
|
||||
$headerLanguage = RequestContext::get('header_language');
|
||||
if (empty($user->lang) || $headerLanguage) {
|
||||
if (Doo::checkLanguage($headerLanguage) && $user->lang != $headerLanguage) {
|
||||
$upArray['lang'] = $headerLanguage;
|
||||
}
|
||||
}
|
||||
if ($upArray) {
|
||||
$user->updateInstance($upArray);
|
||||
$user->save();
|
||||
}
|
||||
return RequestContext::save('auth', $user);
|
||||
if (Doo::userId() <= 0) {
|
||||
// 没有登录
|
||||
return RequestContext::save('auth', false);
|
||||
}
|
||||
return RequestContext::save('auth', false);
|
||||
if (Doo::userExpired()) {
|
||||
// 登录过期
|
||||
return RequestContext::save('auth', false);
|
||||
}
|
||||
if (!UserDevice::check()) {
|
||||
// token 不存在
|
||||
return RequestContext::save('auth', false);
|
||||
}
|
||||
$user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first();
|
||||
if (!$user) {
|
||||
// 登录信息不匹配
|
||||
return RequestContext::save('auth', false);
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
$upArray = [];
|
||||
if (Base::getIp() && $user->line_ip != Base::getIp()) {
|
||||
$upArray['line_ip'] = Base::getIp();
|
||||
}
|
||||
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
|
||||
$upArray['line_at'] = Carbon::now();
|
||||
}
|
||||
$headerLanguage = RequestContext::get('header_language');
|
||||
if (empty($user->lang) || $headerLanguage) {
|
||||
if (Doo::checkLanguage($headerLanguage) && $user->lang != $headerLanguage) {
|
||||
$upArray['lang'] = $headerLanguage;
|
||||
}
|
||||
}
|
||||
if ($upArray) {
|
||||
$user->updateInstance($upArray);
|
||||
$user->save();
|
||||
}
|
||||
return RequestContext::save('auth', $user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,11 +532,28 @@ class User extends AbstractModel
|
||||
} else {
|
||||
$token = Doo::userToken();
|
||||
}
|
||||
UserDevice::record($token);
|
||||
unset($userinfo->encrypt);
|
||||
unset($userinfo->password);
|
||||
return $userinfo->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成无设备的 token(主要用于接口调用,此 token 不检查设备是否存在)
|
||||
* @param self $userinfo
|
||||
* @param $ttl
|
||||
* @return mixed
|
||||
*/
|
||||
public static function generateTokenNoDevice($userinfo, $ttl)
|
||||
{
|
||||
$key = 'user_token_no_device_' . $userinfo->userid;
|
||||
return Cache::remember($key, $ttl, function () use ($userinfo, $ttl) {
|
||||
$token = Doo::tokenEncode($userinfo->userid, $userinfo->email, $userinfo->encrypt);
|
||||
Cache::put(UserDevice::ck(md5($token)), $userinfo->userid, $ttl);
|
||||
return $token;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* userid 获取 基础信息
|
||||
* @param int $userid 会员ID
|
||||
@@ -597,6 +657,10 @@ class User extends AbstractModel
|
||||
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':
|
||||
@@ -676,11 +740,11 @@ class User extends AbstractModel
|
||||
}
|
||||
}
|
||||
if ($update) {
|
||||
$botUser->updateInstance($update);
|
||||
if (isset($update['nickname'])) {
|
||||
if (isset($update['nickname']) && $botUser->nickname != $update['nickname']) {
|
||||
$botUser->az = Base::getFirstCharter($botUser->nickname);
|
||||
$botUser->pinyin = Base::cn2pinyin($botUser->nickname);
|
||||
}
|
||||
$botUser->updateInstance($update);
|
||||
$botUser->save();
|
||||
}
|
||||
return $botUser;
|
||||
@@ -689,17 +753,38 @@ class User extends AbstractModel
|
||||
/**
|
||||
* 是否机器人
|
||||
* @param $userid
|
||||
* @return bool|mixed
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
// 这个不会有变化,所以可以使用永久缓存
|
||||
return (bool)Cache::rememberForever('is-bot-user-' . $userid, function () use ($userid) {
|
||||
return (bool)User::find($userid)?->bot;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索用户
|
||||
* @param $key
|
||||
* @param $take
|
||||
* @return User[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
|
||||
*/
|
||||
public static function searchUser($key, $take = 20)
|
||||
{
|
||||
return User::select(User::$basicField)
|
||||
->where(function ($query) use ($key) {
|
||||
if (str_contains($key, "@")) {
|
||||
$query->where("email", "like", "%{$key}%");
|
||||
} else {
|
||||
$query->where("nickname", "like", "%{$key}%")
|
||||
->orWhere("pinyin", "like", "%{$key}%")
|
||||
->orWhere("profession", "like", "%{$key}%");
|
||||
}
|
||||
})->orderBy('userid')
|
||||
->take($take)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
96
app/Models/UserAppSort.php
Normal file
96
app/Models/UserAppSort.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\UserAppSort
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $userid 用户ID
|
||||
* @property array|null $sorts 排序配置
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereSorts($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserAppSort extends AbstractModel
|
||||
{
|
||||
protected $fillable = [
|
||||
'userid',
|
||||
'sorts',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sorts' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取用户排序配置
|
||||
* @param int $userid
|
||||
* @return array
|
||||
*/
|
||||
public static function getSorts(int $userid): array
|
||||
{
|
||||
$record = static::whereUserid($userid)->first();
|
||||
if (!$record) {
|
||||
return self::normalizeSorts([]);
|
||||
}
|
||||
return self::normalizeSorts($record->sorts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存排序配置
|
||||
* @param int $userid
|
||||
* @param array $sorts
|
||||
* @return static
|
||||
*/
|
||||
public static function saveSorts(int $userid, array $sorts): self
|
||||
{
|
||||
return static::updateOrCreate(
|
||||
['userid' => $userid],
|
||||
['sorts' => self::normalizeSorts($sorts)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化排序数据
|
||||
* @param mixed $sorts
|
||||
* @return array
|
||||
*/
|
||||
public static function normalizeSorts($sorts): array
|
||||
{
|
||||
$result = [
|
||||
'base' => [],
|
||||
'admin' => [],
|
||||
];
|
||||
if (!is_array($sorts)) {
|
||||
return $result;
|
||||
}
|
||||
foreach (['base', 'admin'] as $group) {
|
||||
$list = $sorts[$group] ?? [];
|
||||
if (!is_array($list)) {
|
||||
$list = [];
|
||||
}
|
||||
$normalized = [];
|
||||
foreach ($list as $value) {
|
||||
if (!is_string($value)) {
|
||||
continue;
|
||||
}
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
$normalized[] = $value;
|
||||
}
|
||||
$result[$group] = array_values(array_unique($normalized));
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,12 @@ namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Extranet;
|
||||
use App\Module\Ihttp;
|
||||
use App\Module\Timer;
|
||||
use App\Tasks\JokeSoupTask;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* App\Models\UserBot
|
||||
@@ -20,6 +21,7 @@ use Carbon\Carbon;
|
||||
* @property \Illuminate\Support\Carbon|null $clear_at 下一次清理时间
|
||||
* @property string|null $webhook_url 消息webhook地址
|
||||
* @property int|null $webhook_num 消息webhook请求次数
|
||||
* @property array|null $webhook_events Webhook事件配置
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
@@ -44,6 +46,86 @@ use Carbon\Carbon;
|
||||
*/
|
||||
class UserBot extends AbstractModel
|
||||
{
|
||||
public const WEBHOOK_EVENT_MESSAGE = 'message';
|
||||
public const WEBHOOK_EVENT_DIALOG_OPEN = 'dialog_open';
|
||||
public const WEBHOOK_EVENT_MEMBER_JOIN = 'member_join';
|
||||
public const WEBHOOK_EVENT_MEMBER_LEAVE = 'member_leave';
|
||||
|
||||
protected $casts = [
|
||||
'webhook_events' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取 webhook 事件配置
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
*/
|
||||
public function getWebhookEventsAttribute(mixed $value): array
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return self::normalizeWebhookEvents(null, true);
|
||||
}
|
||||
return self::normalizeWebhookEvents($value, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 webhook 事件配置
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function setWebhookEventsAttribute(mixed $value): void
|
||||
{
|
||||
$useFallback = $value === null;
|
||||
$this->attributes['webhook_events'] = Base::array2json(self::normalizeWebhookEvents($value, $useFallback));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要触发指定 webhook 事件
|
||||
*
|
||||
* @param string $event
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldDispatchWebhook(string $event): bool
|
||||
{
|
||||
if (!$this->webhook_url) {
|
||||
return false;
|
||||
}
|
||||
if (!preg_match('/^https?:\/\//', $this->webhook_url)) {
|
||||
return false;
|
||||
}
|
||||
return in_array($event, $this->webhook_events ?? [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 webhook
|
||||
*
|
||||
* @param string $event
|
||||
* @param array $data
|
||||
* @param int $timeout
|
||||
* @return array|null
|
||||
*/
|
||||
public function dispatchWebhook(string $event, array $data, int $timeout = 30): ?array
|
||||
{
|
||||
if (!$this->shouldDispatchWebhook($event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$data['event'] = $event;
|
||||
$result = Ihttp::ihttp_post($this->webhook_url, $data, $timeout);
|
||||
$this->increment('webhook_num');
|
||||
return $result;
|
||||
} catch (Throwable $th) {
|
||||
info(Base::array2json([
|
||||
'webhook_url' => $this->webhook_url,
|
||||
'data' => $data,
|
||||
'error' => $th->getMessage(),
|
||||
]));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否系统机器人
|
||||
@@ -55,16 +137,6 @@ class UserBot extends AbstractModel
|
||||
return str_ends_with($email, '@bot.system') && self::systemBotName($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否系统AI机器人
|
||||
* @param $email
|
||||
* @return bool
|
||||
*/
|
||||
public static function isAiBot($email)
|
||||
{
|
||||
return str_starts_with($email, 'ai-') && self::isSystemBot($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统机器人名称
|
||||
* @param $name string 邮箱 或 邮箱前缀
|
||||
@@ -84,10 +156,12 @@ class UserBot extends AbstractModel
|
||||
'ai-openai' => 'ChatGPT',
|
||||
'ai-claude' => 'Claude',
|
||||
'ai-deepseek' => 'DeepSeek',
|
||||
'ai-wenxin' => '文心一言',
|
||||
'ai-qianwen' => '通义千问',
|
||||
'ai-gemini' => 'Gemini',
|
||||
'ai-grok' => 'Grok',
|
||||
'ai-ollama' => 'Ollama',
|
||||
'ai-zhipu' => '智谱清言',
|
||||
'ai-qianwen' => '通义千问',
|
||||
'ai-wenxin' => '文心一言',
|
||||
'bot-manager' => '机器人管理',
|
||||
'meeting-alert' => '会议通知',
|
||||
'okr-alert' => 'OKR提醒',
|
||||
@@ -105,39 +179,33 @@ class UserBot extends AbstractModel
|
||||
{
|
||||
switch ($email) {
|
||||
case 'check-in@bot.system':
|
||||
$menu = [
|
||||
/*[
|
||||
'key' => 'it',
|
||||
'label' => Doo::translate('IT资讯')
|
||||
], [
|
||||
'key' => '36ke',
|
||||
'label' => Doo::translate('36氪')
|
||||
], [
|
||||
'key' => '60s',
|
||||
'label' => Doo::translate('60s读世界')
|
||||
], [
|
||||
'key' => 'joke',
|
||||
'label' => Doo::translate('开心笑话')
|
||||
], [
|
||||
'key' => 'soup',
|
||||
'label' => Doo::translate('心灵鸡汤')
|
||||
]*/
|
||||
];
|
||||
$menu = [];
|
||||
$setting = Base::setting('checkinSetting');
|
||||
if ($setting['open'] !== 'open') {
|
||||
return $menu;
|
||||
}
|
||||
if (in_array('locat', $setting['modes']) && Base::isEEUIApp()) {
|
||||
$menu[] = [
|
||||
'key' => 'locat-checkin',
|
||||
'label' => Doo::translate('定位签到'),
|
||||
'config' => [
|
||||
'key' => $setting['locat_bd_lbs_key'],
|
||||
'lng' => $setting['locat_bd_lbs_point']['lng'],
|
||||
'lat' => $setting['locat_bd_lbs_point']['lat'],
|
||||
'radius' => $setting['locat_bd_lbs_point']['radius'],
|
||||
]
|
||||
$mapTypes = [
|
||||
'baidu' => ['key' => 'locat_bd_lbs_key', 'point' => 'locat_bd_lbs_point', 'msg' => '请填写百度地图AK'],
|
||||
'amap' => ['key' => 'locat_amap_key', 'point' => 'locat_amap_point', 'msg' => '请填写高德地图Key'],
|
||||
'tencent' => ['key' => 'locat_tencent_key', 'point' => 'locat_tencent_point', 'msg' => '请填写腾讯地图Key'],
|
||||
];
|
||||
$type = $setting['locat_map_type'];
|
||||
if (isset($mapTypes[$type])) {
|
||||
$conf = $mapTypes[$type];
|
||||
$point = $setting[$conf['point']];
|
||||
$menu[] = [
|
||||
'key' => 'locat-checkin',
|
||||
'label' => Doo::translate('定位签到'),
|
||||
'config' => [
|
||||
'type' => $type,
|
||||
'key' => $setting[$conf['key']],
|
||||
'lng' => $point['lng'],
|
||||
'lat' => $point['lat'],
|
||||
'radius' => intval($point['radius']),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
if (in_array('manual', $setting['modes'])) {
|
||||
$menu[] = [
|
||||
@@ -188,34 +256,38 @@ class UserBot extends AbstractModel
|
||||
];
|
||||
|
||||
default:
|
||||
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');
|
||||
return [
|
||||
[
|
||||
'key' => '~ai-model-select',
|
||||
'label' => Doo::translate('选择模型'),
|
||||
'config' => [
|
||||
'model' => $aibotSetting[$match[1] . '_model']
|
||||
]
|
||||
],
|
||||
if (preg_match('/^(ai-|user-session-)(.*?)@bot\.system$/', $email, $match)) {
|
||||
$menus = [
|
||||
[
|
||||
'key' => '~ai-session-create',
|
||||
'label' => Doo::translate('开启新会话'),
|
||||
'config' => [
|
||||
'model' => $aibotSetting[$match[1] . '_model']
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => '~ai-session-history',
|
||||
'label' => Doo::translate('历史会话'),
|
||||
]
|
||||
];
|
||||
if ($match[1] === "ai-") {
|
||||
$aibotSetting = Base::setting('aibotSetting');
|
||||
$aibotModel = $aibotSetting[$match[2] . '_model'];
|
||||
$aibotModels = Setting::AIBotModels2Array($aibotSetting[$match[2] . '_models']);
|
||||
if ($aibotModels) {
|
||||
$menus = array_merge(
|
||||
[
|
||||
[
|
||||
'key' => '~ai-model-select',
|
||||
'label' => Doo::translate('选择模型'),
|
||||
'config' => [
|
||||
'model' => $aibotModel,
|
||||
'models' => $aibotModels
|
||||
]
|
||||
]
|
||||
],
|
||||
$menus
|
||||
);
|
||||
}
|
||||
}
|
||||
return $menus;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -244,7 +316,6 @@ class UserBot extends AbstractModel
|
||||
return '暂未开放手动签到。';
|
||||
}
|
||||
UserBot::checkinBotCheckin('manual-' . $userid, Timer::time(), true);
|
||||
return null;
|
||||
} elseif ($command === 'locat-checkin') {
|
||||
$setting = Base::setting('checkinSetting');
|
||||
if ($setting['open'] !== 'open') {
|
||||
@@ -256,16 +327,14 @@ class UserBot extends AbstractModel
|
||||
if (empty($extra)) {
|
||||
return '当前客户端版本低(所需版本≥v0.39.75)。';
|
||||
}
|
||||
if ($extra['type'] === 'bd') {
|
||||
if (in_array($extra['type'], ['baidu', 'amap', 'tencent'])) {
|
||||
// todo 判断距离
|
||||
} else {
|
||||
return '错误的定位签到。';
|
||||
}
|
||||
UserBot::checkinBotCheckin('locat-' . $userid, Timer::time(), true);
|
||||
return null;
|
||||
} else {
|
||||
return Extranet::checkinBotQuickMsg($command);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -447,4 +516,87 @@ class UserBot extends AbstractModel
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建我的机器人
|
||||
* @param int $userid 创建人userid
|
||||
* @param string $botName 机器人名称
|
||||
* @param bool $sessionSupported 是否支持会话
|
||||
* @return array
|
||||
*/
|
||||
public static function newBot($userid, $botName, $sessionSupported = false)
|
||||
{
|
||||
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个字符组成。");
|
||||
}
|
||||
$botType = ($sessionSupported ? "user-session-" : "user-normal-") . Base::generatePassword();
|
||||
$data = User::botGetOrCreate($botType, [
|
||||
'nickname' => $botName
|
||||
], $userid);
|
||||
if (empty($data)) {
|
||||
return Base::retError("创建失败。");
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($data, $userid);
|
||||
if ($dialog) {
|
||||
if ($sessionSupported) {
|
||||
$dialogSession = WebSocketDialogSession::create([
|
||||
'dialog_id' => $dialog->id,
|
||||
]);
|
||||
$dialogSession->save();
|
||||
$dialog->session_id = $dialogSession->id;
|
||||
$dialog->save();
|
||||
}
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => '/hello',
|
||||
'title' => '创建成功。',
|
||||
'data' => $data,
|
||||
], $data->userid);
|
||||
}
|
||||
return Base::retSuccess("创建成功。", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可选的 webhook 事件
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function webhookEventOptions(): array
|
||||
{
|
||||
return [
|
||||
self::WEBHOOK_EVENT_MESSAGE,
|
||||
self::WEBHOOK_EVENT_DIALOG_OPEN,
|
||||
self::WEBHOOK_EVENT_MEMBER_JOIN,
|
||||
self::WEBHOOK_EVENT_MEMBER_LEAVE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化 webhook 事件配置
|
||||
*
|
||||
* @param mixed $events
|
||||
* @param bool $useFallback
|
||||
* @return array
|
||||
*/
|
||||
public static function normalizeWebhookEvents(mixed $events, bool $useFallback = true): array
|
||||
{
|
||||
if (is_string($events)) {
|
||||
$events = Base::json2array($events);
|
||||
}
|
||||
if ($events === null) {
|
||||
$events = [];
|
||||
}
|
||||
if (!is_array($events)) {
|
||||
$events = [$events];
|
||||
}
|
||||
$events = array_filter(array_map('strval', $events));
|
||||
$events = array_values(array_intersect($events, self::webhookEventOptions()));
|
||||
return $events ?: ($useFallback ? [self::WEBHOOK_EVENT_MESSAGE] : []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Apps;
|
||||
use App\Module\Base;
|
||||
use App\Module\Ihttp;
|
||||
|
||||
@@ -37,8 +38,9 @@ use App\Module\Ihttp;
|
||||
class UserCheckinFace extends AbstractModel
|
||||
{
|
||||
|
||||
public static function saveFace($userid, $nickname, $faceimg, $remark='')
|
||||
public static function saveFace($userid, $nickname, $faceimg, $remark = '')
|
||||
{
|
||||
Apps::isInstalledThrow('face');
|
||||
// 取上传图片的URL
|
||||
$faceimg = Base::unFillUrl($faceimg);
|
||||
$record = "";
|
||||
@@ -47,7 +49,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,
|
||||
@@ -59,14 +61,14 @@ class UserCheckinFace extends AbstractModel
|
||||
}
|
||||
|
||||
$res = Ihttp::ihttp_post($url, json_encode($data), 15);
|
||||
if($res['data'] && $data = json_decode($res['data'])){
|
||||
if($data->ret != 1 && $data->msg){
|
||||
if ($res['data'] && $data = json_decode($res['data'])) {
|
||||
if ($data->ret != 1 && $data->msg) {
|
||||
throw new ApiException($data->msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return AbstractModel::transaction(function() use ($userid, $faceimg, $remark) {
|
||||
return AbstractModel::transaction(function () use ($userid, $faceimg, $remark) {
|
||||
$checkinFace = self::query()->whereUserid($userid)->first();
|
||||
if ($checkinFace) {
|
||||
self::updateData(['id' => $checkinFace->id], [
|
||||
@@ -82,27 +84,24 @@ class UserCheckinFace extends AbstractModel
|
||||
$checkinFace->save();
|
||||
}
|
||||
if ($faceimg == '') {
|
||||
$res = UserCheckinFace::deleteDeviceUser($userid);
|
||||
if ($res) {
|
||||
return $res;
|
||||
}
|
||||
UserCheckinFace::deleteDeviceUser($userid);
|
||||
}
|
||||
return Base::retSuccess('设置成功');
|
||||
});
|
||||
}
|
||||
|
||||
public static function deleteDeviceUser($userid) {
|
||||
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user/delete";
|
||||
private static function deleteDeviceUser($userid)
|
||||
{
|
||||
$url = "http://face:7788/user/delete";
|
||||
$data = [
|
||||
'enrollid' => $userid,
|
||||
'backupnum' => 50, // 13 删除整个用户 50 删除图片
|
||||
];
|
||||
|
||||
$res = Ihttp::ihttp_post($url, json_encode($data));
|
||||
if($res['data'] && $data = json_decode($res['data'])){
|
||||
if($data->ret != 1 && $data->msg){
|
||||
if ($res['data'] && $data = json_decode($res['data'])) {
|
||||
if ($data->ret != 1 && $data->msg) {
|
||||
throw new ApiException($data->msg);
|
||||
// return Base::retError($data->msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Module\Base;
|
||||
* @property int|null $userid 用户id
|
||||
* @property string|null $email 邮箱帐号
|
||||
* @property string|null $reason 注销原因
|
||||
* @property string $cache 会员资料缓存
|
||||
* @property string|null $cache 会员资料缓存
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
@@ -37,11 +37,6 @@ use App\Module\Base;
|
||||
*/
|
||||
class UserDelete extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 昵称
|
||||
* @param $value
|
||||
* @return string
|
||||
*/
|
||||
public function getCacheAttribute($value)
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
@@ -65,13 +60,25 @@ class UserDelete extends AbstractModel
|
||||
*/
|
||||
public static function userid2basic($userid)
|
||||
{
|
||||
$row = self::whereUserid($userid)->first();
|
||||
if (empty($row) || empty($row->cache)) {
|
||||
return null;
|
||||
}
|
||||
$cache = $row->cache;
|
||||
$cache = array_intersect_key($cache, array_flip(array_merge(User::$basicField, ['department_name'])));
|
||||
$cache['delete_at'] = $row->created_at->toDateTimeString();
|
||||
return $cache;
|
||||
return \Cache::remember("UserDelete:{$userid}", now()->addDays(3), function () use ($userid) {
|
||||
$row = self::whereUserid($userid)->first();
|
||||
if (empty($row) || empty($row->cache)) {
|
||||
return null;
|
||||
}
|
||||
$cache = $row->cache;
|
||||
$cache = array_intersect_key($cache, array_flip(array_merge(User::$basicField, ['department_name'])));
|
||||
$cache['delete_at'] = $row->created_at->toDateTimeString();
|
||||
return $cache;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* userid 获取 昵称
|
||||
* @param $userid
|
||||
* @return string
|
||||
*/
|
||||
public static function userid2nickname($userid)
|
||||
{
|
||||
return self::userid2basic($userid)['nickname'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use Cache;
|
||||
|
||||
/**
|
||||
* App\Models\UserDepartment
|
||||
@@ -34,6 +35,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 +147,7 @@ class UserDepartment extends AbstractModel
|
||||
});
|
||||
// 解散群组
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
if ($dialog) {
|
||||
$dialog->deleteDialog();
|
||||
}
|
||||
$dialog?->deleteDialog();
|
||||
//
|
||||
$this->delete();
|
||||
}
|
||||
@@ -155,4 +169,67 @@ class UserDepartment extends AbstractModel
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取所有子部门ID
|
||||
* @param int $departmentId
|
||||
* @return array
|
||||
*/
|
||||
public static function getAllSubDepartmentIds($departmentId)
|
||||
{
|
||||
$subIds = [];
|
||||
$directSubs = self::whereParentId($departmentId)->pluck('id')->toArray();
|
||||
|
||||
foreach ($directSubs as $subId) {
|
||||
$subIds[] = $subId;
|
||||
// 递归获取子部门的子部门
|
||||
$subSubIds = self::getAllSubDepartmentIds($subId);
|
||||
$subIds = array_merge($subIds, $subSubIds);
|
||||
}
|
||||
|
||||
return array_unique($subIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门基本信息(缓存时间1小时)
|
||||
* @param int|array $ids
|
||||
* @return \Illuminate\Support\Collection|static|null
|
||||
*/
|
||||
public static function getDepartmentsByIds($ids)
|
||||
{
|
||||
$ids = is_array($ids) ? $ids : [$ids];
|
||||
$departments = collect();
|
||||
$uncachedIds = [];
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$cacheKey = "department_info_{$id}";
|
||||
$department = Cache::get($cacheKey);
|
||||
if ($department) {
|
||||
$departments->push($department);
|
||||
} else {
|
||||
$uncachedIds[] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($uncachedIds)) {
|
||||
$dbDepartments = self::select(['id', 'name', 'parent_id', 'owner_userid'])->whereIn('id', $uncachedIds)->get();
|
||||
foreach ($dbDepartments as $department) {
|
||||
$cacheKey = "department_info_{$department->id}";
|
||||
Cache::put($cacheKey, $department, 60 * 60); // 1小时
|
||||
$departments->push($department);
|
||||
}
|
||||
}
|
||||
|
||||
// 保持返回顺序与传入ids一致
|
||||
$departments = $departments->keyBy('id');
|
||||
$result = collect();
|
||||
foreach ($ids as $id) {
|
||||
if ($departments->has($id)) {
|
||||
$result->push($departments->get($id));
|
||||
}
|
||||
}
|
||||
|
||||
return is_array($ids) ? $result : $result->first();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
312
app/Models/UserDevice.php
Normal file
312
app/Models/UserDevice.php
Normal file
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Lock;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use DeviceDetector\DeviceDetector;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
* App\Models\UserDevice
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 会员ID
|
||||
* @property string|null $hash TOKEN MD5
|
||||
* @property string|null $detail 详细信息
|
||||
* @property string|null $expired_at 过期时间
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property-read int $is_current
|
||||
* @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|UserDevice newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereDetail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereExpiredAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereHash($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice withTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserDevice extends AbstractModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'user_devices';
|
||||
|
||||
public static int $deviceLimit = 200; // 每个用户设备限制数量
|
||||
|
||||
protected $appends = [
|
||||
'is_current',
|
||||
];
|
||||
|
||||
public function getDetailAttribute($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
return Base::json2array($value);
|
||||
}
|
||||
|
||||
public function getIsCurrentAttribute(): int
|
||||
{
|
||||
return $this->hash === md5(Doo::userToken()) ? 1 : 0;
|
||||
}
|
||||
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
|
||||
/**
|
||||
* 缓存key
|
||||
* @param string $hash
|
||||
* @return string
|
||||
*/
|
||||
public static function ck(string $hash): string
|
||||
{
|
||||
return "user_devices:{$hash}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 UA 获取设备信息
|
||||
* @param string $ua
|
||||
* @return array
|
||||
*/
|
||||
private static function getDeviceInfo(string $ua): array
|
||||
{
|
||||
$result = [
|
||||
'ip' => Base::getIp(),
|
||||
'type' => '电脑',
|
||||
'os' => 'Unknown',
|
||||
'browser' => 'Unknown',
|
||||
'version' => '',
|
||||
|
||||
'app_name' => '', // 客户端名称
|
||||
'app_type' => '', // 客户端类型
|
||||
'app_version' => '', // 客户端版本
|
||||
];
|
||||
|
||||
if (empty($ua)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 使用 Device-Detector 解析 UA
|
||||
$dd = new DeviceDetector($ua);
|
||||
|
||||
// 解析 UA 字符串
|
||||
$dd->parse();
|
||||
|
||||
// 获取客户端信息(浏览器)
|
||||
$clientInfo = $dd->getClient();
|
||||
if (!empty($clientInfo)) {
|
||||
$result['browser'] = $clientInfo['name'] ?? 'Unknown';
|
||||
$result['version'] = $clientInfo['version'] ?? '';
|
||||
}
|
||||
|
||||
// 获取操作系统信息
|
||||
$osInfo = $dd->getOs();
|
||||
if (!empty($osInfo)) {
|
||||
$result['os'] = trim(($osInfo['name'] ?? '') . ' ' . ($osInfo['version'] ?? ''));
|
||||
if (empty($result['os'])) {
|
||||
$result['os'] = 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match("/android_kuaifan_eeui/i", $ua)) {
|
||||
// Android 客户端
|
||||
$result['app_type'] = 'Android';
|
||||
if ($dd->getBrandName() && $dd->getModel()) {
|
||||
// 厂商+型号
|
||||
$result['app_name'] = $dd->getBrandName() . ' ' . $dd->getModel();
|
||||
} elseif ($dd->getBrandName()) {
|
||||
// 仅厂商
|
||||
$result['app_name'] = $dd->getBrandName();
|
||||
} elseif ($dd->isTablet()) {
|
||||
// 平板
|
||||
$result['app_name'] = 'Tablet';
|
||||
} elseif ($dd->isPhablet()) {
|
||||
// 平板
|
||||
$result['app_name'] = 'Phablet';
|
||||
}
|
||||
$result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/');
|
||||
} elseif (preg_match("/ios_kuaifan_eeui/i", $ua)) {
|
||||
// iOS 客户端
|
||||
$result['app_type'] = 'iOS';
|
||||
if (preg_match("/(macintosh|ipad)/i", $ua)) {
|
||||
// iPad
|
||||
$result['app_name'] = 'iPad';
|
||||
} elseif (preg_match("/iphone/i", $ua)) {
|
||||
// iPhone
|
||||
$result['app_name'] = 'iPhone';
|
||||
}
|
||||
$result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/');
|
||||
} elseif (preg_match("/dootask/i", $ua)) {
|
||||
// DooTask 客户端
|
||||
$result['app_type'] = $osInfo['name'];
|
||||
$result['app_version'] = self::getAfterVersion($ua, 'dootask/');
|
||||
} else {
|
||||
// 其他客户端
|
||||
$result['app_type'] = 'Web';
|
||||
$result['app_version'] = Base::getClientVersion();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 ua 的 find 之后的内容获取版本号
|
||||
* @param string $ua
|
||||
* @param string $find
|
||||
* @return string
|
||||
*/
|
||||
private static function getAfterVersion(string $ua, string $find): string
|
||||
{
|
||||
$findPattern = preg_quote($find, '/');
|
||||
if (preg_match("/{$findPattern}(.*?)(?:\s|$)/i", $ua, $matches)) {
|
||||
$appInfo = $matches[1];
|
||||
|
||||
// 从内容中提取版本号(寻找符合x.x.x格式的部分)
|
||||
if (preg_match("/(\d+\.\d+(?:\.\d+)*)/", $appInfo, $versionMatches)) {
|
||||
return $versionMatches[1];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
|
||||
/**
|
||||
* 检查用户是否存在
|
||||
* @return string|null
|
||||
*/
|
||||
public static function check(): ?string
|
||||
{
|
||||
$token = Doo::userToken();
|
||||
$userid = Doo::userId();
|
||||
|
||||
$hash = md5($token);
|
||||
if (Cache::has(self::ck($hash))) {
|
||||
return $hash;
|
||||
}
|
||||
|
||||
$row = self::whereHash($hash)->first();
|
||||
if ($row) {
|
||||
// 判断是否过期
|
||||
if ($row->expired_at && Carbon::parse($row->expired_at)->isPast()) {
|
||||
self::forget($row);
|
||||
return null;
|
||||
}
|
||||
// 更新缓存
|
||||
self::record();
|
||||
return $hash;
|
||||
}
|
||||
// 没有记录
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录设备(添加、更新)
|
||||
* @param string|null $token
|
||||
* @return self|null
|
||||
*/
|
||||
public static function record(string $token = null): ?self
|
||||
{
|
||||
if (empty($token)) {
|
||||
$token = Doo::userToken();
|
||||
$userid = Doo::userId();
|
||||
$expiredAt = Doo::userExpiredAt();
|
||||
} else {
|
||||
$info = Doo::tokenDecode($token);
|
||||
$userid = $info['userid'] ?? 0;
|
||||
$expiredAt = $info['expired_at'];
|
||||
}
|
||||
$hash = md5($token);
|
||||
//
|
||||
return Lock::withLock("userDeviceRecord:{$hash}", function () use ($expiredAt, $userid, $hash, $token) {
|
||||
return AbstractModel::transaction(function () use ($expiredAt, $userid, $hash, $token) {
|
||||
$row = self::whereHash($hash)->first();
|
||||
if (empty($row)) {
|
||||
// 生成一个新的设备记录
|
||||
$row = self::createInstance([
|
||||
'userid' => $userid,
|
||||
'hash' => $hash,
|
||||
]);
|
||||
if (!$row->save()) {
|
||||
return null;
|
||||
}
|
||||
// 删除多余的设备记录
|
||||
$currentDeviceCount = self::whereUserid($userid)->count();
|
||||
if ($currentDeviceCount > self::$deviceLimit) {
|
||||
$rows = self::whereUserid($userid)->orderBy('id')->take($currentDeviceCount - self::$deviceLimit)->get();
|
||||
foreach ($rows as $row) {
|
||||
UserDevice::forget($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
$row->expired_at = $expiredAt;
|
||||
if (Request::hasHeader('version')) {
|
||||
$deviceInfo = array_merge(Base::json2array($row->detail), self::getDeviceInfo($_SERVER['HTTP_USER_AGENT'] ?? ''));
|
||||
$row->detail = Base::array2json($deviceInfo);
|
||||
}
|
||||
$row->save();
|
||||
|
||||
Cache::put(self::ck($hash), $row->userid, now()->addHour());
|
||||
return $row;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记设备(删除)
|
||||
* @param UserDevice|string|int|null $token
|
||||
* - UserDevice 表示指定的设备对象
|
||||
* - string 表示指定的 token
|
||||
* - int 表示指定的数据ID
|
||||
* - null 表示当前登录的设备
|
||||
* @return void
|
||||
*/
|
||||
public static function forget(UserDevice|string|int $token = null): void
|
||||
{
|
||||
if ($token instanceof UserDevice) {
|
||||
$hash = $token->hash;
|
||||
$token->delete();
|
||||
} elseif (Base::isNumber($token)) {
|
||||
$row = self::find(intval($token));
|
||||
if ($row) {
|
||||
$hash = $row->hash;
|
||||
$row->delete();
|
||||
}
|
||||
} else {
|
||||
if ($token === null) {
|
||||
$token = Doo::userToken();
|
||||
}
|
||||
if ($token) {
|
||||
$hash = md5($token);
|
||||
self::whereHash($hash)->delete();
|
||||
}
|
||||
}
|
||||
if (isset($hash)) {
|
||||
Cache::forget(self::ck($hash));
|
||||
UmengAlias::whereDeviceHash($hash)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
340
app/Models/UserFavorite.php
Normal file
340
app/Models/UserFavorite.php
Normal file
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\File;
|
||||
|
||||
/**
|
||||
* App\Models\UserFavorite
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 用户ID
|
||||
* @property string|null $favoritable_type 收藏类型(比如:task/project/file/message)
|
||||
* @property int|null $favoritable_id 收藏对象ID
|
||||
* @property string $remark 收藏备注
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Model|\Eloquent $favoritable
|
||||
* @property-read \App\Models\User|null $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|UserFavorite newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereFavoritableId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereFavoritableType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereRemark($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserFavorite extends AbstractModel
|
||||
{
|
||||
const TYPE_TASK = 'task';
|
||||
const TYPE_PROJECT = 'project';
|
||||
const TYPE_FILE = 'file';
|
||||
const TYPE_MESSAGE = 'message';
|
||||
|
||||
protected $fillable = [
|
||||
'userid',
|
||||
'favoritable_type',
|
||||
'favoritable_id',
|
||||
'remark',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联用户
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid', 'userid');
|
||||
}
|
||||
|
||||
/**
|
||||
* 多态关联
|
||||
*/
|
||||
public function favoritable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换收藏状态
|
||||
* @param int $userid 用户ID
|
||||
* @param string $type 收藏类型
|
||||
* @param int $id 收藏对象ID
|
||||
* @return array ['favorited' => bool, 'action' => 'added'|'removed']
|
||||
*/
|
||||
public static function toggleFavorite($userid, $type, $id)
|
||||
{
|
||||
$favorite = self::whereUserid($userid)
|
||||
->whereFavoritableType($type)
|
||||
->whereFavoritableId($id)
|
||||
->first();
|
||||
|
||||
if ($favorite) {
|
||||
// 取消收藏
|
||||
$favorite->delete();
|
||||
return ['favorited' => false, 'action' => 'removed', 'remark' => ''];
|
||||
}
|
||||
|
||||
// 添加收藏
|
||||
$favorite = self::create([
|
||||
'userid' => $userid,
|
||||
'favoritable_type' => $type,
|
||||
'favoritable_id' => $id,
|
||||
]);
|
||||
|
||||
return ['favorited' => true, 'action' => 'added', 'remark' => $favorite->remark ?? ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新收藏备注
|
||||
* @param int $userid
|
||||
* @param string $type
|
||||
* @param int $id
|
||||
* @param string $remark
|
||||
* @return static|null
|
||||
*/
|
||||
public static function updateRemark($userid, $type, $id, $remark)
|
||||
{
|
||||
$favorite = self::whereUserid($userid)
|
||||
->whereFavoritableType($type)
|
||||
->whereFavoritableId($id)
|
||||
->first();
|
||||
|
||||
if (!$favorite) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$favorite->remark = $remark;
|
||||
$favorite->save();
|
||||
|
||||
return $favorite;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已收藏
|
||||
* @param int $userid 用户ID
|
||||
* @param string $type 收藏类型
|
||||
* @param int $id 收藏对象ID
|
||||
* @return bool
|
||||
*/
|
||||
public static function isFavorited($userid, $type, $id)
|
||||
{
|
||||
return self::whereUserid($userid)
|
||||
->whereFavoritableType($type)
|
||||
->whereFavoritableId($id)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户收藏列表
|
||||
* @param int $userid 用户ID
|
||||
* @param string|null $type 收藏类型过滤
|
||||
* @param int $page 页码
|
||||
* @param int $pageSize 每页数量
|
||||
* @return array
|
||||
*/
|
||||
public static function getUserFavorites($userid, $type = null, $page = 1, $pageSize = 20)
|
||||
{
|
||||
$query = self::whereUserid($userid)->orderByDesc('created_at');
|
||||
|
||||
if ($type) {
|
||||
$query->whereFavoritableType($type);
|
||||
}
|
||||
|
||||
$favorites = $query->paginate($pageSize, ['*'], 'page', $page);
|
||||
|
||||
$data = [
|
||||
'tasks' => [],
|
||||
'projects' => [],
|
||||
'files' => [],
|
||||
'messages' => []
|
||||
];
|
||||
|
||||
// 分组收集ID
|
||||
$taskIds = [];
|
||||
$projectIds = [];
|
||||
$fileIds = [];
|
||||
$messageIds = [];
|
||||
|
||||
foreach ($favorites->items() as $favorite) {
|
||||
switch ($favorite->favoritable_type) {
|
||||
case self::TYPE_TASK:
|
||||
$taskIds[] = $favorite->favoritable_id;
|
||||
break;
|
||||
case self::TYPE_PROJECT:
|
||||
$projectIds[] = $favorite->favoritable_id;
|
||||
break;
|
||||
case self::TYPE_FILE:
|
||||
$fileIds[] = $favorite->favoritable_id;
|
||||
break;
|
||||
case self::TYPE_MESSAGE:
|
||||
$messageIds[] = $favorite->favoritable_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询具体数据
|
||||
if (!empty($taskIds)) {
|
||||
$tasks = ProjectTask::select([
|
||||
'project_tasks.id',
|
||||
'project_tasks.name',
|
||||
'project_tasks.project_id',
|
||||
'project_tasks.complete_at',
|
||||
'project_tasks.created_at',
|
||||
'project_tasks.flow_item_id',
|
||||
'project_tasks.flow_item_name',
|
||||
'projects.name as project_name'
|
||||
])
|
||||
->leftJoin('projects', 'project_tasks.project_id', '=', 'projects.id')
|
||||
->whereIn('project_tasks.id', $taskIds)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
foreach ($favorites->items() as $favorite) {
|
||||
if ($favorite->favoritable_type === self::TYPE_TASK && isset($tasks[$favorite->favoritable_id])) {
|
||||
$task = $tasks[$favorite->favoritable_id];
|
||||
|
||||
// 解析 flow_item_name 字段(格式:status|name|color)
|
||||
$flowItemParts = explode('|', $task->flow_item_name ?: '');
|
||||
$flowItemStatus = $flowItemParts[0] ?? '';
|
||||
$flowItemName = $flowItemParts[1] ?? $task->flow_item_name;
|
||||
$flowItemColor = $flowItemParts[2] ?? '';
|
||||
|
||||
$data['tasks'][] = [
|
||||
'id' => $task->id,
|
||||
'name' => $task->name,
|
||||
'project_id' => $task->project_id,
|
||||
'project_name' => $task->project_name,
|
||||
'complete_at' => $task->complete_at,
|
||||
'flow_item_id' => $task->flow_item_id,
|
||||
'flow_item_name' => $flowItemName,
|
||||
'flow_item_status' => $flowItemStatus,
|
||||
'flow_item_color' => $flowItemColor,
|
||||
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
|
||||
'remark' => $favorite->remark,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($projectIds)) {
|
||||
$projects = Project::select([
|
||||
'id', 'name', 'desc', 'archived_at', 'created_at'
|
||||
])->whereIn('id', $projectIds)->get()->keyBy('id');
|
||||
|
||||
foreach ($favorites->items() as $favorite) {
|
||||
if ($favorite->favoritable_type === self::TYPE_PROJECT && isset($projects[$favorite->favoritable_id])) {
|
||||
$project = $projects[$favorite->favoritable_id];
|
||||
$data['projects'][] = [
|
||||
'id' => $project->id,
|
||||
'name' => $project->name,
|
||||
'desc' => $project->desc,
|
||||
'archived_at' => $project->archived_at,
|
||||
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
|
||||
'remark' => $favorite->remark,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fileIds)) {
|
||||
$files = File::select([
|
||||
'id', 'name', 'ext', 'size', 'pid', 'created_at'
|
||||
])->whereIn('id', $fileIds)->get()->keyBy('id');
|
||||
|
||||
foreach ($favorites->items() as $favorite) {
|
||||
if ($favorite->favoritable_type === self::TYPE_FILE && isset($files[$favorite->favoritable_id])) {
|
||||
$file = $files[$favorite->favoritable_id];
|
||||
$fileData = File::handleImageUrl(array_merge(
|
||||
$file->only(['id', 'ext']),
|
||||
[
|
||||
'name' => $file->name,
|
||||
'size' => $file->size,
|
||||
'pid' => $file->pid,
|
||||
]
|
||||
));
|
||||
$data['files'][] = [
|
||||
'id' => $file->id,
|
||||
'name' => $file->name,
|
||||
'ext' => $file->ext,
|
||||
'size' => $file->size,
|
||||
'pid' => $file->pid,
|
||||
'image_url' => $fileData['image_url'] ?? null,
|
||||
'image_width' => $fileData['image_width'] ?? null,
|
||||
'image_height' => $fileData['image_height'] ?? null,
|
||||
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
|
||||
'remark' => $favorite->remark,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($messageIds)) {
|
||||
$messages = WebSocketDialogMsg::select([
|
||||
'id', 'dialog_id', 'userid', 'type', 'msg', 'created_at'
|
||||
])->whereIn('id', $messageIds)->get()->keyBy('id');
|
||||
|
||||
foreach ($favorites->items() as $favorite) {
|
||||
if ($favorite->favoritable_type === self::TYPE_MESSAGE && isset($messages[$favorite->favoritable_id])) {
|
||||
$message = $messages[$favorite->favoritable_id];
|
||||
|
||||
// 使用 previewMsg 获取消息预览文本
|
||||
$previewText = '';
|
||||
if ($message->msg && is_array($message->msg)) {
|
||||
$previewText = WebSocketDialogMsg::previewMsg($message);
|
||||
}
|
||||
|
||||
// 如果没有预览文本,使用消息类型作为标题
|
||||
if (empty($previewText)) {
|
||||
$previewText = '[' . ucfirst($message->type) . ']';
|
||||
}
|
||||
|
||||
$data['messages'][] = [
|
||||
'id' => $message->id,
|
||||
'name' => $previewText,
|
||||
'dialog_id' => $message->dialog_id,
|
||||
'userid' => $message->userid,
|
||||
'type' => $message->type,
|
||||
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
|
||||
'remark' => $favorite->remark,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'total' => $favorites->total(),
|
||||
'current_page' => $favorites->currentPage(),
|
||||
'per_page' => $favorites->perPage(),
|
||||
'last_page' => $favorites->lastPage(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理用户收藏
|
||||
* @param int $userid 用户ID
|
||||
* @param string|null $type 收藏类型,null表示全部类型
|
||||
* @return int 删除的记录数
|
||||
*/
|
||||
public static function cleanUserFavorites($userid, $type = null)
|
||||
{
|
||||
$query = self::whereUserid($userid);
|
||||
|
||||
if ($type) {
|
||||
$query->whereFavoritableType($type);
|
||||
}
|
||||
|
||||
return $query->delete();
|
||||
}
|
||||
}
|
||||
79
app/Models/UserRecentItem.php
Normal file
79
app/Models/UserRecentItem.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\UserRecentItem
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $userid 用户ID
|
||||
* @property string $target_type 目标类型(task/file/task_file/message_file 等)
|
||||
* @property int $target_id 目标ID
|
||||
* @property string $source_type 来源类型(project/filesystem/project_task/dialog 等)
|
||||
* @property int $source_id 来源ID
|
||||
* @property \Illuminate\Support\Carbon|null $browsed_at 浏览时间
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @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|UserRecentItem newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereBrowsedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereSourceId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereSourceType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereTargetId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereTargetType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserRecentItem extends AbstractModel
|
||||
{
|
||||
public const TYPE_TASK = 'task';
|
||||
public const TYPE_FILE = 'file';
|
||||
public const TYPE_TASK_FILE = 'task_file';
|
||||
public const TYPE_MESSAGE_FILE = 'message_file';
|
||||
|
||||
public const SOURCE_PROJECT = 'project';
|
||||
public const SOURCE_FILESYSTEM = 'filesystem';
|
||||
public const SOURCE_PROJECT_TASK = 'project_task';
|
||||
public const SOURCE_DIALOG = 'dialog';
|
||||
|
||||
protected $fillable = [
|
||||
'userid',
|
||||
'target_type',
|
||||
'target_id',
|
||||
'source_type',
|
||||
'source_id',
|
||||
'browsed_at',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'browsed_at',
|
||||
];
|
||||
|
||||
public static function record(int $userid, string $targetType, int $targetId, string $sourceType = '', int $sourceId = 0): self
|
||||
{
|
||||
return self::updateOrCreate(
|
||||
[
|
||||
'userid' => $userid,
|
||||
'target_type' => $targetType,
|
||||
'target_id' => $targetId,
|
||||
'source_type' => $sourceType,
|
||||
'source_id' => $sourceId,
|
||||
],
|
||||
[
|
||||
'browsed_at' => Carbon::now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
84
app/Models/UserTag.php
Normal file
84
app/Models/UserTag.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class UserTag extends AbstractModel
|
||||
{
|
||||
protected $table = 'user_tags';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'name',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by', 'userid')
|
||||
->select(['userid', 'nickname']);
|
||||
}
|
||||
|
||||
public function recognitions(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserTagRecognition::class, 'tag_id');
|
||||
}
|
||||
|
||||
public function canManage(User $viewer): bool
|
||||
{
|
||||
return $viewer->isAdmin()
|
||||
|| $viewer->userid === $this->user_id
|
||||
|| $viewer->userid === $this->created_by;
|
||||
}
|
||||
|
||||
public static function listWithMeta(int $targetUserId, ?User $viewer): array
|
||||
{
|
||||
$query = static::query()
|
||||
->where('user_id', $targetUserId)
|
||||
->with(['creator'])
|
||||
->withCount(['recognitions as recognition_total'])
|
||||
->orderByDesc('recognition_total')
|
||||
->orderBy('id');
|
||||
|
||||
$tags = $query->get();
|
||||
|
||||
$viewerId = $viewer?->userid ?? 0;
|
||||
$viewerIsAdmin = $viewer?->isAdmin() ?? false;
|
||||
$viewerIsOwner = $viewerId > 0 && $viewerId === $targetUserId;
|
||||
|
||||
$recognizedIds = [];
|
||||
if ($viewerId > 0 && $tags->isNotEmpty()) {
|
||||
$recognizedIds = UserTagRecognition::query()
|
||||
->where('user_id', $viewerId)
|
||||
->whereIn('tag_id', $tags->pluck('id'))
|
||||
->pluck('tag_id')
|
||||
->all();
|
||||
}
|
||||
$recognizedLookup = array_flip($recognizedIds);
|
||||
|
||||
$list = $tags->map(function (self $tag) use ($viewerId, $viewerIsAdmin, $viewerIsOwner, $recognizedLookup) {
|
||||
$canManage = $viewerIsAdmin || $viewerIsOwner || $viewerId === $tag->created_by;
|
||||
|
||||
return [
|
||||
'id' => $tag->id,
|
||||
'user_id' => $tag->user_id,
|
||||
'name' => $tag->name,
|
||||
'created_by' => $tag->created_by,
|
||||
'created_by_name' => $tag->creator?->nickname ?: '',
|
||||
'recognition_total' => (int) $tag->recognition_total,
|
||||
'recognized' => isset($recognizedLookup[$tag->id]),
|
||||
'can_edit' => $canManage,
|
||||
'can_delete' => $canManage,
|
||||
];
|
||||
})->values()->toArray();
|
||||
|
||||
return [
|
||||
'list' => $list,
|
||||
'top' => array_slice($list, 0, 10),
|
||||
'total' => count($list),
|
||||
];
|
||||
}
|
||||
}
|
||||
26
app/Models/UserTagRecognition.php
Normal file
26
app/Models/UserTagRecognition.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserTagRecognition extends AbstractModel
|
||||
{
|
||||
protected $table = 'user_tag_recognitions';
|
||||
|
||||
protected $fillable = [
|
||||
'tag_id',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
public function tag(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserTag::class, 'tag_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id', 'userid')
|
||||
->select(['userid', 'nickname']);
|
||||
}
|
||||
}
|
||||
138
app/Models/UserTaskBrowse.php
Normal file
138
app/Models/UserTaskBrowse.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\UserTaskBrowse
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 用户ID
|
||||
* @property int|null $task_id 任务ID
|
||||
* @property \Illuminate\Support\Carbon|null $browsed_at 浏览时间
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\ProjectTask|null $task
|
||||
* @property-read \App\Models\User|null $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|UserTaskBrowse newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereBrowsedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserTaskBrowse extends AbstractModel
|
||||
{
|
||||
protected $fillable = [
|
||||
'userid',
|
||||
'task_id',
|
||||
'browsed_at',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'browsed_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联用户
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid', 'userid');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联任务
|
||||
*/
|
||||
public function task()
|
||||
{
|
||||
return $this->belongsTo(ProjectTask::class, 'task_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录用户浏览任务
|
||||
* @param int $userid 用户ID
|
||||
* @param int $task_id 任务ID
|
||||
* @return UserTaskBrowse
|
||||
*/
|
||||
public static function recordBrowse($userid, $task_id)
|
||||
{
|
||||
$record = self::updateOrCreate(
|
||||
[
|
||||
'userid' => $userid,
|
||||
'task_id' => $task_id,
|
||||
],
|
||||
[
|
||||
'browsed_at' => Carbon::now(),
|
||||
]
|
||||
);
|
||||
|
||||
UserRecentItem::record(
|
||||
$userid,
|
||||
UserRecentItem::TYPE_TASK,
|
||||
$task_id,
|
||||
UserRecentItem::SOURCE_PROJECT,
|
||||
0
|
||||
);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户浏览历史
|
||||
* @param int $userid 用户ID
|
||||
* @param int $limit 获取数量
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public static function getUserBrowseHistory($userid, $limit = 20)
|
||||
{
|
||||
return self::with(['task' => function ($query) {
|
||||
$query->select([
|
||||
'id', 'name', 'project_id', 'column_id', 'parent_id',
|
||||
'flow_item_id', 'flow_item_name',
|
||||
'complete_at', 'archived_at'
|
||||
]);
|
||||
}])
|
||||
->whereUserid($userid)
|
||||
->whereHas('task', function ($query) {
|
||||
// 只获取存在且未被删除的任务
|
||||
$query->whereNull('archived_at');
|
||||
})
|
||||
->orderByDesc('browsed_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理用户浏览历史
|
||||
* @param int $userid 用户ID
|
||||
* @param int $keepCount 保留数量,0表示全部删除
|
||||
* @return int 删除的记录数
|
||||
*/
|
||||
public static function cleanUserBrowseHistory($userid, $keepCount = 100)
|
||||
{
|
||||
if ($keepCount === 0) {
|
||||
return self::whereUserid($userid)->delete();
|
||||
}
|
||||
|
||||
$keepIds = self::whereUserid($userid)
|
||||
->orderByDesc('browsed_at')
|
||||
->limit($keepCount)
|
||||
->pluck('id');
|
||||
|
||||
return self::whereUserid($userid)
|
||||
->whereNotIn('id', $keepIds)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,32 @@ class WebSocketDialog extends AbstractModel
|
||||
->whereNull('users.disable_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索对话
|
||||
* @param $userid
|
||||
* @param $key
|
||||
* @param $take
|
||||
* @return array
|
||||
*/
|
||||
public static function searchDialog($userid, $key, $take = 20)
|
||||
{
|
||||
return 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'])
|
||||
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
|
||||
->where('u.userid', $userid)
|
||||
->where(function ($query) use ($key) {
|
||||
$query->where('d.name', 'like', '%' . $key . '%');
|
||||
})
|
||||
->whereNull('d.deleted_at')
|
||||
->orderByDesc('u.top_at')
|
||||
->orderByDesc('u.last_at')
|
||||
->take($take)
|
||||
->get()
|
||||
->map(function($item) use ($userid) {
|
||||
return WebSocketDialog::synthesizeData($item, $userid);
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话列表
|
||||
@@ -298,7 +324,8 @@ class WebSocketDialog extends AbstractModel
|
||||
$data['is_disable'] = $basic->isDisable(true);
|
||||
$data['quick_msgs'] = UserBot::quickMsgs($basic->email);
|
||||
} else {
|
||||
$data['name'] = 'non-existent';
|
||||
$data['name'] = UserDelete::userid2nickname($dialog_user->userid) ?: '[Delete]';
|
||||
$data['is_disable'] = 1;
|
||||
$data['dialog_delete'] = 1;
|
||||
}
|
||||
$data['dialog_user'] = $dialog_user;
|
||||
@@ -447,10 +474,10 @@ class WebSocketDialog extends AbstractModel
|
||||
WebSocketDialogUser::updateInsert([
|
||||
'dialog_id' => $this->id,
|
||||
'userid' => $value,
|
||||
], $updateData, function() use ($value) {
|
||||
return [
|
||||
'bot' => User::isBot($value) ? 1 : 0,
|
||||
];
|
||||
], $updateData, function() use ($value, $updateData) {
|
||||
return array_merge($updateData, [
|
||||
'bot' => User::isBot($value) ? 1 : 0
|
||||
]);
|
||||
}, $isInsert);
|
||||
if ($isInsert) {
|
||||
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
|
||||
@@ -503,6 +530,7 @@ class WebSocketDialog extends AbstractModel
|
||||
}
|
||||
}
|
||||
//
|
||||
$item->operator_id = User::userid();
|
||||
$item->delete();
|
||||
//
|
||||
if ($pushMsg) {
|
||||
@@ -524,6 +552,42 @@ class WebSocketDialog extends AbstractModel
|
||||
$this->pushMsg("groupUpdate", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送成员事件到机器人 webhook
|
||||
* @param string $event
|
||||
* @param int $memberId
|
||||
* @param int $operatorId
|
||||
* @return void
|
||||
*/
|
||||
public function dispatchMemberWebhook(string $event, int $memberId, int $operatorId): void
|
||||
{
|
||||
$botIds = $this->dialogUser()->where('bot', 1)->pluck('userid')->toArray();
|
||||
if (empty($botIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userBots = UserBot::whereIn('bot_id', $botIds)->get();
|
||||
if ($userBots->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$member = User::find($memberId, ['userid', 'nickname', 'email', 'bot'])?->toArray();
|
||||
$operator = $operatorId === $memberId ? $member : User::find($operatorId, ['userid', 'nickname', 'email', 'bot'])?->toArray();
|
||||
|
||||
$payload = [
|
||||
'dialog_id' => $this->id,
|
||||
'dialog_type' => $this->type,
|
||||
'group_type' => $this->group_type,
|
||||
'dialog_name' => $this->getGroupName(),
|
||||
'member' => $member,
|
||||
'operator' => $operator,
|
||||
];
|
||||
|
||||
foreach ($userBots as $userBot) {
|
||||
$userBot->dispatchWebhook($event, $payload, 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
* @return bool
|
||||
@@ -666,6 +730,60 @@ class WebSocketDialog extends AbstractModel
|
||||
Task::deliver($task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是单人对话
|
||||
* @return bool
|
||||
*/
|
||||
public function isSelfDialog()
|
||||
{
|
||||
if ($this->type !== 'user') {
|
||||
return false;
|
||||
}
|
||||
return WebSocketDialogUser::whereDialogId($this->id)->where('userid', '>', 0)->count() === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持创建会话
|
||||
* @return bool
|
||||
*/
|
||||
public function isSessionDialog()
|
||||
{
|
||||
// 这个不会有变化,所以可以使用永久缓存
|
||||
return Cache::rememberForever('is-session-dialog-' . $this->id, function () {
|
||||
if ($this->type !== 'user') {
|
||||
return false;
|
||||
}
|
||||
$data = $this->dialogUserBuilder()->get();
|
||||
foreach ($data as $item) {
|
||||
if (preg_match('/^(ai-|user-session-)(.*?)@bot\.system$/', $item->email)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是AI对话
|
||||
* @return bool
|
||||
*/
|
||||
public function isAiDialog()
|
||||
{
|
||||
// 这个不会有变化,所以可以使用永久缓存
|
||||
return Cache::rememberForever('is-ai-dialog-' . $this->id, function () {
|
||||
if ($this->type !== 'user') {
|
||||
return false;
|
||||
}
|
||||
$data = $this->dialogUserBuilder()->get();
|
||||
foreach ($data as $item) {
|
||||
if (preg_match('/^ai-(.*?)@bot\.system$/', $item->email)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话(同时检验对话身份)
|
||||
* @param $dialog_id
|
||||
@@ -674,6 +792,9 @@ class WebSocketDialog extends AbstractModel
|
||||
*/
|
||||
public static function checkDialog($dialog_id, $checkOwner = false)
|
||||
{
|
||||
if ($dialog_id <= 0) {
|
||||
throw new ApiException('参数错误');
|
||||
}
|
||||
$dialog = WebSocketDialog::find($dialog_id);
|
||||
if (empty($dialog)) {
|
||||
throw new ApiException('对话不存在或已被删除', ['dialog_id' => $dialog_id], -4003);
|
||||
@@ -687,18 +808,25 @@ class WebSocketDialog extends AbstractModel
|
||||
throw new ApiException('仅限群主操作');
|
||||
}
|
||||
//
|
||||
if ($dialog->group_type === 'task') {
|
||||
// 任务群对话校验是否在项目内
|
||||
$project_id = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id'));
|
||||
if ($project_id > 0) {
|
||||
if (ProjectUser::whereProjectId($project_id)->whereUserid($userid)->exists()) {
|
||||
switch ($dialog->group_type) {
|
||||
case 'project':
|
||||
case 'task':
|
||||
// 项目群、任务群对话校验是否在项目内
|
||||
if ($dialog->group_type === 'project') {
|
||||
$projectId = intval(Project::whereDialogId($dialog->id)->value('id'));
|
||||
} else {
|
||||
$projectId = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id'));
|
||||
}
|
||||
if ($projectId > 0 && ProjectUser::whereProjectId($projectId)->whereUserid($userid)->exists()) {
|
||||
return $dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($dialog->group_type == 'okr') {
|
||||
return $dialog;
|
||||
break;
|
||||
|
||||
case 'okr':
|
||||
// OKR群对话不用校验
|
||||
return $dialog;
|
||||
}
|
||||
//
|
||||
if (!WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->exists()) {
|
||||
WebSocketDialogMsgRead::forceRead($dialog_id, $userid);
|
||||
throw new ApiException('不在成员列表内', ['dialog_id' => $dialog_id], -4003);
|
||||
@@ -729,6 +857,7 @@ class WebSocketDialog extends AbstractModel
|
||||
WebSocketDialogUser::createInstance([
|
||||
'dialog_id' => $dialog->id,
|
||||
'userid' => $value,
|
||||
'bot' => User::isBot($value) ? 1 : 0,
|
||||
'important' => !in_array($group_type, ['user', 'all']),
|
||||
'last_at' => in_array($group_type, ['user', 'department', 'all']) ? Carbon::now() : null,
|
||||
])->save();
|
||||
@@ -765,11 +894,22 @@ class WebSocketDialog extends AbstractModel
|
||||
WebSocketDialogUser::createInstance([
|
||||
'dialog_id' => $dialog->id,
|
||||
'userid' => $user->userid,
|
||||
'bot' => User::isBot($user->userid) ? 1 : 0,
|
||||
])->save();
|
||||
WebSocketDialogUser::createInstance([
|
||||
'dialog_id' => $dialog->id,
|
||||
'userid' => $receiver,
|
||||
'bot' => User::isBot($receiver) ? 1 : 0,
|
||||
])->save();
|
||||
//
|
||||
if ($user->isAiBot() || User::find($receiver)?->isAiBot()) {
|
||||
$session = WebSocketDialogSession::create([
|
||||
'dialog_id' => $dialog->id,
|
||||
]);
|
||||
$session->save();
|
||||
$dialog->session_id = $session->id;
|
||||
$dialog->save();
|
||||
}
|
||||
return $dialog;
|
||||
});
|
||||
}
|
||||
@@ -831,7 +971,7 @@ class WebSocketDialog extends AbstractModel
|
||||
$data = [];
|
||||
foreach ($dialogIds as $dialog_id) {
|
||||
$dialog = WebSocketDialog::checkDialog($dialog_id);
|
||||
//
|
||||
|
||||
$action = $replyId > 0 ? "reply-$replyId" : "";
|
||||
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
|
||||
if ($image64) {
|
||||
@@ -845,49 +985,52 @@ class WebSocketDialog extends AbstractModel
|
||||
Base::makeDir(public_path($path));
|
||||
copy($filePath, public_path($path) . basename($filePath));
|
||||
} else {
|
||||
$setting = Base::setting("system");
|
||||
$data = Base::upload([
|
||||
"file" => $files,
|
||||
"type" => 'more',
|
||||
"path" => $path,
|
||||
"fileName" => $fileName,
|
||||
"quality" => true,
|
||||
"convertVideo" => true
|
||||
"convertVideo" => $setting['convert_video'] === 'open',
|
||||
"compressVideo" => $setting['compress_video'] === 'open',
|
||||
]);
|
||||
}
|
||||
//
|
||||
if (Base::isError($data)) {
|
||||
throw new ApiException($data['msg']);
|
||||
} else {
|
||||
$fileData = $data['data'];
|
||||
$filePath = $fileData['file'];
|
||||
$fileName = $fileData['name'];
|
||||
$fileData['thumb'] = Base::unFillUrl($fileData['thumb']);
|
||||
$fileData['size'] *= 1024;
|
||||
//
|
||||
if ($dialog->type === 'group' && $dialog->group_type === 'task') { // 任务群组保存文件
|
||||
if ($imageAttachment || !in_array($fileData['ext'], File::imageExt)) { // 如果是图片不保存
|
||||
$task = ProjectTask::whereDialogId($dialog->id)->first();
|
||||
if ($task) {
|
||||
$file = ProjectTaskFile::createInstance([
|
||||
'project_id' => $task->project_id,
|
||||
'task_id' => $task->id,
|
||||
'name' => $fileData['name'],
|
||||
'size' => $fileData['size'],
|
||||
'ext' => $fileData['ext'],
|
||||
'path' => $fileData['path'],
|
||||
'thumb' => $fileData['thumb'],
|
||||
'userid' => $user->userid,
|
||||
]);
|
||||
$file->save();
|
||||
}
|
||||
}
|
||||
$fileData = $data['data'];
|
||||
$filePath = $fileData['file'];
|
||||
$fileName = $fileData['name'];
|
||||
$fileData['thumb'] = Base::unFillUrl($fileData['thumb']);
|
||||
$fileData['size'] *= 1024;
|
||||
|
||||
// 任务群组保存文件
|
||||
if ($dialog->group_type === 'task') {
|
||||
// 如果是图片不保存
|
||||
if ($imageAttachment || !in_array($fileData['ext'], File::imageExt)) {
|
||||
$task = ProjectTask::whereDialogId($dialog->id)->first();
|
||||
if ($task) {
|
||||
$file = ProjectTaskFile::createInstance([
|
||||
'project_id' => $task->project_id,
|
||||
'task_id' => $task->id,
|
||||
'name' => $fileData['name'],
|
||||
'size' => $fileData['size'],
|
||||
'ext' => $fileData['ext'],
|
||||
'path' => $fileData['path'],
|
||||
'thumb' => $fileData['thumb'],
|
||||
'userid' => $user->userid,
|
||||
]);
|
||||
$file->save();
|
||||
}
|
||||
}
|
||||
//
|
||||
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid);
|
||||
if (Base::isSuccess($result)) {
|
||||
if (isset($task)) {
|
||||
$result['data']['task_id'] = $task->id;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid);
|
||||
if (Base::isSuccess($result)) {
|
||||
if (isset($task)) {
|
||||
$result['data']['task_id'] = $task->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace App\Models;
|
||||
* @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|null $user
|
||||
* @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)
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Image;
|
||||
use App\Tasks\PushTask;
|
||||
use App\Models\ProjectTaskRelation;
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Tasks\WebSocketDialogMsgTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
@@ -18,6 +20,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 消息类型(用于搜索)
|
||||
@@ -27,7 +30,6 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property int|null $read 已阅数量
|
||||
* @property int|null $send 发送数量
|
||||
* @property int|null $tag 标注会员ID
|
||||
* @property int|null $session_id 会话ID
|
||||
* @property int|null $todo 设为待办会员ID
|
||||
* @property int|null $link 是否存在链接
|
||||
* @property int|null $modify 是否编辑
|
||||
@@ -305,11 +307,34 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否完成所有待办
|
||||
* @param bool $noCache 是否禁止缓存
|
||||
* @return int 1=已完成 0=未完成
|
||||
*/
|
||||
public function isTodoDone(?bool $noCache = false): int
|
||||
{
|
||||
if ($noCache) {
|
||||
Cache::forget('todo_done_' . $this->id);
|
||||
}
|
||||
if ($this->todo <= 0) {
|
||||
return 1;
|
||||
}
|
||||
return (int) Cache::remember('todo_done_' . $this->id, Carbon::now()->addDays(), function () {
|
||||
return WebSocketDialogMsgTodo::whereMsgId($this->id)->whereDoneAt(null)->exists() ? 0 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 标注、取消标注
|
||||
* @param int $sender 标注的会员ID
|
||||
@@ -362,24 +387,15 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
if (in_array($this->type, ['tag', 'todo', 'notice'])) {
|
||||
return Base::retError('此消息不支持设待办');
|
||||
}
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
$current = WebSocketDialogMsgTodo::whereMsgId($this->id)->pluck('userid')->toArray();
|
||||
$cancel = array_diff($current, $userids);
|
||||
$setup = array_diff($userids, $current);
|
||||
//
|
||||
$this->todo = $setup || count($current) > count($cancel) ? $sender : 0;
|
||||
$this->save();
|
||||
$upData = [
|
||||
'id' => $this->id,
|
||||
'todo' => $this->todo,
|
||||
'dialog_id' => $this->dialog_id,
|
||||
];
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
$dialog->pushMsg('update', $upData);
|
||||
//
|
||||
$retData = [
|
||||
'add' => [],
|
||||
'update' => $upData
|
||||
];
|
||||
$addData = [];
|
||||
if ($cancel) {
|
||||
$res = self::sendMsg(null, $this->dialog_id, 'todo', [
|
||||
'action' => 'remove',
|
||||
@@ -391,7 +407,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
]
|
||||
], $sender);
|
||||
if (Base::isSuccess($res)) {
|
||||
$retData['add'][] = $res['data'];
|
||||
$addData[] = $res['data'];
|
||||
WebSocketDialogMsgTodo::whereMsgId($this->id)->whereIn('userid', $cancel)->delete();
|
||||
}
|
||||
}
|
||||
@@ -406,7 +422,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
]
|
||||
], $sender);
|
||||
if (Base::isSuccess($res)) {
|
||||
$retData['add'][] = $res['data'];
|
||||
$addData[] = $res['data'];
|
||||
$useridList = $dialog->dialogUser->pluck('userid')->toArray();
|
||||
foreach ($setup as $userid) {
|
||||
if (!in_array($userid, $useridList)) {
|
||||
@@ -421,7 +437,18 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
}
|
||||
//
|
||||
return Base::retSuccess($this->todo ? '设置成功' : '取消成功', $retData);
|
||||
$upData = [
|
||||
'id' => $this->id,
|
||||
'todo' => $this->todo,
|
||||
'todo_done' => $this->isTodoDone(true),
|
||||
'dialog_id' => $this->dialog_id,
|
||||
];
|
||||
$dialog->pushMsg('update', $upData);
|
||||
//
|
||||
return Base::retSuccess($this->todo ? '设置成功' : '取消成功', [
|
||||
'add' => $addData,
|
||||
'update' => $upData,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -451,27 +478,10 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
'parent_id' => $this->id, // 转发的消息ID
|
||||
'parent_userid' => $this->userid, // 转发的消息会员ID
|
||||
'show' => $showSource, // 是否显示原发送者信息
|
||||
'leave' => $leaveMessage ? 1 : 0, // 是否留言(用于判断是否发给AI)
|
||||
];
|
||||
$msgs = [];
|
||||
$already = [];
|
||||
if ($dialogids) {
|
||||
if (!is_array($dialogids)) {
|
||||
$dialogids = [$dialogids];
|
||||
}
|
||||
foreach ($dialogids as $dialogid) {
|
||||
$res = self::sendMsg('forward-' . $forwardId, $dialogid, $this->type, $msgData, $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
$already[] = $dialogid;
|
||||
}
|
||||
if ($leaveMessage) {
|
||||
$res = self::sendMsg(null, $dialogid, 'text', ['text' => $leaveMessage], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$dialogs = [];
|
||||
if ($userids) {
|
||||
if (!is_array($userids)) {
|
||||
$userids = [$userids];
|
||||
@@ -481,17 +491,35 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
continue;
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
|
||||
if ($dialog && !in_array($dialog->id, $already)) {
|
||||
$res = self::sendMsg('forward-' . $forwardId, $dialog->id, $this->type, $msgData, $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
}
|
||||
if ($leaveMessage) {
|
||||
$res = self::sendMsg(null, $dialog->id, 'text', ['text' => $leaveMessage], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
}
|
||||
}
|
||||
if ($dialog) {
|
||||
$dialogs[$dialog->id] = $dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($dialogids) {
|
||||
if (!is_array($dialogids)) {
|
||||
$dialogids = [$dialogids];
|
||||
}
|
||||
foreach ($dialogids as $dialogid) {
|
||||
if (isset($dialogs[$dialogid])) {
|
||||
continue;
|
||||
}
|
||||
$dialog = WebSocketDialog::find($dialogid);
|
||||
if ($dialog) {
|
||||
$dialogs[$dialog->id] = $dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($dialogs as $dialog) {
|
||||
$res = self::sendMsg('forward-' . $forwardId, $dialog->id, $this->type, $msgData, $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
}
|
||||
if ($leaveMessage) {
|
||||
$action = $dialog->isAiDialog() ? "reply-{$res['data']['id']}" : null;
|
||||
$res = self::sendMsg($action, $dialog->id, 'text', ['text' => $leaveMessage], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,10 +562,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();
|
||||
@@ -654,14 +678,24 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
* @param bool $preserveHtml 保留html格式
|
||||
* @return string|string[]|null
|
||||
*/
|
||||
private static function previewTextMsg($msgData, $preserveHtml = false)
|
||||
public static function previewTextMsg($msgData, $preserveHtml = false)
|
||||
{
|
||||
$text = $msgData['text'] ?? '';
|
||||
if (!$text) return '';
|
||||
if ($msgData['type'] === 'md') {
|
||||
$text = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $text);
|
||||
$text = Base::markdown2html($text);
|
||||
$text = self::previewConvertTaskList($text);
|
||||
if (preg_match('/:::\s*reasoning\s+/', $text)) {
|
||||
return Doo::translate('思考中...');
|
||||
}
|
||||
$title = '';
|
||||
if (preg_match('/^#{1,2}\s+(.+)/m', $text, $matches)) {
|
||||
$title = trim($matches[1]);
|
||||
}
|
||||
if ($title) {
|
||||
$text = $title;
|
||||
} else {
|
||||
$text = Base::markdown2html($text);
|
||||
}
|
||||
}
|
||||
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text);
|
||||
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?>/", "[" . Doo::translate('动画表情') . "]", $text);
|
||||
@@ -676,36 +710,6 @@ 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
|
||||
@@ -759,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':
|
||||
@@ -811,6 +821,89 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
return $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取消息内容
|
||||
* 根据消息类型(文件、文本等)提取相应的内容文本
|
||||
*
|
||||
* @param int $maxLength 最大长度,超过则截取,0表示不限制
|
||||
* @return string 提取出的消息文本内容
|
||||
*/
|
||||
public function extractMessageContent(int $maxLength = 0): string
|
||||
{
|
||||
$reserves = [];
|
||||
switch ($this->type) {
|
||||
case "file":
|
||||
// 提取文件消息
|
||||
$result = " 文件:{$this->msg['name']}(大小:{$this->msg['size']}B,URL:{$this->msg['path']}) ";
|
||||
break;
|
||||
|
||||
case "text":
|
||||
// 提取文本消息
|
||||
$result = $this->msg['text'] ?: '';
|
||||
if (empty($result)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 提取快捷键
|
||||
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>(.*?)<\/span>/is", $result, $match)) {
|
||||
$command = $match[2] ?? '';
|
||||
$command = preg_replace("/^%3A\.?/", ":", $command);
|
||||
$command = trim($command);
|
||||
if ($command) {
|
||||
return $command;
|
||||
}
|
||||
}
|
||||
|
||||
// 提及任务、文件、报告
|
||||
$result = preg_replace_callback_array([
|
||||
// 用户
|
||||
"/<span class=\"mention user\" data-id=\"(\d+)\">(.*?)<\/span>/" => function ($match) {
|
||||
return "";
|
||||
},
|
||||
|
||||
// 任务
|
||||
"/<span class=\"mention task\" data-id=\"(\d+)\">#?(.*?)<\/span>/" => function ($match) {
|
||||
return " 任务:{$match[2]} (任务ID:{$match[1]}) ";
|
||||
},
|
||||
|
||||
// 文件
|
||||
"/<a class=\"mention file\" href=\"([^\"']+?)\"[^>]*?>~?(.*?)<\/a>/" => function ($match) use (&$reserves) {
|
||||
$idOrCode = "";
|
||||
if (preg_match("/single\/file\/(.*?)$/", $match[1], $subMatch)) {
|
||||
$idOrCode = " (" . (Base::isNumber($subMatch[1]) ? "文件ID:{$subMatch[1]}" : "文件分享码:{$subMatch[1]}") . ")";
|
||||
}
|
||||
return " 文件:{$match[2]}{$idOrCode} ";
|
||||
},
|
||||
|
||||
// 报告
|
||||
"/<a class=\"mention report\" href=\"([^\"']+?)\"[^>]*?>%?(.*?)<\/a>/" => function ($match) use (&$reserves) {
|
||||
$idOrCode = "";
|
||||
if (preg_match("/single\/report\/detail\/(.*?)$/", $match[1], $subMatch)) {
|
||||
$idOrCode = " (" . (Base::isNumber($subMatch[1]) ? "报告ID:{$subMatch[1]}" : "报告分享码:{$subMatch[1]}") . ")";
|
||||
}
|
||||
return " 工作汇报:{$match[2]}{$idOrCode} ";
|
||||
},
|
||||
], $result);
|
||||
|
||||
// 转成 markdown
|
||||
if ($this->msg['type'] !== 'md') {
|
||||
$result = Base::html2markdown($result);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// 其他类型消息不处理
|
||||
return '';
|
||||
}
|
||||
|
||||
// 截取最大长度
|
||||
if ($maxLength > 0 && mb_strlen($result) > $maxLength) {
|
||||
$result = mb_substr($result, 0, $maxLength);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文本消息内容,用于发送前
|
||||
* @param $text
|
||||
@@ -885,8 +978,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"];
|
||||
}
|
||||
@@ -921,7 +1022,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);
|
||||
@@ -929,6 +1030,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') {
|
||||
@@ -944,6 +1046,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);
|
||||
}
|
||||
@@ -972,31 +1087,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);
|
||||
}
|
||||
}
|
||||
// 过滤标签
|
||||
@@ -1026,10 +1128,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 动作
|
||||
@@ -1139,6 +1272,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
//
|
||||
$updateData = [
|
||||
'type' => $type,
|
||||
'mtype' => $mtype,
|
||||
'link' => $link,
|
||||
'msg' => array_merge($oldMsg, $msg),
|
||||
@@ -1146,6 +1280,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
];
|
||||
$dialogMsg->updateInstance($updateData);
|
||||
$dialogMsg->generateKeyAndSave($search_key);
|
||||
ProjectTaskRelation::recordMentionsFromMessage($dialogMsg);
|
||||
//
|
||||
WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($sender)->whereHide(1)->change([
|
||||
'hide' => 0, // 修改消息时,显示会话(仅自己)
|
||||
@@ -1212,6 +1347,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
|
||||
]);
|
||||
});
|
||||
ProjectTaskRelation::recordMentionsFromMessage($dialogMsg);
|
||||
//
|
||||
$task = new WebSocketDialogMsgTask($dialogMsg->id);
|
||||
if ($push_self) {
|
||||
@@ -1229,6 +1365,54 @@ 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 对话
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogMsgRead
|
||||
@@ -76,24 +77,74 @@ class WebSocketDialogMsgRead extends AbstractModel
|
||||
*/
|
||||
public static function onlyMarkRead($list)
|
||||
{
|
||||
$dialogMsg = [];
|
||||
if (empty($list)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$collection = collect($list);
|
||||
if ($collection->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
$ids = [];
|
||||
$msgCounts = [];
|
||||
|
||||
/** @var WebSocketDialogMsgRead $item */
|
||||
foreach ($list as $item) {
|
||||
$item->read_at = Carbon::now();
|
||||
$item->save();
|
||||
if (isset($dialogMsg[$item->msg_id])) {
|
||||
$dialogMsg[$item->msg_id]['readNum']++;
|
||||
} else {
|
||||
$dialogMsg[$item->msg_id] = [
|
||||
'dialogMsg' => $item->webSocketDialogMsg,
|
||||
'readNum' => 1
|
||||
];
|
||||
foreach ($collection as $item) {
|
||||
$ids[] = $item->id;
|
||||
if ($item->msg_id) {
|
||||
$msgCounts[$item->msg_id] = ($msgCounts[$item->msg_id] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
foreach ($dialogMsg as $item) {
|
||||
if ($item['dialogMsg']) {
|
||||
$item['dialogMsg']->increment('read', $item['readNum']);
|
||||
|
||||
if (!empty($ids)) {
|
||||
DB::table((new self())->getTable())
|
||||
->whereIn('id', $ids)
|
||||
->whereNull('read_at')
|
||||
->update(['read_at' => $now]);
|
||||
}
|
||||
|
||||
if (!empty($msgCounts)) {
|
||||
$cases = [];
|
||||
$bindings = [];
|
||||
foreach ($msgCounts as $msgId => $num) {
|
||||
$cases[] = 'WHEN ? THEN ?';
|
||||
$bindings[] = $msgId;
|
||||
$bindings[] = $num;
|
||||
}
|
||||
$msgIds = array_keys($msgCounts);
|
||||
$bindings = array_merge($bindings, $msgIds);
|
||||
$placeholders = implode(',', array_fill(0, count($msgIds), '?'));
|
||||
$table = DB::getTablePrefix() . (new WebSocketDialogMsg())->getTable();
|
||||
$sql = "UPDATE {$table} SET `read` = `read` + CASE `id` " . implode(' ', $cases) . " END WHERE `deleted_at` IS NULL AND `id` IN ({$placeholders})";
|
||||
DB::update($sql, $bindings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记指定会话的历史消息为已读
|
||||
* @param int $dialogId
|
||||
* @param int $sessionId
|
||||
* @param int $chunkSize
|
||||
* @return void
|
||||
*/
|
||||
public static function markSessionMessagesAsRead(int $dialogId, int $sessionId, int $chunkSize = 100): void
|
||||
{
|
||||
if ($dialogId <= 0 || $sessionId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::whereDialogId($dialogId)
|
||||
->whereNull('read_at')
|
||||
->whereIn('msg_id', function ($query) use ($dialogId, $sessionId) {
|
||||
$query->select('id')
|
||||
->from((new WebSocketDialogMsg())->getTable())
|
||||
->where('dialog_id', $dialogId)
|
||||
->where('session_id', $sessionId);
|
||||
})
|
||||
->chunkById($chunkSize, function ($list) {
|
||||
self::onlyMarkRead($list);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Extranet;
|
||||
use Swoole\Coroutine;
|
||||
use App\Tasks\UpdateSessionTitleViaAiTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Cache;
|
||||
|
||||
/**
|
||||
@@ -16,7 +16,6 @@ use Cache;
|
||||
* @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)
|
||||
@@ -67,15 +66,14 @@ class WebSocketDialogSession extends AbstractModel
|
||||
if ($dialogMsg->type != 'text') {
|
||||
return;
|
||||
}
|
||||
if ($dialogMsg->msg['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;
|
||||
}
|
||||
$title = $dialogMsg->key ?: WebSocketDialogMsg::previewTextMsg($dialogMsg->msg) ?: 'Untitled';
|
||||
$session = self::whereId($sessionId)->first();
|
||||
if (!$session) {
|
||||
return;
|
||||
@@ -83,18 +81,6 @@ class WebSocketDialogSession extends AbstractModel
|
||||
$session->title = $title;
|
||||
$session->save();
|
||||
Cache::forever($cacheKey, true);
|
||||
// 通过AI接口更新对话标题
|
||||
go(function () use ($session, $title, $originalTitle) {
|
||||
Coroutine::sleep(0.1);
|
||||
$res = Extranet::openAIGenerateTitle($originalTitle);
|
||||
if (Base::isError($res)) {
|
||||
return;
|
||||
}
|
||||
$newTitle = $res['data'];
|
||||
if ($newTitle && $newTitle != $title) {
|
||||
$session->title = Base::cutStr($newTitle, 100);
|
||||
$session->save();
|
||||
}
|
||||
});
|
||||
Task::deliver(new UpdateSessionTitleViaAiTask($session->id, $dialogMsg->msg['text']));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace App\Models;
|
||||
* @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是
|
||||
@@ -28,6 +29,7 @@ namespace App\Models;
|
||||
* @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)
|
||||
|
||||
735
app/Module/AI.php
Normal file
735
app/Module/AI.php
Normal file
@@ -0,0 +1,735 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* AI 助手模块
|
||||
*/
|
||||
class AI
|
||||
{
|
||||
public const TEXT_MODEL_PRIORITY = [
|
||||
'openai',
|
||||
'claude',
|
||||
'deepseek',
|
||||
'gemini',
|
||||
'grok',
|
||||
'ollama',
|
||||
'zhipu',
|
||||
'qianwen',
|
||||
'wenxin'
|
||||
];
|
||||
protected const OPENAI_DEFAULT_MODEL = 'gpt-5-mini';
|
||||
|
||||
protected $post = [];
|
||||
protected $headers = [];
|
||||
protected $urlPath = '';
|
||||
protected $timeout = 30;
|
||||
protected $providerConfig = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param array $post
|
||||
* @param array $headers
|
||||
*/
|
||||
public function __construct($post = [], $headers = [])
|
||||
{
|
||||
$this->post = $post ?? [];
|
||||
$this->headers = $headers ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求参数
|
||||
* @param array $post
|
||||
*/
|
||||
public function setPost($post)
|
||||
{
|
||||
$this->post = array_merge($this->post, $post);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求头
|
||||
* @param array $headers
|
||||
*/
|
||||
public function setHeaders($headers)
|
||||
{
|
||||
$this->headers = array_merge($this->headers, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求路径
|
||||
* @param string $urlPath
|
||||
*/
|
||||
public function setUrlPath($urlPath)
|
||||
{
|
||||
$this->urlPath = $urlPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求超时时间
|
||||
* @param int $timeout
|
||||
*/
|
||||
public function setTimeout($timeout)
|
||||
{
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定请求所使用的模型配置
|
||||
* @param array $provider
|
||||
*/
|
||||
public function setProvider(array $provider)
|
||||
{
|
||||
$this->providerConfig = $provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求 AI 接口
|
||||
* @param bool $resRaw 是否返回原始数据
|
||||
* @return array
|
||||
*/
|
||||
public function request($resRaw = false)
|
||||
{
|
||||
$provider = $this->providerConfig ?: self::resolveTextProvider();
|
||||
if (!$provider) {
|
||||
return Base::retError("请先配置 AI 助手");
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $provider['api_key'],
|
||||
];
|
||||
if (!empty($provider['agency'])) {
|
||||
$headers['CURLOPT_PROXY'] = $provider['agency'];
|
||||
$headers['CURLOPT_PROXYTYPE'] = str_contains($provider['agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
|
||||
}
|
||||
$headers = array_merge($headers, $this->headers);
|
||||
|
||||
$baseUrl = $provider['base_url'] ?: 'https://api.openai.com/v1';
|
||||
$url = $baseUrl . ($this->urlPath ?: '/chat/completions');
|
||||
|
||||
$result = Ihttp::ihttp_request($url, $this->post, $headers, $this->timeout);
|
||||
if (Base::isError($result)) {
|
||||
return Base::retError("AI 接口请求失败", $result);
|
||||
}
|
||||
$result = $result['data'];
|
||||
|
||||
if (!$resRaw) {
|
||||
$resData = Base::json2array($result);
|
||||
if (empty($resData['choices'])) {
|
||||
return Base::retError("AI 接口返回数据格式错误", $resData);
|
||||
}
|
||||
$result = $resData['choices'][0]['message']['content'];
|
||||
$result = trim($result);
|
||||
if (empty($result)) {
|
||||
return Base::retError("AI 接口返回数据为空");
|
||||
}
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 AI 流式会话凭证
|
||||
* @param string $modelType
|
||||
* @param string $modelName
|
||||
* @param mixed $contextInput
|
||||
* @return array
|
||||
*/
|
||||
public static function createStreamKey($modelType, $modelName, $contextInput = [])
|
||||
{
|
||||
$modelType = trim((string)$modelType);
|
||||
$modelName = trim((string)$modelName);
|
||||
|
||||
if ($modelType === '' || $modelName === '') {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
|
||||
if (is_string($contextInput)) {
|
||||
$decoded = json_decode($contextInput, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$contextInput = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($contextInput)) {
|
||||
return Base::retError('context 参数格式错误');
|
||||
}
|
||||
|
||||
$context = [];
|
||||
foreach ($contextInput as $item) {
|
||||
if (!is_array($item) || count($item) < 2) {
|
||||
continue;
|
||||
}
|
||||
$role = trim((string)($item[0] ?? ''));
|
||||
$message = trim((string)($item[1] ?? ''));
|
||||
if ($role === '' || $message === '') {
|
||||
continue;
|
||||
}
|
||||
$context[] = [$role, $message];
|
||||
}
|
||||
|
||||
$contextJson = json_encode($context, JSON_UNESCAPED_UNICODE);
|
||||
if ($contextJson === false) {
|
||||
return Base::retError('context 参数格式错误');
|
||||
}
|
||||
|
||||
$setting = Base::setting('aibotSetting');
|
||||
if (!is_array($setting)) {
|
||||
$setting = [];
|
||||
}
|
||||
|
||||
$apiKey = Base::val($setting, $modelType . '_key');
|
||||
if ($modelType === 'wenxin') {
|
||||
$wenxinSecret = Base::val($setting, 'wenxin_secret');
|
||||
if ($wenxinSecret) {
|
||||
$apiKey = trim(($apiKey ?: '') . ':' . $wenxinSecret);
|
||||
}
|
||||
}
|
||||
if ($modelType === 'ollama' && empty($apiKey)) {
|
||||
$apiKey = Base::strRandom(6);
|
||||
}
|
||||
if (empty($apiKey)) {
|
||||
return Base::retError('模型未启用');
|
||||
}
|
||||
|
||||
$remoteModelType = match ($modelType) {
|
||||
'qianwen' => 'qwen',
|
||||
default => $modelType,
|
||||
};
|
||||
|
||||
$authParams = [
|
||||
'api_key' => $apiKey,
|
||||
'model_type' => $remoteModelType,
|
||||
'model_name' => $modelName,
|
||||
'context' => $contextJson,
|
||||
];
|
||||
|
||||
$baseUrl = trim((string)($setting[$modelType . '_base_url'] ?? ''));
|
||||
if ($baseUrl !== '') {
|
||||
$authParams['base_url'] = $baseUrl;
|
||||
}
|
||||
|
||||
$agency = trim((string)($setting[$modelType . '_agency'] ?? ''));
|
||||
if ($agency !== '') {
|
||||
$authParams['agency'] = $agency;
|
||||
}
|
||||
|
||||
$thinkPatterns = [
|
||||
"/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/",
|
||||
"/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/"
|
||||
];
|
||||
$thinkMatch = [];
|
||||
foreach ($thinkPatterns as $pattern) {
|
||||
if (preg_match($pattern, $authParams['model_name'], $thinkMatch)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($thinkMatch && !empty($thinkMatch[1])) {
|
||||
$authParams['model_name'] = $thinkMatch[1];
|
||||
}
|
||||
|
||||
$authResult = Ihttp::ihttp_request('http://nginx/ai/invoke/auth', $authParams, [
|
||||
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
'Authorization' => 'Bearer ' . Base::token(),
|
||||
], 30);
|
||||
if (Base::isError($authResult)) {
|
||||
return Base::retError($authResult['msg']);
|
||||
}
|
||||
|
||||
$body = Base::json2array($authResult['data']);
|
||||
if (($body['code'] ?? null) !== 200) {
|
||||
return Base::retError(($body['error'] ?? '') ?: 'AI 接口返回异常', $body);
|
||||
}
|
||||
|
||||
$streamKey = Base::val($body, 'data.stream_key');
|
||||
if (empty($streamKey)) {
|
||||
return Base::retError('AI 接口返回数据异常');
|
||||
}
|
||||
|
||||
return Base::retSuccess('success', [
|
||||
'stream_key' => $streamKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/** ******************************************************************************************** */
|
||||
/** ******************************************************************************************** */
|
||||
/** ******************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 通过 openAI 语音转文字
|
||||
* @param string $filePath 语音文件路径
|
||||
* @param array $extParams 扩展参数
|
||||
* @param array $extHeaders 扩展请求头
|
||||
* @param bool $noCache 是否禁用缓存
|
||||
* @return array
|
||||
*/
|
||||
public static function transcriptions($filePath, $extParams = [], $extHeaders = [], $noCache = false)
|
||||
{
|
||||
Apps::isInstalledThrow('ai');
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
return Base::retError("语音文件不存在");
|
||||
}
|
||||
$cacheKey = "openAItranscriptions::" . md5($filePath . '_' . Base::array2json($extParams));
|
||||
if ($noCache) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
$audioProvider = self::resolveOpenAIAudioProvider();
|
||||
if (!$audioProvider) {
|
||||
return Base::retError("请先在「AI 助手」设置中配置 OpenAI");
|
||||
}
|
||||
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addDays(), function () use ($extParams, $extHeaders, $filePath, $audioProvider) {
|
||||
$post = array_merge($extParams, [
|
||||
'file' => new \CURLFile($filePath),
|
||||
'model' => 'whisper-1',
|
||||
]);
|
||||
$header = array_merge($extHeaders, [
|
||||
'Content-Type' => 'multipart/form-data',
|
||||
]);
|
||||
|
||||
$ai = new self($post, $header);
|
||||
$ai->setProvider($audioProvider);
|
||||
$ai->setUrlPath('/audio/transcriptions');
|
||||
$ai->setTimeout(15);
|
||||
|
||||
$res = $ai->request(true);
|
||||
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", [
|
||||
'file' => $filePath,
|
||||
'text' => $resData['text'],
|
||||
]);
|
||||
});
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 openAI 翻译
|
||||
* @param string $text 需要翻译的文本内容
|
||||
* @param string $targetLanguage 目标语言(如:English, 简体中文, 日本語等)
|
||||
* @param bool $noCache 是否禁用缓存
|
||||
* @return array
|
||||
*/
|
||||
public static function translations($text, $targetLanguage, $noCache = false)
|
||||
{
|
||||
Apps::isInstalledThrow('ai');
|
||||
|
||||
$cacheKey = "openAItranslations::" . md5($text . '_' . $targetLanguage);
|
||||
if ($noCache) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
$provider = self::resolveTextProvider();
|
||||
if (!$provider) {
|
||||
return Base::retError("请先配置 AI 助手");
|
||||
}
|
||||
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addDays(7), function () use ($text, $targetLanguage, $provider) {
|
||||
$payload = [
|
||||
"model" => $provider['model'],
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一名资深的专业翻译专家,专门从事项目任务管理系统的多语言本地化工作。
|
||||
|
||||
翻译任务:将提供的文本内容翻译为 {$targetLanguage}
|
||||
|
||||
专业要求:
|
||||
1. 术语一致性:确保项目管理、任务管理、团队协作等专业术语的准确翻译
|
||||
2. 上下文理解:根据项目管理场景选择最合适的表达方式
|
||||
3. 格式保持:严格保持原文的格式、结构、标点符号和排版
|
||||
4. 语言规范:使用目标语言的标准表达,符合该语言的语法和习惯
|
||||
5. 专业性:体现项目管理领域的专业水准和准确性
|
||||
6. 简洁性:避免冗余表达,保持语言简洁明了
|
||||
|
||||
注意事项:
|
||||
- 保留所有HTML标签、特殊符号、数字、日期格式
|
||||
- 对于专有名词(如软件名称、品牌名)保持原文
|
||||
- 确保翻译后的文本自然流畅,符合目标语言的表达习惯
|
||||
- 如遇到歧义表达,优先选择项目管理场景下的含义
|
||||
|
||||
请直接返回翻译结果,不要包含任何解释或标记。
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => "请将以下内容翻译为 {$targetLanguage}:\n\n{$text}"
|
||||
]
|
||||
],
|
||||
];
|
||||
if (self::shouldSendReasoningEffort($provider)) {
|
||||
$payload['reasoning_effort'] = 'minimal';
|
||||
}
|
||||
$post = json_encode($payload);
|
||||
|
||||
$ai = new self($post);
|
||||
$ai->setProvider($provider);
|
||||
$ai->setTimeout(60);
|
||||
|
||||
$res = $ai->request();
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("翻译请求失败", $res);
|
||||
}
|
||||
$result = $res['data'];
|
||||
if (empty($result)) {
|
||||
return Base::retError("翻译结果为空");
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", [
|
||||
'translated_text' => $result,
|
||||
'target_language' => $targetLanguage,
|
||||
'translated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
});
|
||||
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 openAI 生成标题
|
||||
* @param string $text 需要生成标题的文本内容
|
||||
* @param bool $noCache 是否禁用缓存
|
||||
* @return array
|
||||
*/
|
||||
public static function generateTitle($text, $noCache = false)
|
||||
{
|
||||
if (!Apps::isInstalled('ai')) {
|
||||
return Base::retError('应用「AI Assistant」未安装');
|
||||
}
|
||||
|
||||
$cacheKey = "openAIGenerateTitle::" . md5($text);
|
||||
if ($noCache) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
$provider = self::resolveTextProvider();
|
||||
if (!$provider) {
|
||||
return Base::retError("请先配置 AI 助手");
|
||||
}
|
||||
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addHours(24), function () use ($text, $provider) {
|
||||
$payload = [
|
||||
"model" => $provider['model'],
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一个专业的标题生成器,专门为项目任务管理系统的对话内容生成精准、简洁的标题。
|
||||
|
||||
要求:
|
||||
1. 标题要准确概括文本的核心内容和主要意图
|
||||
2. 标题长度控制在5-20个字符之间
|
||||
3. 语言简洁明了,避免冗余词汇
|
||||
4. 适合在项目管理场景中使用
|
||||
5. 不要包含引号或特殊符号
|
||||
6. 如果是技术讨论,突出技术要点
|
||||
7. 如果是项目管理内容,突出关键动作或目标
|
||||
8. 如果是需求讨论,突出需求的核心点
|
||||
|
||||
请直接返回标题,不要包含任何解释或其他内容。
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => "请为以下内容生成一个合适的标题:\n\n" . $text
|
||||
]
|
||||
],
|
||||
];
|
||||
if (self::shouldSendReasoningEffort($provider)) {
|
||||
$payload['reasoning_effort'] = 'minimal';
|
||||
}
|
||||
$post = json_encode($payload);
|
||||
|
||||
$ai = new self($post);
|
||||
$ai->setProvider($provider);
|
||||
$ai->setTimeout(10);
|
||||
|
||||
$res = $ai->request();
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("标题生成失败", $res);
|
||||
}
|
||||
$result = $res['data'];
|
||||
if (empty($result)) {
|
||||
return Base::retError("生成的标题为空");
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", [
|
||||
'title' => $result,
|
||||
'length' => mb_strlen($result),
|
||||
'generated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
});
|
||||
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 openAI 生成职场笑话、心灵鸡汤
|
||||
* @param bool $noCache 是否禁用缓存
|
||||
* @return array 返回20个笑话和20个心灵鸡汤
|
||||
*/
|
||||
public static function generateJokeAndSoup($noCache = false)
|
||||
{
|
||||
if (!Apps::isInstalled('ai')) {
|
||||
return Base::retError('应用「AI Assistant」未安装');
|
||||
}
|
||||
|
||||
$cacheKey = "openAIJokeAndSoup::" . md5(date('Y-m-d'));
|
||||
if ($noCache) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
$provider = self::resolveTextProvider();
|
||||
if (!$provider) {
|
||||
return Base::retError("请先配置 AI 助手");
|
||||
}
|
||||
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addHours(6), function () use ($provider) {
|
||||
$payload = [
|
||||
"model" => $provider['model'],
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一个专业的内容生成器。
|
||||
|
||||
要求:
|
||||
1. 笑话要幽默风趣,适合职场环境,内容积极正面
|
||||
2. 心灵鸡汤要励志温暖,适合职场人士阅读
|
||||
3. 每个笑话和鸡汤都要简洁明了,尽量不超过100字
|
||||
4. 必须严格按照以下JSON格式返回,不要markdown格式,不要包含任何其他内容:
|
||||
|
||||
{
|
||||
"jokes": [
|
||||
"笑话内容1",
|
||||
"笑话内容2",
|
||||
...
|
||||
],
|
||||
"soups": [
|
||||
"心灵鸡汤内容1",
|
||||
"心灵鸡汤内容2",
|
||||
...
|
||||
]
|
||||
}
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => "请生成20个职场笑话和20个心灵鸡汤"
|
||||
]
|
||||
],
|
||||
];
|
||||
if (self::shouldSendReasoningEffort($provider)) {
|
||||
$payload['reasoning_effort'] = 'minimal';
|
||||
}
|
||||
$post = json_encode($payload);
|
||||
|
||||
$ai = new self($post);
|
||||
$ai->setProvider($provider);
|
||||
$ai->setTimeout(120);
|
||||
|
||||
$res = $ai->request();
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("生成失败", $res);
|
||||
}
|
||||
|
||||
// 清理可能的markdown代码块标记
|
||||
$content = $res['data'];
|
||||
$content = preg_replace('/^\s*```json\s*/', '', $content);
|
||||
$content = preg_replace('/\s*```\s*$/', '', $content);
|
||||
if (empty($content)) {
|
||||
return Base::retError("翻译结果为空");
|
||||
}
|
||||
|
||||
// 解析JSON
|
||||
$parsedData = Base::json2array($content);
|
||||
if (!$parsedData || !isset($parsedData['jokes']) || !isset($parsedData['soups'])) {
|
||||
return Base::retError("生成内容格式错误", $content);
|
||||
}
|
||||
|
||||
// 验证数据完整性
|
||||
if (!is_array($parsedData['jokes']) || !is_array($parsedData['soups'])) {
|
||||
return Base::retError("生成内容格式错误", $parsedData);
|
||||
}
|
||||
|
||||
// 过滤空内容并确保有内容
|
||||
$jokes = array_filter(array_map('trim', $parsedData['jokes']));
|
||||
$soups = array_filter(array_map('trim', $parsedData['soups']));
|
||||
|
||||
if (empty($jokes) || empty($soups)) {
|
||||
return Base::retError("生成内容为空", $parsedData);
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", [
|
||||
'jokes' => array_values($jokes),
|
||||
'soups' => array_values($soups),
|
||||
'total_jokes' => count($jokes),
|
||||
'total_soups' => count($soups),
|
||||
'generated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
});
|
||||
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择可用的文本模型配置
|
||||
* @return array|null
|
||||
*/
|
||||
protected static function resolveTextProvider()
|
||||
{
|
||||
$setting = Base::setting('aibotSetting');
|
||||
if (!is_array($setting)) {
|
||||
$setting = [];
|
||||
}
|
||||
foreach (self::TEXT_MODEL_PRIORITY as $vendor) {
|
||||
$config = self::buildProviderConfig($setting, $vendor);
|
||||
if ($config) {
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建指定厂商的请求参数
|
||||
* @param array $setting
|
||||
* @param string $vendor
|
||||
* @return array|null
|
||||
*/
|
||||
protected static function buildProviderConfig(array $setting, string $vendor)
|
||||
{
|
||||
$key = trim((string)($setting[$vendor . '_key'] ?? ''));
|
||||
$baseUrl = trim((string)($setting[$vendor . '_base_url'] ?? ''));
|
||||
$agency = trim((string)($setting[$vendor . '_agency'] ?? ''));
|
||||
|
||||
switch ($vendor) {
|
||||
case 'openai':
|
||||
if ($key === '') {
|
||||
return null;
|
||||
}
|
||||
$baseUrl = $baseUrl ?: 'https://api.openai.com/v1';
|
||||
$model = self::resolveOpenAITextModel($setting);
|
||||
break;
|
||||
case 'ollama':
|
||||
if ($baseUrl === '') {
|
||||
return null;
|
||||
}
|
||||
if ($key === '') {
|
||||
$key = Base::strRandom(6);
|
||||
}
|
||||
$model = trim((string)($setting[$vendor . '_model'] ?? ''));
|
||||
break;
|
||||
case 'wenxin':
|
||||
$secret = trim((string)($setting['wenxin_secret'] ?? ''));
|
||||
if ($key === '' || $secret === '' || $baseUrl === '') {
|
||||
return null;
|
||||
}
|
||||
$key = $key . ':' . $secret;
|
||||
$model = trim((string)($setting[$vendor . '_model'] ?? ''));
|
||||
break;
|
||||
default:
|
||||
if ($key === '' || $baseUrl === '') {
|
||||
return null;
|
||||
}
|
||||
$model = trim((string)($setting[$vendor . '_model'] ?? ''));
|
||||
break;
|
||||
}
|
||||
|
||||
if ($model === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'vendor' => $vendor,
|
||||
'model' => $model,
|
||||
'api_key' => $key,
|
||||
'base_url' => rtrim($baseUrl, '/'),
|
||||
'agency' => $agency,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 OpenAI 文本模型
|
||||
* @param array $setting
|
||||
* @return string
|
||||
*/
|
||||
protected static function resolveOpenAITextModel(array $setting)
|
||||
{
|
||||
$models = Setting::AIBotModels2Array($setting['openai_models'] ?? '', true);
|
||||
if (in_array(self::OPENAI_DEFAULT_MODEL, $models, true)) {
|
||||
return self::OPENAI_DEFAULT_MODEL;
|
||||
}
|
||||
if (!empty($setting['openai_model'])) {
|
||||
return $setting['openai_model'];
|
||||
}
|
||||
return $models[0] ?? self::OPENAI_DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI 语音模型配置
|
||||
* @return array|null
|
||||
*/
|
||||
protected static function resolveOpenAIAudioProvider()
|
||||
{
|
||||
$setting = Base::setting('aibotSetting');
|
||||
if (!is_array($setting)) {
|
||||
$setting = [];
|
||||
}
|
||||
$key = trim((string)($setting['openai_key'] ?? ''));
|
||||
if ($key === '') {
|
||||
return null;
|
||||
}
|
||||
$baseUrl = trim((string)($setting['openai_base_url'] ?? ''));
|
||||
$baseUrl = $baseUrl ?: 'https://api.openai.com/v1';
|
||||
$agency = trim((string)($setting['openai_agency'] ?? ''));
|
||||
|
||||
return [
|
||||
'vendor' => 'openai',
|
||||
'model' => 'whisper-1',
|
||||
'api_key' => $key,
|
||||
'base_url' => rtrim($baseUrl, '/'),
|
||||
'agency' => $agency,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要附加 reasoning_effort 参数
|
||||
* @param array $provider
|
||||
* @return bool
|
||||
*/
|
||||
protected static function shouldSendReasoningEffort(array $provider): bool
|
||||
{
|
||||
if (($provider['vendor'] ?? '') !== 'openai') {
|
||||
return false;
|
||||
}
|
||||
$model = $provider['model'] ?? '';
|
||||
return str_starts_with($model, 'gpt-5');
|
||||
}
|
||||
}
|
||||
60
app/Module/Apps.php
Normal file
60
app/Module/Apps.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Services\RequestContext;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Apps
|
||||
{
|
||||
/**
|
||||
* 判断应用是否已安装
|
||||
*
|
||||
* @param string $appId 应用ID(名称)
|
||||
* @return bool 如果应用已安装返回 true,否则返回 false
|
||||
*/
|
||||
public static function isInstalled(string $appId): bool
|
||||
{
|
||||
if ($appId === 'appstore') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$key = 'app_installed_' . $appId;
|
||||
if (RequestContext::has($key)) {
|
||||
return RequestContext::get($key);
|
||||
}
|
||||
|
||||
$configFile = base_path('docker/appstore/config/' . $appId . '/config.yml');
|
||||
$installed = false;
|
||||
if (file_exists($configFile)) {
|
||||
$configData = Yaml::parseFile($configFile);
|
||||
$installed = $configData['status'] === 'installed';
|
||||
}
|
||||
|
||||
return RequestContext::save($key, $installed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断应用是否已安装,如果未安装则抛出异常
|
||||
* @param string $appId
|
||||
* @return void
|
||||
*/
|
||||
public static function isInstalledThrow(string $appId): void
|
||||
{
|
||||
if (!self::isInstalled($appId)) {
|
||||
$name = match ($appId) {
|
||||
'ai' => 'AI Assistant',
|
||||
'face' => 'Face check-in',
|
||||
'appstore' => 'AppStore',
|
||||
'approve' => 'Approval',
|
||||
'office' => 'OnlyOffice',
|
||||
'drawio' => 'Drawio',
|
||||
'minder' => 'Minder',
|
||||
'search' => 'ZincSearch',
|
||||
default => $appId,
|
||||
};
|
||||
throw new ApiException("应用「{$name}」未安装", [], 0, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ 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;
|
||||
@@ -351,7 +351,7 @@ class Base
|
||||
/**
|
||||
* 删除文件夹及文件夹下所有的文件
|
||||
* @param $dirName
|
||||
* @param bool $undeleteDir 不删除文件夹(只删除文件)
|
||||
* @param bool $undeleteDir 不删除文件夹本身(只删除文件夹里面的内容)
|
||||
*/
|
||||
public static function deleteDirAndFile($dirName, $undeleteDir = false)
|
||||
{
|
||||
@@ -802,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -827,12 +827,19 @@ class Base
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
try {
|
||||
$find = url('');
|
||||
} catch (\Throwable) {
|
||||
$find = self::getSchemeAndHost();
|
||||
if (empty($str)) {
|
||||
return $str;
|
||||
}
|
||||
return Base::leftDelete($str, $find . '/');
|
||||
$parsedUrl = parse_url($str);
|
||||
if (isset($parsedUrl['scheme']) && isset($parsedUrl['host'])) {
|
||||
$relativePath = $parsedUrl['path'] ?? '';
|
||||
$relativePath = ltrim($relativePath, '/');
|
||||
$absolutePath = public_path($relativePath);
|
||||
if (file_exists($absolutePath) || file_exists(Base::thumbRestore($absolutePath))) {
|
||||
return $relativePath;
|
||||
}
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1295,7 +1302,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)
|
||||
@@ -1397,11 +1404,13 @@ class Base
|
||||
*/
|
||||
public static function ajaxError($msg, $data = [], $ret = 0, $abortCode = 404)
|
||||
{
|
||||
if (Request::header('Content-Type') === 'application/json') {
|
||||
return Base::retError($msg, $data, $ret);
|
||||
} else {
|
||||
abort($abortCode, $msg);
|
||||
if (Request::header('Content-Type') !== 'application/json') {
|
||||
$translateMsg = Doo::translate($msg);
|
||||
abort($abortCode, $translateMsg, [
|
||||
'X-Error-Message-Base64' => base64_encode($translateMsg),
|
||||
]);
|
||||
}
|
||||
return Base::retError($msg, $data, $ret);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1476,14 +1485,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
|
||||
@@ -1829,12 +1860,22 @@ class Base
|
||||
* 获取每页数量
|
||||
* @param $max
|
||||
* @param $default
|
||||
* @param string $inputName
|
||||
* @param string|array $inputName
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getPaginate($max, $default, $inputName = 'pagesize')
|
||||
public static function getPaginate($max, $default, $inputName = ['pagesize', 'take'])
|
||||
{
|
||||
return Min(Max(Base::nullShow(Request::input($inputName), $default), 1), $max);
|
||||
$value = null;
|
||||
if (!is_array($inputName)) {
|
||||
$inputName = [$inputName];
|
||||
}
|
||||
foreach ($inputName as $name) {
|
||||
if (Request::exists($name)) {
|
||||
$value = Request::input($name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Min(Max(Base::nullShow($value, $default), 1), $max);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1850,18 +1891,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);
|
||||
@@ -1872,8 +1913,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)
|
||||
{
|
||||
@@ -1884,8 +1940,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'];
|
||||
@@ -1896,21 +1952,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, //图片高度
|
||||
@@ -1941,10 +1997,10 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1978,12 +2034,14 @@ class Base
|
||||
file=>Request::file,
|
||||
path=>文件路径,
|
||||
fileName=>文件名称,
|
||||
saveName=>保存文件名称,
|
||||
scale=>[压缩原图宽,高, 压缩方式],
|
||||
size=>限制大小KB,
|
||||
autoThumb=>false不要自动生成缩略图,
|
||||
chmod=>权限(默认0644),
|
||||
quality=>压缩图片质量(默认:0不压缩),
|
||||
convertVideo=>转换视频格式(默认false) ,
|
||||
compressVideo=>压缩视频(默认false,如果转换就不压缩) ,
|
||||
]
|
||||
* @return array [
|
||||
name=>原文件名,
|
||||
@@ -2069,10 +2127,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 +2141,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"
|
||||
@@ -2141,6 +2199,7 @@ class Base
|
||||
}
|
||||
@shell_exec($command);
|
||||
if (file_exists($output) && filesize($output) > 0) {
|
||||
// 压缩后的文件正常
|
||||
@unlink($array['file']);
|
||||
$array = array_merge($array, [
|
||||
"name" => Base::rightReplace($array['name'], ".{$array['ext']}", '.mp4'),
|
||||
@@ -2151,6 +2210,27 @@ class Base
|
||||
"ext" => 'mp4',
|
||||
]);
|
||||
}
|
||||
$param['compressVideo'] = false; // 如果转换就不压缩
|
||||
}
|
||||
if ($param['compressVideo'] && $array['ext'] == 'mp4') {
|
||||
// 压缩视频
|
||||
$output = $array['file'] . '_compress';
|
||||
$command = sprintf("ffmpeg -y -i %s -c:v libx264 -crf 28 -preset medium -c:a aac -b:a 96k %s 2>&1", escapeshellarg($array['file']), escapeshellarg($output));
|
||||
@shell_exec($command);
|
||||
if (file_exists($output) && filesize($output) > 0) {
|
||||
// 压缩后的文件正常
|
||||
if (filesize($output) < filesize($array['file'])) {
|
||||
// 小于原文件
|
||||
@unlink($array['file']);
|
||||
$array = array_merge($array, [
|
||||
"size" => Base::twoFloat(filesize($output) / 1024, true),
|
||||
"file" => $output,
|
||||
]);
|
||||
} else {
|
||||
// 大于原文件
|
||||
@unlink($output);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (in_array($array['ext'], ['mov', 'webm', 'mp4'])) {
|
||||
// 视频尺寸
|
||||
@@ -2185,10 +2265,10 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2513,22 +2593,37 @@ class Base
|
||||
/**
|
||||
* 中文转拼音
|
||||
* @param $str
|
||||
* @param $delim
|
||||
* @return string
|
||||
*/
|
||||
public static function cn2pinyin($str)
|
||||
public static function cn2pinyin($str, $delim = '')
|
||||
{
|
||||
if (empty($str)) {
|
||||
return '';
|
||||
}
|
||||
if (!preg_match("/^[a-zA-Z0-9_.]+$/", $str)) {
|
||||
$str = Cache::rememberForever("cn2pinyin:" . md5($str), function() use ($str) {
|
||||
$str = Cache::rememberForever("cn2pinyin:" . md5($str . '_' . $delim), function () use ($delim, $str) {
|
||||
$pinyin = new Pinyin();
|
||||
return $pinyin->permalink($str, '');
|
||||
return $pinyin->permalink($str, $delim);
|
||||
});
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰转下划线
|
||||
* @param $str
|
||||
* @return string
|
||||
*/
|
||||
public static function camel2snake($str)
|
||||
{
|
||||
if (empty($str)) {
|
||||
return '';
|
||||
}
|
||||
$str = preg_replace('/([a-z])([A-Z])/', '$1_$2', $str);
|
||||
return strtolower($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存数据
|
||||
* @param $name
|
||||
@@ -2955,11 +3050,84 @@ class Base
|
||||
*/
|
||||
public static function markdown2html($markdown)
|
||||
{
|
||||
$converter = new CommonMarkConverter();
|
||||
try {
|
||||
return $converter->convert($markdown);
|
||||
} catch (CommonMarkException $e) {
|
||||
$converter = new CommonMarkConverter();
|
||||
return $converter->convert($markdown)->getContent();
|
||||
} catch (\League\CommonMark\Exception\CommonMarkException $e) {
|
||||
return $markdown;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* html 转 MD(markdown)
|
||||
* @param $html
|
||||
* @param array $options
|
||||
* @return mixed|string
|
||||
*/
|
||||
public static function html2markdown($html, $options = [])
|
||||
{
|
||||
try {
|
||||
$converter = new HtmlConverter($options);
|
||||
return $converter->convert($html);
|
||||
} catch (\Exception) {
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时读取 .env 配置(不受配置缓存影响)
|
||||
* @param string $key 配置键名
|
||||
* @param mixed $default 默认值
|
||||
* @return mixed
|
||||
*/
|
||||
public static function liveEnv($key, $default = null)
|
||||
{
|
||||
$envFile = base_path('.env');
|
||||
if (!file_exists($envFile)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$envContent = file_get_contents($envFile);
|
||||
$lines = explode("\n", $envContent);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// 跳过注释和空行
|
||||
if (empty($line) || str_starts_with($line, '#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析 KEY=VALUE
|
||||
if (str_contains($line, '=')) {
|
||||
[$envKey, $envValue] = explode('=', $line, 2);
|
||||
$envKey = trim($envKey);
|
||||
|
||||
if ($envKey === $key) {
|
||||
$envValue = trim($envValue);
|
||||
|
||||
// 移除引号
|
||||
if (preg_match('/^(["\'])(.*)\1$/', $envValue, $matches)) {
|
||||
$envValue = $matches[2];
|
||||
}
|
||||
|
||||
// 处理布尔值
|
||||
$lowerValue = strtolower($envValue);
|
||||
if ($lowerValue === 'true') {
|
||||
return true;
|
||||
}
|
||||
if ($lowerValue === 'false') {
|
||||
return false;
|
||||
}
|
||||
if ($lowerValue === 'null' || $lowerValue === '(null)') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $envValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
83
app/Module/ClientContext.php
Normal file
83
app/Module/ClientContext.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
/**
|
||||
* 客户端上下文
|
||||
*/
|
||||
class ClientContext
|
||||
{
|
||||
public array $context = [];
|
||||
public float $createdAt = 0;
|
||||
public float $updatedAt = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = microtime(true);
|
||||
$this->updatedAt = microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置上下文
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function set(string $key, mixed $value): void
|
||||
{
|
||||
$this->context[$key] = $value;
|
||||
$this->updatedAt = microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置上下文
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function setMultiple(array $data): void
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
$this->context[$key] = $value;
|
||||
}
|
||||
$this->updatedAt = microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上下文
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->context[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断上下文是否存在
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return isset($this->context[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新上下文
|
||||
* @return void
|
||||
*/
|
||||
public function update(): void
|
||||
{
|
||||
$this->updatedAt = microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除上下文
|
||||
* @return void
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->context = [];
|
||||
}
|
||||
}
|
||||
@@ -2,72 +2,40 @@
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\User;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use FFI;
|
||||
use App\Module\Interface\DooSo;
|
||||
use App\Services\RequestContext;
|
||||
|
||||
class Doo
|
||||
{
|
||||
private static $doo;
|
||||
private static $userLanguage = "";
|
||||
private const DOO_INSTANCE = 'doo_instance';
|
||||
private const DOO_LANGUAGE = 'doo_language';
|
||||
|
||||
/**
|
||||
* char转为字符串
|
||||
* @param $text
|
||||
* @return string
|
||||
*/
|
||||
private static function string($text): string
|
||||
{
|
||||
return FFI::string($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 装载
|
||||
* 加载Doo实例
|
||||
* - 如果已经存在,则直接返回
|
||||
* - 否则,创建一个新的FFI实例,并初始化
|
||||
* @param $token
|
||||
* @param $language
|
||||
* @return DooSo
|
||||
*/
|
||||
public static function load($token = null, $language = null)
|
||||
public static function load($token = null, $language = null): DooSo
|
||||
{
|
||||
self::$doo = FFI::cdef(<<<EOF
|
||||
void initialize(char* work, char* token, char* lang);
|
||||
char* license();
|
||||
char* licenseDecode(char* license);
|
||||
char* licenseSave(char* license);
|
||||
int userId();
|
||||
char* userExpiredAt();
|
||||
char* userEmail();
|
||||
char* userEncrypt();
|
||||
char* userToken();
|
||||
char* userCreate(char* email, char* password);
|
||||
char* tokenEncode(int userid, char* email, char* encrypt, int days);
|
||||
char* tokenDecode(char* val);
|
||||
char* translate(char* val, char* val);
|
||||
char* md5s(char* text, char* password);
|
||||
char* macs();
|
||||
char* dooSN();
|
||||
char* pgpGenerateKeyPair(char* name, char* email, char* passphrase);
|
||||
char* pgpEncrypt(char* plainText, char* publicKey);
|
||||
char* pgpDecrypt(char* cipherText, char* privateKey, char* passphrase);
|
||||
EOF, "/usr/lib/doo/doo.so");
|
||||
$token = $token ?: Base::token();
|
||||
$language = $language ?: Base::headerOrInput('language');
|
||||
self::$doo->initialize("/var/www", $token, $language);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实例
|
||||
* @param $token
|
||||
* @param $language
|
||||
* @return mixed
|
||||
*/
|
||||
public static function doo($token = null, $language = null)
|
||||
{
|
||||
if (self::$doo == null) {
|
||||
self::load($token, $language);
|
||||
if (RequestContext::has(self::DOO_INSTANCE)) {
|
||||
return RequestContext::get(self::DOO_INSTANCE);
|
||||
}
|
||||
return self::$doo;
|
||||
|
||||
$request = request();
|
||||
if ($request && method_exists($request, 'header')) {
|
||||
$token = $token ?: Base::token();
|
||||
$language = $language ?: Base::headerOrInput('language');
|
||||
}
|
||||
$instance = new DooSo($token, $language);
|
||||
|
||||
RequestContext::set(self::DOO_INSTANCE, $instance);
|
||||
RequestContext::set(self::DOO_LANGUAGE, $language);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,41 +44,7 @@ class Doo
|
||||
*/
|
||||
public static function license(): array
|
||||
{
|
||||
$array = Base::json2array(self::string(self::doo()->license()));
|
||||
|
||||
$ips = explode(",", $array['ip']);
|
||||
$array['ip'] = [];
|
||||
foreach ($ips as $ip) {
|
||||
if (Base::is_ipv4($ip)) {
|
||||
$array['ip'][] = $ip;
|
||||
}
|
||||
}
|
||||
|
||||
$domains = explode(",", $array['domain']);
|
||||
$array['domain'] = [];
|
||||
foreach ($domains as $domain) {
|
||||
if (Base::is_domain($domain)) {
|
||||
$array['domain'][] = $domain;
|
||||
}
|
||||
}
|
||||
|
||||
$macs = explode(",", $array['mac']);
|
||||
$array['mac'] = [];
|
||||
foreach ($macs as $mac) {
|
||||
if (Base::isMac($mac)) {
|
||||
$array['mac'][] = $mac;
|
||||
}
|
||||
}
|
||||
|
||||
$emails = explode(",", $array['email']);
|
||||
$array['email'] = [];
|
||||
foreach ($emails as $email) {
|
||||
if (Base::isEmail($email)) {
|
||||
$array['email'][] = $email;
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
return self::load()->license();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,26 +72,13 @@ class Doo
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析License
|
||||
* @param $license
|
||||
* @return array
|
||||
*/
|
||||
public static function licenseDecode($license): array
|
||||
{
|
||||
return Base::json2array(self::string(self::doo()->licenseDecode($license)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存License
|
||||
* @param $license
|
||||
*/
|
||||
public static function licenseSave($license): void
|
||||
{
|
||||
$res = self::string(self::doo()->licenseSave($license));
|
||||
if ($res != 'success') {
|
||||
throw new ApiException($res ?: 'LICENSE 保存失败');
|
||||
}
|
||||
self::load()->licenseSave($license);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +87,7 @@ class Doo
|
||||
*/
|
||||
public static function userId(): int
|
||||
{
|
||||
return intval(self::doo()->userId());
|
||||
return self::load()->userId();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,18 +96,16 @@ class Doo
|
||||
*/
|
||||
public static function userExpired(): bool
|
||||
{
|
||||
$expiredAt = self::userExpiredAt();
|
||||
return $expiredAt && Carbon::parse($expiredAt)->isBefore(Carbon::now());
|
||||
return self::load()->userExpired();
|
||||
}
|
||||
|
||||
/**
|
||||
* token过期时间(来自请求的token)
|
||||
* @return string
|
||||
* @return string|null
|
||||
*/
|
||||
public static function userExpiredAt(): string
|
||||
public static function userExpiredAt(): ?string
|
||||
{
|
||||
$expiredAt = self::string(self::doo()->userExpiredAt());
|
||||
return $expiredAt === 'forever' ? '' : $expiredAt;
|
||||
return self::load()->userExpiredAt();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,7 +114,7 @@ class Doo
|
||||
*/
|
||||
public static function userEmail(): string
|
||||
{
|
||||
return self::string(self::doo()->userEmail());
|
||||
return self::load()->userEmail();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,7 +123,7 @@ class Doo
|
||||
*/
|
||||
public static function userEncrypt(): string
|
||||
{
|
||||
return self::string(self::doo()->userEncrypt());
|
||||
return self::load()->userEncrypt();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,7 +132,7 @@ class Doo
|
||||
*/
|
||||
public static function userToken(): string
|
||||
{
|
||||
return self::string(self::doo()->userToken());
|
||||
return self::load()->userToken();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,23 +143,7 @@ class Doo
|
||||
*/
|
||||
public static function userCreate($email, $password): User|null
|
||||
{
|
||||
$data = Base::json2array(self::string(self::doo()->userCreate($email, $password)));
|
||||
if (Base::isError($data)) {
|
||||
throw new ApiException($data['msg'] ?: '注册失败');
|
||||
}
|
||||
if (\DB::transactionLevel() > 0) {
|
||||
try {
|
||||
\DB::commit();
|
||||
\DB::beginTransaction();
|
||||
} catch (\Throwable) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
$user = User::whereEmail($email)->first();
|
||||
if (empty($user)) {
|
||||
throw new ApiException('注册失败');
|
||||
}
|
||||
return $user;
|
||||
return self::load()->userCreate($email, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,7 +156,7 @@ class Doo
|
||||
*/
|
||||
public static function tokenEncode($userid, $email, $encrypt, int $days = 15): string
|
||||
{
|
||||
return self::string(self::doo()->tokenEncode($userid, $email, $encrypt, $days));
|
||||
return self::load()->tokenEncode($userid, $email, $encrypt, $days);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,38 +166,42 @@ class Doo
|
||||
*/
|
||||
public static function tokenDecode($token): array
|
||||
{
|
||||
return Base::json2array(self::string(self::doo()->tokenDecode($token)));
|
||||
return self::load()->tokenDecode($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译
|
||||
* @param $text
|
||||
* @param string $lang
|
||||
* @param ?string $lang
|
||||
* @return string
|
||||
*/
|
||||
public static function translate($text, string $lang = ""): string
|
||||
public static function translate($text, ?string $lang = ""): string
|
||||
{
|
||||
return self::string(self::doo()->translate($text, $lang ?: self::$userLanguage));
|
||||
if (empty($lang)) {
|
||||
$lang = RequestContext::get(self::DOO_LANGUAGE);
|
||||
}
|
||||
return self::load()->translate($text, $lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置语言
|
||||
* @param string|integer $lang 语言 或 会员ID
|
||||
* @param int|string $lang 语言 或 会员ID
|
||||
* @return void
|
||||
*/
|
||||
public static function setLanguage($lang) {
|
||||
public static function setLanguage(int|string $lang): void
|
||||
{
|
||||
if (Base::isNumber($lang)) {
|
||||
$lang = User::find(intval($lang))?->lang ?: "";
|
||||
}
|
||||
self::$userLanguage = $lang;
|
||||
RequestContext::set(self::DOO_LANGUAGE, $lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取语言列表 或 语言名称
|
||||
* @param string|false $lang
|
||||
* @param bool|string $lang
|
||||
* @return string|string[]
|
||||
*/
|
||||
public static function getLanguages($lang = false)
|
||||
public static function getLanguages(bool|string $lang = false): array|string
|
||||
{
|
||||
$array = [
|
||||
"zh" => "简体中文",
|
||||
@@ -331,7 +238,7 @@ class Doo
|
||||
*/
|
||||
public static function md5s($text, string $password = ""): string
|
||||
{
|
||||
return self::string(self::doo()->md5s($text, $password));
|
||||
return self::load()->md5s($text, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,14 +247,7 @@ class Doo
|
||||
*/
|
||||
public static function macs(): array
|
||||
{
|
||||
$macs = explode(",", self::string(self::doo()->macs()));
|
||||
$array = [];
|
||||
foreach ($macs as $mac) {
|
||||
if (Base::isMac($mac)) {
|
||||
$array[] = $mac;
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
return self::load()->macs();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,7 +256,16 @@ class Doo
|
||||
*/
|
||||
public static function dooSN(): string
|
||||
{
|
||||
return self::string(self::doo()->dooSN());
|
||||
return self::load()->dooSN();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前版本
|
||||
* @return string
|
||||
*/
|
||||
public static function dooVersion(): string
|
||||
{
|
||||
return self::load()->dooVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -368,7 +277,7 @@ class Doo
|
||||
*/
|
||||
public static function pgpGenerateKeyPair($name, $email, string $passphrase = ""): array
|
||||
{
|
||||
return Base::json2array(self::string(self::doo()->pgpGenerateKeyPair($name, $email, $passphrase)));
|
||||
return self::load()->pgpGenerateKeyPair($name, $email, $passphrase);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -379,11 +288,7 @@ class Doo
|
||||
*/
|
||||
public static function pgpEncrypt($plaintext, $publicKey): string
|
||||
{
|
||||
if (strlen($publicKey) < 50) {
|
||||
$keyCache = Base::json2array(Cache::get("KeyPair::" . $publicKey));
|
||||
$publicKey = $keyCache['public_key'];
|
||||
}
|
||||
return self::string(self::doo()->pgpEncrypt($plaintext, $publicKey));
|
||||
return self::load()->pgpEncrypt($plaintext, $publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -395,12 +300,7 @@ class Doo
|
||||
*/
|
||||
public static function pgpDecrypt($encryptedText, $privateKey, $passphrase = null): string
|
||||
{
|
||||
if (strlen($privateKey) < 50) {
|
||||
$keyCache = Base::json2array(Cache::get("KeyPair::" . $privateKey));
|
||||
$privateKey = $keyCache['private_key'];
|
||||
$passphrase = $keyCache['passphrase'];
|
||||
}
|
||||
return self::string(self::doo()->pgpDecrypt($encryptedText, $privateKey, $passphrase));
|
||||
return self::load()->pgpDecrypt($encryptedText, $privateKey, $passphrase);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -411,9 +311,7 @@ class Doo
|
||||
*/
|
||||
public static function pgpEncryptApi($plaintext, $publicKey): string
|
||||
{
|
||||
$content = Base::array2json($plaintext);
|
||||
$content = self::pgpEncrypt($content, $publicKey);
|
||||
return preg_replace("/\s*-----(BEGIN|END) PGP MESSAGE-----\s*/i", "", $content);
|
||||
return self::load()->pgpEncryptApi($plaintext, $publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,9 +323,7 @@ class Doo
|
||||
*/
|
||||
public static function pgpDecryptApi($encryptedText, $privateKey, $passphrase = null): array
|
||||
{
|
||||
$content = "-----BEGIN PGP MESSAGE-----\n\n" . $encryptedText . "\n-----END PGP MESSAGE-----";
|
||||
$content = self::pgpDecrypt($content, $privateKey, $passphrase);
|
||||
return Base::json2array($content);
|
||||
return self::load()->pgpDecryptApi($encryptedText, $privateKey, $passphrase);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -437,24 +333,7 @@ class Doo
|
||||
*/
|
||||
public static function pgpParseStr($string): array
|
||||
{
|
||||
$array = [
|
||||
'encrypt_type' => '',
|
||||
'encrypt_id' => '',
|
||||
'client_type' => '',
|
||||
'client_key' => '',
|
||||
];
|
||||
$string = str_replace(";", "&", $string);
|
||||
parse_str($string, $params);
|
||||
foreach ($params as $key => $value) {
|
||||
$key = strtolower(trim($key));
|
||||
if ($key) {
|
||||
$array[$key] = trim($value);
|
||||
}
|
||||
}
|
||||
if ($array['client_type'] === 'pgp' && $array['client_key']) {
|
||||
$array['client_key'] = self::pgpPublicFormat($array['client_key']);
|
||||
}
|
||||
return $array;
|
||||
return self::load()->pgpParseStr($string);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -464,10 +343,6 @@ class Doo
|
||||
*/
|
||||
public static function pgpPublicFormat($key): string
|
||||
{
|
||||
$key = str_replace(["-", "_", "$"], ["+", "/", "\n"], $key);
|
||||
if (!str_contains($key, '-----BEGIN PGP PUBLIC KEY BLOCK-----')) {
|
||||
$key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n" . $key . "\n-----END PGP PUBLIC KEY BLOCK-----";
|
||||
}
|
||||
return $key;
|
||||
return self::load()->pgpPublicFormat($key);
|
||||
}
|
||||
}
|
||||
|
||||
37
app/Module/Down.php
Normal file
37
app/Module/Down.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use Request;
|
||||
use Cache;
|
||||
|
||||
class Down
|
||||
{
|
||||
/**
|
||||
* @param $data
|
||||
* @param null $ttl
|
||||
* @return string
|
||||
*/
|
||||
public static function cache_encode($data, $ttl = null): string
|
||||
{
|
||||
$base64 = base64_encode(Base::array2string($data));
|
||||
$key = md5($base64);
|
||||
Cache::put("down::{$key}", $base64, $ttl ?: now()->addHour());
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $inputName
|
||||
* @return array
|
||||
*/
|
||||
public static function cache_decode(?string $inputName = 'key'): array
|
||||
{
|
||||
$key = Request::input($inputName);
|
||||
$base64 = Cache::get("down::{$key}");
|
||||
if (empty($base64)) {
|
||||
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 403);
|
||||
}
|
||||
//
|
||||
return Base::string2array(base64_decode($base64));
|
||||
}
|
||||
}
|
||||
@@ -4,266 +4,12 @@ namespace App\Module;
|
||||
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* 外网资源请求
|
||||
*/
|
||||
class Extranet
|
||||
{
|
||||
/**
|
||||
* 通过 openAI 语音转文字
|
||||
* @param string $filePath
|
||||
* @return array
|
||||
*/
|
||||
public static function openAItranscriptions($filePath)
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
return Base::retError("语音文件不存在");
|
||||
}
|
||||
$systemSetting = Base::setting('system');
|
||||
$aibotSetting = Base::setting('aibotSetting');
|
||||
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;
|
||||
}
|
||||
}
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', [
|
||||
'file' => new \CURLFile($filePath),
|
||||
'model' => 'whisper-1'
|
||||
], $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']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 openAI 翻译
|
||||
* @param $text
|
||||
* @param $targetLanguage
|
||||
* @return array
|
||||
*/
|
||||
public static function openAItranslations($text, $targetLanguage)
|
||||
{
|
||||
$systemSetting = Base::setting('system');
|
||||
$aibotSetting = Base::setting('aibotSetting');
|
||||
if ($systemSetting['translation'] !== 'open' || empty($aibotSetting['openai_key'])) {
|
||||
return Base::retError("翻译功能未开启");
|
||||
}
|
||||
$extra = [
|
||||
'Content-Type' => 'application/json',
|
||||
'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;
|
||||
}
|
||||
}
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([
|
||||
"model" => "gpt-3.5-turbo",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => "你是一个专业的翻译器,翻译的结果尽量符合“项目任务管理系统”的使用,并且翻译的结果不用额外添加换行尽量保持原格式,将提供的文本翻译成“{$targetLanguage}”语言。"
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => $text
|
||||
]
|
||||
]
|
||||
]), $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('/^\"|\"$/', '', $result);
|
||||
if (empty($result)) {
|
||||
return Base::retError("翻译失败", $result);
|
||||
}
|
||||
return Base::retSuccess("success", $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'];
|
||||
if (str_contains($aibotSetting['openai_agency'], 'socks')) {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_SOCKS5;
|
||||
} else {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_HTTP;
|
||||
}
|
||||
}
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([
|
||||
"model" => "gpt-3.5-turbo",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => "你是一个专业的标题生成器,擅长为对话生成简洁的标题,请将提供的文本生成一个标题。"
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => $text
|
||||
]
|
||||
]
|
||||
]), $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('/^\"|\"$/', '', $result);
|
||||
if (empty($result)) {
|
||||
return Base::retError("生成失败", $result);
|
||||
}
|
||||
return Base::retSuccess("success", $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取IP地址经纬度
|
||||
* @param string $ip
|
||||
* @return array
|
||||
*/
|
||||
public static function getIpGcj02(string $ip = ''): array
|
||||
{
|
||||
if (empty($ip)) {
|
||||
$ip = Base::getIp();
|
||||
}
|
||||
$cacheKey = "getIpPoint::" . md5($ip);
|
||||
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
|
||||
return Ihttp::ihttp_request("https://www.ifreesite.com/ipaddress/address.php?q=" . $ip, [], [], 12);
|
||||
});
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget($cacheKey);
|
||||
return $result;
|
||||
}
|
||||
$data = $result['data'];
|
||||
$lastPos = strrpos($data, ',');
|
||||
$long = floatval(Base::getMiddle(substr($data, $lastPos + 1), null, ')'));
|
||||
$lat = floatval(Base::getMiddle(substr($data, strrpos(substr($data, 0, $lastPos), ',') + 1), null, ','));
|
||||
return Base::retSuccess("success", [
|
||||
'long' => $long,
|
||||
'lat' => $lat,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 百度接口:根据ip获取经纬度
|
||||
* @param string $ip
|
||||
* @return array
|
||||
*/
|
||||
public static function getIpGcj02ByBaidu(string $ip = ''): array
|
||||
{
|
||||
if (empty($ip)) {
|
||||
$ip = Base::getIp();
|
||||
}
|
||||
|
||||
$cacheKey = "getIpPoint::" . md5($ip);
|
||||
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
|
||||
$ak = Config::get('app.baidu_app_key');
|
||||
$url = 'http://api.map.baidu.com/location/ip?ak=' . $ak . '&ip=' . $ip . '&coor=bd09ll';
|
||||
return Ihttp::ihttp_request($url, [], [], 12);
|
||||
});
|
||||
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget($cacheKey);
|
||||
return $result;
|
||||
}
|
||||
$data = json_decode($result['data'], true);
|
||||
|
||||
// x坐标纬度, y坐标经度
|
||||
$long = Arr::get($data, 'content.point.x');
|
||||
$lat = Arr::get($data, 'content.point.y');
|
||||
return Base::retSuccess("success", [
|
||||
'long' => $long,
|
||||
'lat' => $lat,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取IP地址详情
|
||||
* @param string $ip
|
||||
* @return array
|
||||
*/
|
||||
public static function getIpInfo(string $ip = ''): array
|
||||
{
|
||||
if (empty($ip)) {
|
||||
$ip = Base::getIp();
|
||||
}
|
||||
$cacheKey = "getIpInfo::" . md5($ip);
|
||||
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
|
||||
return Ihttp::ihttp_request("http://ip.taobao.com/service/getIpInfo.php?accessKey=alibaba-inc&ip=" . $ip, [], [], 12);
|
||||
});
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget($cacheKey);
|
||||
return $result;
|
||||
}
|
||||
$data = json_decode($result['data'], true);
|
||||
if (!is_array($data) || intval($data['code']) != 0) {
|
||||
Cache::forget($cacheKey);
|
||||
return Base::retError("error ip: -1");
|
||||
}
|
||||
$data = $data['data'];
|
||||
if (!is_array($data) || !isset($data['country'])) {
|
||||
return Base::retError("error ip: -2");
|
||||
}
|
||||
$data['text'] = $data['country'];
|
||||
$data['textSmall'] = $data['country'];
|
||||
if ($data['region'] && $data['region'] != $data['country'] && $data['region'] != "XX") {
|
||||
$data['text'] .= " " . $data['region'];
|
||||
$data['textSmall'] = $data['region'];
|
||||
}
|
||||
if ($data['city'] && $data['city'] != $data['region'] && $data['city'] != "XX") {
|
||||
$data['text'] .= " " . $data['city'];
|
||||
$data['textSmall'] .= " " . $data['city'];
|
||||
}
|
||||
if ($data['county'] && $data['county'] != $data['city'] && $data['county'] != "XX") {
|
||||
$data['text'] .= " " . $data['county'];
|
||||
$data['textSmall'] .= " " . $data['county'];
|
||||
}
|
||||
return Base::retSuccess("success", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否工作日
|
||||
* @param string $Ymd 年月日(如:20220102)
|
||||
@@ -319,125 +65,6 @@ class Extranet
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机笑话接口
|
||||
* @return string
|
||||
*/
|
||||
public static function randJoke(): string
|
||||
{
|
||||
$data = self::curl("https://hmajax.itheima.net/api/randjoke");
|
||||
$data = Base::json2array($data);
|
||||
if ($data['message'] === '获取成功' && $text = trim($data['data'])) {
|
||||
return $text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 心灵鸡汤
|
||||
* @return string
|
||||
*/
|
||||
public static function soups(): string
|
||||
{
|
||||
$data = self::curl("https://hmajax.itheima.net/api/ambition");
|
||||
$data = Base::json2array($data);
|
||||
if ($data['message'] === '获取成功' && $text = trim($data['data'])) {
|
||||
return $text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 签到机器人网络内容
|
||||
* @param $type
|
||||
* @return string
|
||||
*/
|
||||
public static function checkinBotQuickMsg($type): string
|
||||
{
|
||||
$text = "维护中...";
|
||||
switch ($type) {
|
||||
case "it":
|
||||
$data = self::curl('http://vvhan.api.hitosea.com/api/hotlist?type=itNews', 3600);
|
||||
if ($data = Base::json2array($data)) {
|
||||
$i = 1;
|
||||
$array = array_map(function ($item) use (&$i) {
|
||||
if ($item['title'] && $item['desc']) {
|
||||
return "<p>" . ($i++) . ". <strong><a href='{$item['mobilUrl']}' target='_blank'>{$item['title']}</a></strong></p><p>{$item['desc']}</p>";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, $data['data']);
|
||||
$array = array_values(array_filter($array));
|
||||
if ($array) {
|
||||
array_unshift($array, "<p><strong>{$data['title']}</strong>({$data['update_time']})</p>");
|
||||
$text = implode("<p> </p>", $array);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "36ke":
|
||||
$data = self::curl('http://vvhan.api.hitosea.com/api/hotlist?type=36Ke', 3600);
|
||||
if ($data = Base::json2array($data)) {
|
||||
$i = 1;
|
||||
$array = array_map(function ($item) use (&$i) {
|
||||
if ($item['title'] && $item['desc']) {
|
||||
return "<p>" . ($i++) . ". <strong><a href='{$item['mobilUrl']}' target='_blank'>{$item['title']}</a></strong></p><p>{$item['desc']}</p>";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, $data['data']);
|
||||
$array = array_values(array_filter($array));
|
||||
if ($array) {
|
||||
array_unshift($array, "<p><strong>{$data['title']}</strong>({$data['update_time']})</p>");
|
||||
$text = implode("<p> </p>", $array);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "60s":
|
||||
$data = self::curl('http://vvhan.api.hitosea.com/api/60s?type=json', 3600);
|
||||
if ($data = Base::json2array($data)) {
|
||||
$i = 1;
|
||||
$array = array_map(function ($item) use (&$i) {
|
||||
if ($item) {
|
||||
return "<p>" . ($i++) . ". {$item}</p>";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, $data['data']);
|
||||
$array = array_values(array_filter($array));
|
||||
if ($array) {
|
||||
array_unshift($array, "<p><strong>{$data['name']}</strong>({$data['time'][0]})</p>");
|
||||
$text = implode("<p> </p>", $array);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "joke":
|
||||
$text = "笑话被掏空";
|
||||
$data = self::curl('http://vvhan.api.hitosea.com/api/joke?type=json', 5);
|
||||
if ($data = Base::json2array($data)) {
|
||||
if ($data = trim($data['joke'])) {
|
||||
$text = "开心笑话:{$data}";
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "soup":
|
||||
$text = "鸡汤分完了";
|
||||
$data = self::curl('https://api.ayfre.com/jt/?type=bot', 5);
|
||||
if ($data) {
|
||||
$text = "心灵鸡汤:{$data}";
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$text = "";
|
||||
break;
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取搜狗表情包
|
||||
* @param $keyword
|
||||
|
||||
412
app/Module/Interface/DooSo.php
Normal file
412
app/Module/Interface/DooSo.php
Normal file
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\Interface;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Models\User;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
use FFI;
|
||||
use FFI\CData;
|
||||
use FFI\Exception;
|
||||
use Throwable;
|
||||
|
||||
class DooSo
|
||||
{
|
||||
private mixed $so;
|
||||
|
||||
public function __construct($token = null, $language = null)
|
||||
{
|
||||
$this->so = FFI::cdef(<<<EOF
|
||||
void initialize(char* work, char* token, char* lang);
|
||||
char* license();
|
||||
char* licenseDecode(char* license);
|
||||
char* licenseSave(char* license);
|
||||
int userId();
|
||||
char* userExpiredAt();
|
||||
char* userEmail();
|
||||
char* userEncrypt();
|
||||
char* userToken();
|
||||
char* userCreate(char* email, char* password);
|
||||
char* tokenEncode(int userid, char* email, char* encrypt, int days);
|
||||
char* tokenDecode(char* val);
|
||||
char* translate(char* val, char* val);
|
||||
char* md5s(char* text, char* password);
|
||||
char* macs();
|
||||
char* dooSN();
|
||||
char* version();
|
||||
char* pgpGenerateKeyPair(char* name, char* email, char* passphrase);
|
||||
char* pgpEncrypt(char* plainText, char* publicKey);
|
||||
char* pgpDecrypt(char* cipherText, char* privateKey, char* passphrase);
|
||||
EOF, "/usr/lib/doo/doo.so");
|
||||
$this->so->initialize("/var/www", $token, $language);
|
||||
return $this->so;
|
||||
}
|
||||
|
||||
/**
|
||||
* char转为字符串
|
||||
* @param $text
|
||||
* @return string
|
||||
*/
|
||||
private static function string($text): string
|
||||
{
|
||||
if (!($text instanceof CData)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
return FFI::string($text);
|
||||
} catch (Exception) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* License
|
||||
* @return array
|
||||
*/
|
||||
public function license(): array
|
||||
{
|
||||
$array = Base::json2array(self::string($this->so->license()));
|
||||
|
||||
$ips = explode(",", $array['ip']);
|
||||
$array['ip'] = [];
|
||||
foreach ($ips as $ip) {
|
||||
if (Base::is_ipv4($ip)) {
|
||||
$array['ip'][] = $ip;
|
||||
}
|
||||
}
|
||||
|
||||
$domains = explode(",", $array['domain']);
|
||||
$array['domain'] = [];
|
||||
foreach ($domains as $domain) {
|
||||
if (Base::is_domain($domain)) {
|
||||
$array['domain'][] = $domain;
|
||||
}
|
||||
}
|
||||
|
||||
$macs = explode(",", $array['mac']);
|
||||
$array['mac'] = [];
|
||||
foreach ($macs as $mac) {
|
||||
if (Base::isMac($mac)) {
|
||||
$array['mac'][] = $mac;
|
||||
}
|
||||
}
|
||||
|
||||
$emails = explode(",", $array['email']);
|
||||
$array['email'] = [];
|
||||
foreach ($emails as $email) {
|
||||
if (Base::isEmail($email)) {
|
||||
$array['email'][] = $email;
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析License
|
||||
* @param $license
|
||||
* @return array
|
||||
*/
|
||||
public function licenseDecode($license): array
|
||||
{
|
||||
return Base::json2array(self::string($this->so->licenseDecode($license)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存License
|
||||
* @param $license
|
||||
*/
|
||||
public function licenseSave($license): void
|
||||
{
|
||||
$res = self::string($this->so->licenseSave($license));
|
||||
if ($res != 'success') {
|
||||
throw new ApiException($res ?: 'LICENSE 保存失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前会员ID(来自请求的token)
|
||||
* @return int
|
||||
*/
|
||||
public function userId(): int
|
||||
{
|
||||
return intval($this->so->userId());
|
||||
}
|
||||
|
||||
/**
|
||||
* token是否过期(来自请求的token)
|
||||
* @return bool
|
||||
*/
|
||||
public function userExpired(): bool
|
||||
{
|
||||
$expiredAt = $this->userExpiredAt();
|
||||
return $expiredAt && Carbon::parse($expiredAt)->isBefore(Carbon::now());
|
||||
}
|
||||
|
||||
/**
|
||||
* token过期时间(来自请求的token)
|
||||
* @return string|null
|
||||
*/
|
||||
public function userExpiredAt(): ?string
|
||||
{
|
||||
$expiredAt = self::string($this->so->userExpiredAt());
|
||||
return $expiredAt === 'forever' ? null : $expiredAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前会员邮箱地址(来自请求的token)
|
||||
* @return string
|
||||
*/
|
||||
public function userEmail(): string
|
||||
{
|
||||
return self::string($this->so->userEmail());
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前会员Encrypt(来自请求的token)
|
||||
* @return string
|
||||
*/
|
||||
public function userEncrypt(): string
|
||||
{
|
||||
return self::string($this->so->userEncrypt());
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前会员token(来自请求的token)
|
||||
* @return string
|
||||
*/
|
||||
public function userToken(): string
|
||||
{
|
||||
return self::string($this->so->userToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建帐号
|
||||
* @param $email
|
||||
* @param $password
|
||||
* @return User|null
|
||||
*/
|
||||
public function userCreate($email, $password): User|null
|
||||
{
|
||||
$data = Base::json2array(self::string($this->so->userCreate($email, $password)));
|
||||
if (Base::isError($data)) {
|
||||
throw new ApiException($data['msg'] ?: '注册失败');
|
||||
}
|
||||
if (DB::transactionLevel() > 0) {
|
||||
try {
|
||||
DB::commit();
|
||||
DB::beginTransaction();
|
||||
} catch (Throwable) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
$user = User::whereEmail($email)->first();
|
||||
if (empty($user)) {
|
||||
throw new ApiException('注册失败');
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成token(编码token)
|
||||
* @param $userid
|
||||
* @param $email
|
||||
* @param $encrypt
|
||||
* @param int $days 有效时间(天)
|
||||
* @return string
|
||||
*/
|
||||
public function tokenEncode($userid, $email, $encrypt, int $days = 15): string
|
||||
{
|
||||
return self::string($this->so->tokenEncode($userid, $email, $encrypt, $days));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码token
|
||||
* @param $token
|
||||
* @return array
|
||||
*/
|
||||
public function tokenDecode($token): array
|
||||
{
|
||||
$array = Base::json2array(self::string($this->so->tokenDecode($token)));
|
||||
$array['expired_at'] = $array['expired_at'] === 'forever' ? null : $array['expired_at'];
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译
|
||||
* @param $text
|
||||
* @param ?string $lang
|
||||
* @return string
|
||||
*/
|
||||
public function translate($text, ?string $lang = ""): string
|
||||
{
|
||||
if (empty($text)) {
|
||||
return "";
|
||||
}
|
||||
if (empty($lang)) {
|
||||
$lang = "";
|
||||
}
|
||||
return self::string($this->so->translate($text, $lang));
|
||||
}
|
||||
|
||||
/**
|
||||
* md5防破解
|
||||
* @param $text
|
||||
* @param string $password
|
||||
* @return string
|
||||
*/
|
||||
public function md5s($text, string $password = ""): string
|
||||
{
|
||||
return self::string($this->so->md5s($text, $password));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取php容器mac地址组
|
||||
* @return array
|
||||
*/
|
||||
public function macs(): array
|
||||
{
|
||||
$macs = explode(",", self::string($this->so->macs()));
|
||||
$array = [];
|
||||
foreach ($macs as $mac) {
|
||||
if (Base::isMac($mac)) {
|
||||
$array[] = $mac;
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前SN
|
||||
* @return string
|
||||
*/
|
||||
public function dooSN(): string
|
||||
{
|
||||
return self::string($this->so->dooSN());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前版本
|
||||
* @return string
|
||||
*/
|
||||
public function dooVersion(): string
|
||||
{
|
||||
return self::string($this->so->version());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成PGP密钥对
|
||||
* @param $name
|
||||
* @param $email
|
||||
* @param string $passphrase
|
||||
* @return array
|
||||
*/
|
||||
public function pgpGenerateKeyPair($name, $email, string $passphrase = ""): array
|
||||
{
|
||||
return Base::json2array(self::string($this->so->pgpGenerateKeyPair($name, $email, $passphrase)));
|
||||
}
|
||||
|
||||
/**
|
||||
* PGP加密
|
||||
* @param $plaintext
|
||||
* @param $publicKey
|
||||
* @return string
|
||||
*/
|
||||
public function pgpEncrypt($plaintext, $publicKey): string
|
||||
{
|
||||
if (strlen($publicKey) < 50) {
|
||||
$keyCache = Base::json2array(Cache::get("KeyPair::" . $publicKey));
|
||||
$publicKey = $keyCache['public_key'];
|
||||
}
|
||||
return self::string($this->so->pgpEncrypt($plaintext, $publicKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* PGP解密
|
||||
* @param $encryptedText
|
||||
* @param $privateKey
|
||||
* @param null $passphrase
|
||||
* @return string
|
||||
*/
|
||||
public function pgpDecrypt($encryptedText, $privateKey, $passphrase = null): string
|
||||
{
|
||||
if (strlen($privateKey) < 50) {
|
||||
$keyCache = Base::json2array(Cache::get("KeyPair::" . $privateKey));
|
||||
$privateKey = $keyCache['private_key'];
|
||||
$passphrase = $keyCache['passphrase'];
|
||||
}
|
||||
return self::string($this->so->pgpDecrypt($encryptedText, $privateKey, $passphrase));
|
||||
}
|
||||
|
||||
/**
|
||||
* PGP加密API
|
||||
* @param $plaintext
|
||||
* @param $publicKey
|
||||
* @return string
|
||||
*/
|
||||
public function pgpEncryptApi($plaintext, $publicKey): string
|
||||
{
|
||||
$content = Base::array2json($plaintext);
|
||||
$content = $this->pgpEncrypt($content, $publicKey);
|
||||
return preg_replace("/\s*-----(BEGIN|END) PGP MESSAGE-----\s*/i", "", $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* PGP解密API
|
||||
* @param $encryptedText
|
||||
* @param null $privateKey
|
||||
* @param null $passphrase
|
||||
* @return array
|
||||
*/
|
||||
public function pgpDecryptApi($encryptedText, $privateKey, $passphrase = null): array
|
||||
{
|
||||
$content = "-----BEGIN PGP MESSAGE-----\n\n" . $encryptedText . "\n-----END PGP MESSAGE-----";
|
||||
$content = $this->pgpDecrypt($content, $privateKey, $passphrase);
|
||||
return Base::json2array($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析PGP参数
|
||||
* @param $string
|
||||
* @return string[]
|
||||
*/
|
||||
public function pgpParseStr($string): array
|
||||
{
|
||||
$array = [
|
||||
'encrypt_type' => '',
|
||||
'encrypt_id' => '',
|
||||
'client_type' => '',
|
||||
'client_key' => '',
|
||||
];
|
||||
$string = str_replace(";", "&", $string);
|
||||
parse_str($string, $params);
|
||||
foreach ($params as $key => $value) {
|
||||
$key = strtolower(trim($key));
|
||||
if ($key) {
|
||||
$array[$key] = trim($value);
|
||||
}
|
||||
}
|
||||
if ($array['client_type'] === 'pgp' && $array['client_key']) {
|
||||
$array['client_key'] = $this->pgpPublicFormat($array['client_key']);
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 还原公钥格式
|
||||
* @param $key
|
||||
* @return string
|
||||
*/
|
||||
public function pgpPublicFormat($key): string
|
||||
{
|
||||
$key = str_replace(["-", "_", "$"], ["+", "/", "\n"], $key);
|
||||
if (!str_contains($key, '-----BEGIN PGP PUBLIC KEY BLOCK-----')) {
|
||||
$key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n" . $key . "\n-----END PGP PUBLIC KEY BLOCK-----";
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
52
app/Module/Lock.php
Normal file
52
app/Module/Lock.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class Lock
|
||||
{
|
||||
/**
|
||||
* 使用Redis分布式锁执行闭包
|
||||
* @param string $key 锁的key
|
||||
* @param Closure $closure 要执行的闭包函数
|
||||
* @param int $ttl 锁的过期时间(毫秒),默认10000(10秒)
|
||||
* @param int $waitTimeout 等待锁的超时时间(毫秒),0表示不等待,默认10000(10秒)
|
||||
* @return mixed 闭包函数的返回值
|
||||
* @throws Exception 如果获取锁失败或闭包执行异常
|
||||
*/
|
||||
public static function withLock(string $key, Closure $closure, int $ttl = 10000, int $waitTimeout = 10000)
|
||||
{
|
||||
$lockKey = "lock:{$key}";
|
||||
$lockValue = uniqid('', true); // 生成唯一值,用于安全释放锁
|
||||
|
||||
// 尝试获取锁,如果waitTimeout为0则直接返回false,否则等待指定时间
|
||||
$acquired = false;
|
||||
if ($waitTimeout > 0) {
|
||||
$end = microtime(true) + ($waitTimeout / 1000);
|
||||
while (microtime(true) < $end) {
|
||||
if (Redis::set($lockKey, $lockValue, 'PX', $ttl, 'NX')) {
|
||||
$acquired = true;
|
||||
break;
|
||||
}
|
||||
usleep(100000); // 休眠100ms后重试
|
||||
}
|
||||
} else {
|
||||
$acquired = Redis::set($lockKey, $lockValue, 'PX', $ttl, 'NX');
|
||||
}
|
||||
|
||||
if (!$acquired) {
|
||||
throw new Exception("Failed to acquire lock for key: {$key}");
|
||||
}
|
||||
|
||||
try {
|
||||
// 执行闭包
|
||||
return $closure();
|
||||
} finally {
|
||||
// 安全释放锁(仅当锁值未变时删除)
|
||||
Redis::eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", 1, $lockKey, $lockValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,23 @@ class MsgTool
|
||||
}
|
||||
|
||||
$isMd = strtolower($type) === 'md';
|
||||
$placeholders = [];
|
||||
|
||||
// 如果是Markdown,转换为HTML
|
||||
// 如果是Markdown,先处理特殊标记及转换为HTML
|
||||
if ($isMd) {
|
||||
$converter = new CommonMarkConverter();
|
||||
// 处理特殊标记
|
||||
$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 "";
|
||||
@@ -50,10 +62,26 @@ class MsgTool
|
||||
// 递归函数来遍历节点并截取内容
|
||||
self::traverseNodes($body, $currentLength, $length, $truncatedHtml);
|
||||
|
||||
// 如果是Markdown,转换回Markdown
|
||||
// 如果是Markdown,转换回Markdown及还原特殊标记
|
||||
if ($isMd) {
|
||||
$converter = new HtmlConverter();
|
||||
$truncatedHtml = $converter->convert($truncatedHtml);
|
||||
// 转换回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;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
267
app/Module/ZincSearch/ZincSearchBase.php
Normal file
267
app/Module/ZincSearch/ZincSearchBase.php
Normal file
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ZincSearch;
|
||||
|
||||
use App\Module\Apps;
|
||||
use App\Module\Doo;
|
||||
|
||||
/**
|
||||
* ZincSearch 公共类
|
||||
*/
|
||||
class ZincSearchBase
|
||||
{
|
||||
private mixed $host;
|
||||
private mixed $port;
|
||||
private mixed $user;
|
||||
private mixed $pass;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->host = env('ZINCSEARCH_HOST', 'search');
|
||||
$this->port = env('ZINCSEARCH_PORT', '4080');
|
||||
$this->user = env('DB_USERNAME', '');
|
||||
$this->pass = env('DB_PASSWORD', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
*/
|
||||
private function request($path, $body = null, $method = 'POST')
|
||||
{
|
||||
if (!Apps::isInstalled("search")) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => Doo::translate("应用「ZincSearch」未安装")
|
||||
];
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, "http://{$this->host}:{$this->port}{$path}");
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_USERPWD, $this->user . ':' . $this->pass);
|
||||
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
||||
|
||||
$headers = ['Content-Type: application/json'];
|
||||
if ($method === 'BULK') {
|
||||
$headers = ['Content-Type: text/plain'];
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
if ($body !== null) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
}
|
||||
$result = curl_exec($ch);
|
||||
$error = curl_error($ch);
|
||||
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($error) {
|
||||
return ['success' => false, 'error' => $error];
|
||||
}
|
||||
$data = json_decode($result, true);
|
||||
return [
|
||||
'success' => $status >= 200 && $status < 300,
|
||||
'status' => $status,
|
||||
'data' => $data
|
||||
];
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 索引管理相关方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 创建索引
|
||||
*/
|
||||
public static function createIndex($index, $mappings = []): array
|
||||
{
|
||||
$body = json_encode([
|
||||
'name' => $index,
|
||||
'mappings' => $mappings
|
||||
]);
|
||||
return (new self())->request("/api/index", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取索引信息
|
||||
*/
|
||||
public static function getIndex($index): array
|
||||
{
|
||||
return (new self())->request("/api/index/{$index}", null, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断索引是否存在
|
||||
*/
|
||||
public static function indexExists($index): bool
|
||||
{
|
||||
$result = self::getIndex($index);
|
||||
return $result['success'] && isset($result['data']['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有索引
|
||||
*/
|
||||
public static function listIndices(): array
|
||||
{
|
||||
return (new self())->request("/api/index", null, 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除索引
|
||||
*/
|
||||
public static function deleteIndex($index): array
|
||||
{
|
||||
return (new self())->request("/api/index/{$index}", null, 'DELETE');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除所有索引
|
||||
*/
|
||||
public static function deleteAllIndices(): array
|
||||
{
|
||||
$instance = new self();
|
||||
$result = $instance->request("/api/index", null, 'GET');
|
||||
|
||||
if (!$result['success']) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$indices = $result['data'] ?? [];
|
||||
$deleteResults = [];
|
||||
$success = true;
|
||||
|
||||
foreach ($indices as $index) {
|
||||
$indexName = $index['name'] ?? '';
|
||||
if (!empty($indexName)) {
|
||||
$deleteResult = $instance->request("/api/index/{$indexName}", null, 'DELETE');
|
||||
$deleteResults[$indexName] = $deleteResult;
|
||||
|
||||
if (!$deleteResult['success']) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => $success,
|
||||
'message' => $success ? '所有索引删除成功' : '部分索引删除失败',
|
||||
'details' => $deleteResults
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析文本
|
||||
*/
|
||||
public static function analyze($analyzer, $text): array
|
||||
{
|
||||
$body = json_encode([
|
||||
'analyzer' => $analyzer,
|
||||
'text' => $text
|
||||
]);
|
||||
return (new self())->request("/api/_analyze", $body);
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 文档管理相关方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 写入单条文档
|
||||
*/
|
||||
public static function addDoc($index, $doc): array
|
||||
{
|
||||
$body = json_encode($doc);
|
||||
return (new self())->request("/api/{$index}/_doc", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文档
|
||||
*/
|
||||
public static function updateDoc($index, $id, $doc): array
|
||||
{
|
||||
$body = json_encode($doc);
|
||||
return (new self())->request("/api/{$index}/_update/{$id}", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文档
|
||||
*/
|
||||
public static function deleteDoc($index, $id): array
|
||||
{
|
||||
return (new self())->request("/api/{$index}/_doc/{$id}", null, 'DELETE');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量写入文档
|
||||
*/
|
||||
public static function addDocs($index, $docs): array
|
||||
{
|
||||
$body = json_encode([
|
||||
'index' => $index,
|
||||
'records' => $docs
|
||||
]);
|
||||
return (new self())->request("/api/_bulkv2", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用原始BULK API批量写入文档
|
||||
* 请求格式为Elasticsearch兼容格式
|
||||
*/
|
||||
public static function bulkDocs($data): array
|
||||
{
|
||||
return (new self())->request("/api/_bulk", $data, 'BULK');
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 搜索相关方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 查询文档
|
||||
*/
|
||||
public static function search($index, $query, $from = 0, $size = 10): array
|
||||
{
|
||||
$searchParams = [
|
||||
'search_type' => 'match',
|
||||
'query' => [
|
||||
'term' => $query
|
||||
],
|
||||
'from' => $from,
|
||||
'max_results' => $size
|
||||
];
|
||||
|
||||
$body = json_encode($searchParams);
|
||||
return (new self())->request("/api/{$index}/_search", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级查询文档
|
||||
*/
|
||||
public static function advancedSearch($index, $searchParams): array
|
||||
{
|
||||
$body = json_encode($searchParams);
|
||||
return (new self())->request("/api/{$index}/_search", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容ES查询文档
|
||||
*/
|
||||
public static function elasticSearch($index, $searchParams): array
|
||||
{
|
||||
$body = json_encode($searchParams);
|
||||
return (new self())->request("/es/{$index}/_search", $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多索引查询
|
||||
*/
|
||||
public static function multiSearch($queries): array
|
||||
{
|
||||
$body = json_encode($queries);
|
||||
return (new self())->request("/api/_msearch", $body);
|
||||
}
|
||||
}
|
||||
612
app/Module/ZincSearch/ZincSearchDialogMsg.php
Normal file
612
app/Module/ZincSearch/ZincSearchDialogMsg.php
Normal file
@@ -0,0 +1,612 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ZincSearch;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Module\Apps;
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* ZincSearch 会话消息类
|
||||
*
|
||||
* 使用方法:
|
||||
*
|
||||
* 1. 基础方法
|
||||
* - 清空所有数据: clear();
|
||||
*
|
||||
* 2. 搜索方法
|
||||
* - 关键词搜索: search('用户ID', '关键词');
|
||||
*
|
||||
* 3. 基本方法
|
||||
* - 单个同步: sync(WebSocketDialogMsg $dialogMsg);
|
||||
* - 批量同步: batchSync(WebSocketDialogMsg[] $dialogMsgs);
|
||||
* - 用户同步: userSync(WebSocketDialogUser $dialogUser);
|
||||
* - 删除消息: delete(WebSocketDialogMsg|WebSocketDialogUser|int $data);
|
||||
*/
|
||||
class ZincSearchDialogMsg
|
||||
{
|
||||
/**
|
||||
* 索引名称
|
||||
*/
|
||||
protected static string $indexNameMsg = 'dialogMsg';
|
||||
protected static string $indexNameUser = 'dialogUser';
|
||||
|
||||
// ==============================
|
||||
// 基础方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 确保索引存在
|
||||
*/
|
||||
private static function ensureIndex(): bool
|
||||
{
|
||||
if (!ZincSearchBase::indexExists(self::$indexNameMsg)) {
|
||||
$mappings = [
|
||||
'properties' => [
|
||||
// 拓展数据
|
||||
'dialog_userid' => ['type' => 'keyword', 'index' => true], // 对话ID+用户ID
|
||||
'to_userid' => ['type' => 'numeric', 'index' => true], // 此消息发给的用户ID
|
||||
|
||||
// 消息数据
|
||||
'id' => ['type' => 'numeric', 'index' => true],
|
||||
'dialog_id' => ['type' => 'numeric', 'index' => true],
|
||||
'dialog_type' => ['type' => 'keyword', 'index' => true],
|
||||
'session_id' => ['type' => 'numeric', 'index' => true],
|
||||
'userid' => ['type' => 'numeric', 'index' => true],
|
||||
'type' => ['type' => 'keyword', 'index' => true],
|
||||
'key' => ['type' => 'text', 'index' => true],
|
||||
'created_at' => ['type' => 'date', 'index' => true],
|
||||
'updated_at' => ['type' => 'date', 'index' => true],
|
||||
]
|
||||
];
|
||||
$result = ZincSearchBase::createIndex(self::$indexNameMsg, $mappings);
|
||||
return $result['success'] ?? false;
|
||||
}
|
||||
if (!ZincSearchBase::indexExists(self::$indexNameUser)) {
|
||||
$mappings = [
|
||||
'properties' => [
|
||||
// 拓展数据
|
||||
'dialog_userid' => ['type' => 'keyword', 'index' => true], // 对话ID+用户ID
|
||||
|
||||
// 用户数据
|
||||
'id' => ['type' => 'numeric', 'index' => true],
|
||||
'dialog_id' => ['type' => 'numeric', 'index' => true],
|
||||
'userid' => ['type' => 'numeric', 'index' => true],
|
||||
'top_at' => ['type' => 'date', 'index' => true],
|
||||
'last_at' => ['type' => 'date', 'index' => true],
|
||||
'mark_unread' => ['type' => 'numeric', 'index' => true],
|
||||
'silence' => ['type' => 'numeric', 'index' => true],
|
||||
'hide' => ['type' => 'numeric', 'index' => true],
|
||||
'color' => ['type' => 'keyword', 'index' => true],
|
||||
'created_at' => ['type' => 'date', 'index' => true],
|
||||
'updated_at' => ['type' => 'date', 'index' => true],
|
||||
]
|
||||
];
|
||||
$result = ZincSearchBase::createIndex(self::$indexNameUser, $mappings);
|
||||
return $result['success'] ?? false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有键值
|
||||
*
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function clear(): bool
|
||||
{
|
||||
// 检查索引是否存在然后删除
|
||||
if (ZincSearchBase::indexExists(self::$indexNameMsg)) {
|
||||
$deleteResult = ZincSearchBase::deleteIndex(self::$indexNameMsg);
|
||||
if (!($deleteResult['success'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (ZincSearchBase::indexExists(self::$indexNameUser)) {
|
||||
$deleteResult = ZincSearchBase::deleteIndex(self::$indexNameUser);
|
||||
if (!($deleteResult['success'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return self::ensureIndex();
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 搜索方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 根据用户ID和消息关键词搜索会话
|
||||
*
|
||||
* @param string $userid 用户ID
|
||||
* @param string $keyword 消息关键词
|
||||
* @param int $from 起始位置
|
||||
* @param int $size 返回结果数量
|
||||
* @return array
|
||||
*/
|
||||
public static function search(string $userid, string $keyword, int $from = 0, int $size = 20): array
|
||||
{
|
||||
if (!Apps::isInstalled("search")) {
|
||||
// 如果搜索功能未安装,使用数据库查询
|
||||
return self::searchByMysql($userid, $keyword, $from, $size);
|
||||
}
|
||||
|
||||
$searchParams = [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
['term' => ['to_userid' => $userid]],
|
||||
['match_phrase' => ['key' => $keyword]]
|
||||
]
|
||||
]
|
||||
],
|
||||
'from' => $from,
|
||||
'size' => $size,
|
||||
'sort' => [
|
||||
['updated_at' => 'desc']
|
||||
]
|
||||
];
|
||||
try {
|
||||
$result = ZincSearchBase::elasticSearch(self::$indexNameMsg, $searchParams);
|
||||
$hits = $result['data']['hits']['hits'] ?? [];
|
||||
|
||||
// 收集所有的用户信息
|
||||
$dialogUserids = [];
|
||||
foreach ($hits as $hit) {
|
||||
$source = $hit['_source'];
|
||||
$dialogUserids[] = $source['dialog_userid'];
|
||||
}
|
||||
$userInfos = self::searchUser(array_unique($dialogUserids));
|
||||
|
||||
// 组合返回结果,将用户信息合并到消息中
|
||||
$msgs = [];
|
||||
foreach ($hits as $hit) {
|
||||
$msgInfo = $hit['_source'];
|
||||
$userInfo = $userInfos[$msgInfo['dialog_userid']] ?? [];
|
||||
if ($userInfo) {
|
||||
$msgs[] = [
|
||||
'id' => $msgInfo['dialog_id'],
|
||||
'search_msg_id' => $msgInfo['id'],
|
||||
'user_at' => Carbon::parse($msgInfo['updated_at'])->format('Y-m-d H:i:s'),
|
||||
|
||||
'mark_unread' => $userInfo['mark_unread'],
|
||||
'silence' => $userInfo['silence'],
|
||||
'hide' => $userInfo['hide'],
|
||||
'color' => $userInfo['color'],
|
||||
'top_at' => Carbon::parse($userInfo['top_at'])->format('Y-m-d H:i:s'),
|
||||
'last_at' => Carbon::parse($userInfo['last_at'])->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
}
|
||||
return $msgs;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('search: ' . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID和消息关键词搜索会话(MySQL 版本,主要用于未安装ZincSearch的情况)
|
||||
*
|
||||
* @param string $userid 用户ID
|
||||
* @param string $keyword 消息关键词
|
||||
* @param int $from 起始位置
|
||||
* @param int $size 返回结果数量
|
||||
* @return array
|
||||
*/
|
||||
private static function searchByMysql(string $userid, string $keyword, int $from = 0, int $size = 20): array
|
||||
{
|
||||
$items = DB::table('web_socket_dialog_users as u')
|
||||
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at', 'm.id as search_msg_id'])
|
||||
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
|
||||
->join('web_socket_dialog_msgs as m', 'm.dialog_id', '=', 'd.id')
|
||||
->where('u.userid', $userid)
|
||||
->where('m.bot', 0)
|
||||
->whereNull('d.deleted_at')
|
||||
->where('m.key', 'like', "%{$keyword}%")
|
||||
->orderByDesc('m.id')
|
||||
->offset($from)
|
||||
->limit($size)
|
||||
->get()
|
||||
->all();
|
||||
$msgs = [];
|
||||
foreach ($items as $item) {
|
||||
$msgs[] = [
|
||||
'id' => $item->id,
|
||||
'search_msg_id' => $item->search_msg_id,
|
||||
'user_at' => Carbon::parse($item->user_at)->format('Y-m-d H:i:s'),
|
||||
|
||||
'mark_unread' => $item->mark_unread,
|
||||
'silence' => $item->silence,
|
||||
'hide' => $item->hide,
|
||||
'color' => $item->color,
|
||||
'top_at' => Carbon::parse($item->top_at)->format('Y-m-d H:i:s'),
|
||||
'last_at' => Carbon::parse($item->last_at)->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
return $msgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据对话用户ID搜索用户信息
|
||||
* @param array $dialogUserids
|
||||
* @return array
|
||||
*/
|
||||
private static function searchUser(array $dialogUserids): array
|
||||
{
|
||||
if (empty($dialogUserids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$userInfos = [];
|
||||
|
||||
// 构建用户查询条件
|
||||
$userSearchParams = [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'should' => []
|
||||
]
|
||||
],
|
||||
'size' => count($dialogUserids) // 确保取到所有符合条件的记录
|
||||
];
|
||||
|
||||
// 添加所有 dialog_userid 到查询条件
|
||||
foreach ($dialogUserids as $dialogUserid) {
|
||||
$userSearchParams['query']['bool']['should'][] = [
|
||||
'term' => ['dialog_userid' => $dialogUserid]
|
||||
];
|
||||
}
|
||||
|
||||
// 查询用户信息
|
||||
$userResult = ZincSearchBase::elasticSearch(self::$indexNameUser, $userSearchParams);
|
||||
$userHits = $userResult['data']['hits']['hits'] ?? [];
|
||||
|
||||
// 以 dialog_userid 为键保存用户信息
|
||||
foreach ($userHits as $userHit) {
|
||||
$userSource = $userHit['_source'];
|
||||
$userInfos[$userSource['dialog_userid']] = $userSource;
|
||||
}
|
||||
|
||||
return $userInfos;
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 生成内容
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 生成 dialog_userid
|
||||
*
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return string
|
||||
*/
|
||||
private static function generateDialogUserid(WebSocketDialogUser $dialogUser): string
|
||||
{
|
||||
return "{$dialogUser->dialog_id}_{$dialogUser->userid}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文档内容
|
||||
*
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return array
|
||||
*/
|
||||
private static function generateMsgData(WebSocketDialogMsg $dialogMsg, WebSocketDialogUser $dialogUser): array
|
||||
{
|
||||
return [
|
||||
'_id' => self::$indexNameMsg . "_" . $dialogMsg->id . "_" . $dialogUser->userid,
|
||||
'dialog_userid' => self::generateDialogUserid($dialogUser),
|
||||
'to_userid' => $dialogUser->userid,
|
||||
|
||||
'id' => $dialogMsg->id,
|
||||
'dialog_id' => $dialogMsg->dialog_id,
|
||||
'dialog_type' => $dialogMsg->dialog_type,
|
||||
'session_id' => $dialogMsg->session_id,
|
||||
'userid' => $dialogMsg->userid,
|
||||
'type' => $dialogMsg->type,
|
||||
'key' => $dialogMsg->key,
|
||||
'created_at' => $dialogMsg->created_at,
|
||||
'updated_at' => $dialogMsg->updated_at,
|
||||
];
|
||||
}
|
||||
private static function generateUserData(WebSocketDialogUser $dialogUser): array
|
||||
{
|
||||
return [
|
||||
'_id' => self::$indexNameUser . "_" . $dialogUser->id,
|
||||
'dialog_userid' => self::generateDialogUserid($dialogUser),
|
||||
|
||||
'id' => $dialogUser->id,
|
||||
'dialog_id' => $dialogUser->dialog_id,
|
||||
'userid' => $dialogUser->userid,
|
||||
'top_at' => $dialogUser->top_at,
|
||||
'last_at' => $dialogUser->last_at,
|
||||
'mark_unread' => $dialogUser->mark_unread,
|
||||
'silence' => $dialogUser->silence,
|
||||
'hide' => $dialogUser->hide,
|
||||
'color' => $dialogUser->color,
|
||||
'created_at' => $dialogUser->created_at,
|
||||
'updated_at' => $dialogUser->updated_at,
|
||||
];
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 基本方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 同步消息(建议在异步进程中使用)
|
||||
*
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @return bool
|
||||
*/
|
||||
public static function sync(WebSocketDialogMsg $dialogMsg): bool
|
||||
{
|
||||
if (!self::ensureIndex()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($dialogMsg->bot) {
|
||||
// 如果是机器人消息,跳过
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取此会话的所有用户
|
||||
$dialogUsers = WebSocketDialogUser::whereDialogId($dialogMsg->dialog_id)->get();
|
||||
|
||||
if ($dialogUsers->isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$msgs = [];
|
||||
$users = [];
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
if (empty($dialogMsg->key)) {
|
||||
// 如果消息没有关键词,跳过
|
||||
continue;
|
||||
}
|
||||
if ($dialogUser->userid == 0) {
|
||||
// 跳过系统用户
|
||||
continue;
|
||||
}
|
||||
$msgs[] = self::generateMsgData($dialogMsg, $dialogUser);
|
||||
$users[$dialogUser->id] = self::generateUserData($dialogUser);
|
||||
}
|
||||
|
||||
if ($msgs) {
|
||||
// 批量写入消息
|
||||
ZincSearchBase::addDocs(self::$indexNameMsg, $msgs);
|
||||
}
|
||||
|
||||
if ($users) {
|
||||
// 批量写入用户
|
||||
ZincSearchBase::addDocs(self::$indexNameUser, array_values($users));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('sync: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步消息(建议在异步进程中使用)
|
||||
*
|
||||
* @param WebSocketDialogMsg[] $dialogMsgs
|
||||
* @return int 成功同步的消息数
|
||||
*/
|
||||
public static function batchSync($dialogMsgs): int
|
||||
{
|
||||
if (!self::ensureIndex()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
try {
|
||||
$msgs = [];
|
||||
$users = [];
|
||||
$userDialogs = [];
|
||||
|
||||
// 预处理:收集所有涉及的对话ID
|
||||
$dialogIds = [];
|
||||
foreach ($dialogMsgs as $dialogMsg) {
|
||||
$dialogIds[] = $dialogMsg->dialog_id;
|
||||
}
|
||||
$dialogIds = array_unique($dialogIds);
|
||||
|
||||
// 获取所有相关的用户-对话关系
|
||||
if (!empty($dialogIds)) {
|
||||
$dialogUsers = WebSocketDialogUser::whereIn('dialog_id', $dialogIds)->get();
|
||||
|
||||
// 按对话ID组织用户
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
$userDialogs[$dialogUser->dialog_id][] = $dialogUser;
|
||||
}
|
||||
}
|
||||
|
||||
// 为每条消息准备所有相关用户的文档
|
||||
foreach ($dialogMsgs as $dialogMsg) {
|
||||
if (!isset($userDialogs[$dialogMsg->dialog_id])) {
|
||||
// 如果该会话没有用户,跳过
|
||||
continue;
|
||||
}
|
||||
if ($dialogMsg->bot) {
|
||||
// 如果是机器人消息,跳过
|
||||
continue;
|
||||
}
|
||||
/** @var WebSocketDialogUser $dialogUser */
|
||||
foreach ($userDialogs[$dialogMsg->dialog_id] as $dialogUser) {
|
||||
if (empty($dialogMsg->key)) {
|
||||
// 如果消息没有关键词,跳过
|
||||
continue;
|
||||
}
|
||||
if ($dialogUser->userid == 0) {
|
||||
// 跳过系统用户
|
||||
continue;
|
||||
}
|
||||
$msgs[] = self::generateMsgData($dialogMsg, $dialogUser);
|
||||
$users[$dialogUser->id] = self::generateUserData($dialogUser);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($msgs) {
|
||||
// 批量写入消息
|
||||
ZincSearchBase::addDocs(self::$indexNameMsg, $msgs);
|
||||
}
|
||||
|
||||
if ($users) {
|
||||
// 批量写入用户
|
||||
ZincSearchBase::addDocs(self::$indexNameUser, array_values($users));
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('batchSync: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步用户(建议在异步进程中使用)
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return bool
|
||||
*/
|
||||
public static function userSync(WebSocketDialogUser $dialogUser): bool
|
||||
{
|
||||
if (!self::ensureIndex()) {
|
||||
return false;
|
||||
}
|
||||
$data = self::generateUserData($dialogUser);
|
||||
|
||||
// 生成查询用户条件
|
||||
$searchParams = [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
['term' => ['dialog_userid' => $data['dialog_userid']]]
|
||||
]
|
||||
]
|
||||
],
|
||||
'size' => 1
|
||||
];
|
||||
|
||||
try {
|
||||
// 查询用户是否存在
|
||||
$result = ZincSearchBase::elasticSearch(self::$indexNameUser, $searchParams);
|
||||
$hits = $result['data']['hits']['hits'] ?? [];
|
||||
|
||||
// 同步用户(存在更新、不存在添加)
|
||||
$result = ZincSearchBase::addDoc(self::$indexNameUser, $data);
|
||||
if (!isset($result['success'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 用户不存在,同步消息
|
||||
if (empty($hits)) {
|
||||
$lastId = 0; // 上次同步的最后ID
|
||||
$batchSize = 500; // 每批处理的消息数量
|
||||
|
||||
// 分批同步消息
|
||||
do {
|
||||
// 获取一批
|
||||
$dialogMsgs = WebSocketDialogMsg::whereDialogId($dialogUser->dialog_id)
|
||||
->where('id', '>', $lastId)
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($dialogMsgs->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 同步数据
|
||||
ZincSearchDialogMsg::batchSync($dialogMsgs);
|
||||
|
||||
// 更新最后ID
|
||||
$lastId = $dialogMsgs->last()->id;
|
||||
} while (count($dialogMsgs) == $batchSize);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('userSync: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除(建议在异步进程中使用)
|
||||
*
|
||||
* @param WebSocketDialogMsg|WebSocketDialogUser|int $data
|
||||
* @return int
|
||||
*/
|
||||
public static function delete(mixed $data): int
|
||||
{
|
||||
$batchSize = 500; // 每批处理的文档数量
|
||||
$totalDeleted = 0; // 总共删除的文档数量
|
||||
$from = 0;
|
||||
|
||||
// 根据数据类型生成查询条件
|
||||
if ($data instanceof WebSocketDialogMsg) {
|
||||
$query = [
|
||||
'field' => 'id',
|
||||
'term' => (string) $data->id
|
||||
];
|
||||
} elseif ($data instanceof WebSocketDialogUser) {
|
||||
$query = [
|
||||
'field' => 'dialog_userid',
|
||||
'term' => self::generateDialogUserid($data),
|
||||
];
|
||||
} else {
|
||||
$query = [
|
||||
'field' => 'id',
|
||||
'term' => (string) $data
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
// 根据消息ID查找相关文档
|
||||
$result = ZincSearchBase::advancedSearch(self::$indexNameMsg, [
|
||||
'search_type' => 'term',
|
||||
'query' => $query,
|
||||
'from' => $from,
|
||||
'max_results' => $batchSize
|
||||
]);
|
||||
$hits = $result['data']['hits']['hits'] ?? [];
|
||||
|
||||
// 如果没有更多文档,退出循环
|
||||
if (empty($hits)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 删除本批次找到的所有文档
|
||||
foreach ($hits as $hit) {
|
||||
if (isset($hit['_id'])) {
|
||||
ZincSearchBase::deleteDoc(self::$indexNameMsg, $hit['_id']);
|
||||
$totalDeleted++;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果返回的文档数少于批次大小,说明已经没有更多文档了
|
||||
if (count($hits) < $batchSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 移动到下一批
|
||||
$from += $batchSize;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('delete: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $totalDeleted;
|
||||
}
|
||||
}
|
||||
276
app/Module/ZincSearch/ZincSearchKeyValue.php
Normal file
276
app/Module/ZincSearch/ZincSearchKeyValue.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ZincSearch;
|
||||
|
||||
/**
|
||||
* ZincSearch 键值存储类
|
||||
*
|
||||
* 使用方法:
|
||||
*
|
||||
* 1. 基础方法
|
||||
* - 确保索引存在: ensureIndex();
|
||||
* - 清空所有数据: clear();
|
||||
*
|
||||
* 2. 基本操作
|
||||
* - 设置键值: set('site_name', '我的网站');
|
||||
* - 设置复杂数据: set('site_config', ['logo' => 'logo.png', 'theme' => 'dark']);
|
||||
* - 合并现有数据: set('site_config', ['footer' => '版权所有'], true);
|
||||
* - 获取键值: $siteName = get('site_name');
|
||||
* - 获取键值带默认值: $theme = get('theme', 'light');
|
||||
* - 删除键值: delete('temporary_data');
|
||||
*
|
||||
* 3. 批量操作
|
||||
* - 批量设置: batchSet(['user_count' => 100, 'active_users' => 50]);
|
||||
* - 批量获取: $stats = batchGet(['user_count', 'active_users']);
|
||||
*/
|
||||
class ZincSearchKeyValue
|
||||
{
|
||||
/**
|
||||
* 索引名称
|
||||
*/
|
||||
protected static string $indexName = 'keyValue';
|
||||
|
||||
// ==============================
|
||||
// 基础方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 确保索引存在
|
||||
*/
|
||||
public static function ensureIndex(): bool
|
||||
{
|
||||
if (!ZincSearchBase::indexExists(self::$indexName)) {
|
||||
$mappings = [
|
||||
'properties' => [
|
||||
'key' => ['type' => 'keyword', 'index' => true],
|
||||
'value' => ['type' => 'text', 'index' => true],
|
||||
'created_at' => ['type' => 'date', 'index' => true],
|
||||
'updated_at' => ['type' => 'date', 'index' => true]
|
||||
]
|
||||
];
|
||||
$result = ZincSearchBase::createIndex(self::$indexName, $mappings);
|
||||
return $result['success'] ?? false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有键值
|
||||
*
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function clear(): bool
|
||||
{
|
||||
// 检查索引是否存在
|
||||
if (!ZincSearchBase::indexExists(self::$indexName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 删除再重建索引
|
||||
$deleteResult = ZincSearchBase::deleteIndex(self::$indexName);
|
||||
if (!($deleteResult['success'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::ensureIndex();
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 基本操作
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 设置键值
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @param mixed $value 值
|
||||
* @param bool $merge 是否合并现有数据(如果值是数组)
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function set(string $key, mixed $value, bool $merge = false): bool
|
||||
{
|
||||
if (!self::ensureIndex()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查键是否已存在
|
||||
if ($merge && is_array($value)) {
|
||||
$existingData = self::get($key);
|
||||
if (is_array($existingData)) {
|
||||
$value = array_merge($existingData, $value);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否存在相同键的文档 - 使用精确查询而不是普通搜索
|
||||
$searchParams = [
|
||||
'search_type' => 'term',
|
||||
'query' => [
|
||||
'field' => 'key',
|
||||
'term' => $key
|
||||
],
|
||||
'from' => 0,
|
||||
'max_results' => 1
|
||||
];
|
||||
|
||||
$result = ZincSearchBase::advancedSearch(self::$indexName, $searchParams);
|
||||
$docs = $result['data']['hits']['hits'] ?? [];
|
||||
$now = date('c');
|
||||
|
||||
if (!empty($docs)) {
|
||||
$docId = $docs[0]['_id'] ?? null;
|
||||
if ($docId) {
|
||||
// 更新现有文档
|
||||
$docData = [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'updated_at' => $now
|
||||
];
|
||||
$updateResult = ZincSearchBase::updateDoc(self::$indexName, $docId, $docData);
|
||||
return $updateResult['success'] ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新文档
|
||||
$docData = [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now
|
||||
];
|
||||
$addResult = ZincSearchBase::addDoc(self::$indexName, $docData);
|
||||
return $addResult['success'] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @param mixed $default 默认值
|
||||
* @return mixed 值或默认值
|
||||
*/
|
||||
public static function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if (!self::ensureIndex() || empty($key)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// 精确匹配键名
|
||||
$searchParams = [
|
||||
'search_type' => 'term',
|
||||
'query' => [
|
||||
'field' => 'key',
|
||||
'term' => $key
|
||||
],
|
||||
'from' => 0,
|
||||
'max_results' => 1
|
||||
];
|
||||
|
||||
$result = ZincSearchBase::advancedSearch(self::$indexName, $searchParams);
|
||||
if (!($result['success'] ?? false)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$hits = $result['data']['hits']['hits'] ?? [];
|
||||
if (empty($hits)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $hits[0]['_source']['value'] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键值
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function delete(string $key): bool
|
||||
{
|
||||
if (!self::ensureIndex() || empty($key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查找文档ID
|
||||
$searchParams = [
|
||||
'search_type' => 'term',
|
||||
'query' => [
|
||||
'field' => 'key',
|
||||
'term' => $key
|
||||
],
|
||||
'from' => 0,
|
||||
'max_results' => 1
|
||||
];
|
||||
|
||||
$result = ZincSearchBase::advancedSearch(self::$indexName, $searchParams);
|
||||
if (!($result['success'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hits = $result['data']['hits']['hits'] ?? [];
|
||||
if (empty($hits)) {
|
||||
return true; // 不存在视为删除成功
|
||||
}
|
||||
|
||||
$docId = $hits[0]['_id'] ?? null;
|
||||
if (empty($docId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$deleteResult = ZincSearchBase::deleteDoc(self::$indexName, $docId);
|
||||
return $deleteResult['success'] ?? false;
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 批量操作
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 批量设置键值对
|
||||
*
|
||||
* @param array $keyValues 键值对数组
|
||||
* @return bool 是否全部成功
|
||||
*/
|
||||
public static function batchSet(array $keyValues): bool
|
||||
{
|
||||
if (!self::ensureIndex() || empty($keyValues)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$docs = [];
|
||||
$now = date('c');
|
||||
|
||||
foreach ($keyValues as $key => $value) {
|
||||
$docs[] = [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now
|
||||
];
|
||||
}
|
||||
|
||||
$result = ZincSearchBase::addDocs(self::$indexName, $docs);
|
||||
return $result['success'] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取键值
|
||||
*
|
||||
* @param array $keys 键名数组
|
||||
* @return array 键值对数组
|
||||
*/
|
||||
public static function batchGet(array $keys): array
|
||||
{
|
||||
if (!self::ensureIndex() || empty($keys)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
// 遍历查询每个键
|
||||
foreach ($keys as $key) {
|
||||
$results[$key] = self::get($key);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
19
app/Observers/AbstractObserver.php
Normal file
19
app/Observers/AbstractObserver.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
|
||||
class AbstractObserver
|
||||
{
|
||||
/**
|
||||
* @param $task
|
||||
* @return void
|
||||
*/
|
||||
public static function taskDeliver($task)
|
||||
{
|
||||
if (app()->bound('swoole')) {
|
||||
Task::deliver($task);
|
||||
}
|
||||
}
|
||||
}
|
||||
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\Tasks\ZincSearchSyncTask;
|
||||
|
||||
class WebSocketDialogMsgObserver extends AbstractObserver
|
||||
{
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "created" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function created(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
self::taskDeliver(new ZincSearchSyncTask('sync', $webSocketDialogMsg->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "updated" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function updated(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
self::taskDeliver(new ZincSearchSyncTask('sync', $webSocketDialogMsg->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "deleted" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function deleted(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
self::taskDeliver(new ZincSearchSyncTask('delete', $webSocketDialogMsg->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\Deleted;
|
||||
use App\Models\UserBot;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Tasks\ZincSearchSyncTask;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class WebSocketDialogUserObserver
|
||||
class WebSocketDialogUserObserver extends AbstractObserver
|
||||
{
|
||||
/**
|
||||
* Handle the WebSocketDialogUser "created" event.
|
||||
@@ -29,6 +31,12 @@ class WebSocketDialogUserObserver
|
||||
}
|
||||
}
|
||||
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||
self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray()));
|
||||
//
|
||||
$dialog = $webSocketDialogUser->webSocketDialog;
|
||||
if ($dialog) {
|
||||
$dialog->dispatchMemberWebhook(UserBot::WEBHOOK_EVENT_MEMBER_JOIN, $webSocketDialogUser->userid, intval($webSocketDialogUser->inviter));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +47,7 @@ class WebSocketDialogUserObserver
|
||||
*/
|
||||
public function updated(WebSocketDialogUser $webSocketDialogUser)
|
||||
{
|
||||
//
|
||||
self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +59,13 @@ class WebSocketDialogUserObserver
|
||||
public function deleted(WebSocketDialogUser $webSocketDialogUser)
|
||||
{
|
||||
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||
self::taskDeliver(new ZincSearchSyncTask('deleteUser', $webSocketDialogUser->toArray()));
|
||||
//
|
||||
$dialog = $webSocketDialogUser->webSocketDialog;
|
||||
if ($dialog) {
|
||||
$operatorId = $webSocketDialogUser->operator_id ?? 0;
|
||||
$dialog->dispatchMemberWebhook(UserBot::WEBHOOK_EVENT_MEMBER_LEAVE, $webSocketDialogUser->userid, intval($operatorId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,33 +2,101 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Module\ClientContext;
|
||||
use Illuminate\Http\Request;
|
||||
use Swoole\Coroutine;
|
||||
|
||||
/**
|
||||
* 请求上下文
|
||||
*/
|
||||
class RequestContext
|
||||
{
|
||||
/** @var array<string, array<string, mixed>> */
|
||||
private static array $context = [];
|
||||
/** @var string 请求ID的上下文键 */
|
||||
private const CONTEXT_KEY = 'request_id';
|
||||
|
||||
private const REQUEST_ID_PREFIX = 'req_';
|
||||
/** @var string 请求ID前缀 */
|
||||
private const REQUEST_ID_PREFIX = 'req';
|
||||
|
||||
/** @var int 上下文的TTL(生存时间) */
|
||||
private const TTL_SECONDS = 3600; // 上下文 TTL 为 1 小时
|
||||
|
||||
/** @var array<string, ClientContext> 存储每个请求的上下文数据 */
|
||||
private static array $context = [];
|
||||
|
||||
/**
|
||||
* 生成请求唯一ID
|
||||
*/
|
||||
public static function generateRequestId(): string
|
||||
{
|
||||
return self::REQUEST_ID_PREFIX . uniqid() . mt_rand(10000, 99999);
|
||||
$pid = getmypid();
|
||||
$cid = Coroutine::getCid() ?? 0;
|
||||
$microtime = str_replace('.', '', microtime(true));
|
||||
return self::REQUEST_ID_PREFIX . '_' . $pid . '_' . $cid . '_' . $microtime . '_' . mt_rand(1000, 9999);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求ID
|
||||
*/
|
||||
private static function getCurrentRequestId(): ?string
|
||||
public static function getCurrentRequestId($requestId = null): ?string
|
||||
{
|
||||
/** @var Request $request */
|
||||
// 如果提供了有效的请求ID,直接返回
|
||||
if ($requestId && str_starts_with($requestId, self::REQUEST_ID_PREFIX)) {
|
||||
return $requestId;
|
||||
}
|
||||
|
||||
// 尝试从当前请求获取
|
||||
$request = request();
|
||||
return $request?->requestId;
|
||||
if ($request && method_exists($request, 'attributes') && $request->attributes) {
|
||||
if (!$request->attributes->has(static::CONTEXT_KEY)) {
|
||||
$request->attributes->set(static::CONTEXT_KEY, self::generateRequestId());
|
||||
}
|
||||
return $request->attributes->get(static::CONTEXT_KEY);
|
||||
}
|
||||
|
||||
// 如果没有请求上下文,生成一个新的请求ID
|
||||
return self::generateRequestId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求的上下文示例
|
||||
*/
|
||||
public static function getCurrentRequestContext($requestId = null): ?ClientContext
|
||||
{
|
||||
$requestId = self::getCurrentRequestId($requestId);
|
||||
if ($requestId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset(self::$context[$requestId])) {
|
||||
// 如果上下文不存在,则创建一个新的上下文
|
||||
self::$context[$requestId] = new ClientContext();
|
||||
} else {
|
||||
// 如果上下文已存在,更新访问时间
|
||||
self::$context[$requestId]->update();
|
||||
}
|
||||
|
||||
return self::$context[$requestId];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期上下文数据,防止内存泄漏
|
||||
*/
|
||||
public static function cleanExpired(): void
|
||||
{
|
||||
$now = microtime(true);
|
||||
|
||||
// 清理过期的上下文
|
||||
foreach (self::$context as $requestId => $context) {
|
||||
if ($now - $context->updatedAt > self::TTL_SECONDS) {
|
||||
unset(self::$context[$requestId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 设置请求上下文
|
||||
*
|
||||
@@ -39,13 +107,34 @@ class RequestContext
|
||||
*/
|
||||
public static function set(string $key, mixed $value, ?string $requestId = null): void
|
||||
{
|
||||
$requestId = $requestId ?? self::getCurrentRequestId();
|
||||
if ($requestId === null) {
|
||||
$context = self::getCurrentRequestContext($requestId);
|
||||
if ($context === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$context[$requestId] ??= [];
|
||||
self::$context[$requestId][$key] = $value;
|
||||
$context->set($key, $value);
|
||||
|
||||
// 概率性清理,避免频繁清理影响性能
|
||||
if (mt_rand(1, 100) === 1) {
|
||||
self::cleanExpired();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置上下文数据
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @param string|null $requestId
|
||||
* @return void
|
||||
*/
|
||||
public static function setMultiple(array $data, ?string $requestId = null): void
|
||||
{
|
||||
$context = self::getCurrentRequestContext($requestId);
|
||||
if ($context === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->setMultiple($data);
|
||||
}
|
||||
|
||||
// 与 set 方法的区别是,save 方法会返回传入的 value 值
|
||||
@@ -65,12 +154,28 @@ class RequestContext
|
||||
*/
|
||||
public static function get(string $key, mixed $default = null, ?string $requestId = null): mixed
|
||||
{
|
||||
$requestId = $requestId ?? self::getCurrentRequestId();
|
||||
if ($requestId === null) {
|
||||
$context = self::getCurrentRequestContext($requestId);
|
||||
if ($context === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return self::$context[$requestId][$key] ?? $default;
|
||||
return $context->get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求的所有上下文数据
|
||||
*
|
||||
* @param string|null $requestId
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function getAll(?string $requestId = null): array
|
||||
{
|
||||
$context = self::getCurrentRequestContext($requestId);
|
||||
if ($context === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $context->context ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,12 +187,12 @@ class RequestContext
|
||||
*/
|
||||
public static function has(string $key, ?string $requestId = null): bool
|
||||
{
|
||||
$requestId = $requestId ?? self::getCurrentRequestId();
|
||||
if ($requestId === null) {
|
||||
$context = self::getCurrentRequestContext($requestId);
|
||||
if ($context === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isset(self::$context[$requestId][$key]);
|
||||
return $context->has($key);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,9 +201,9 @@ class RequestContext
|
||||
* @param string|null $requestId
|
||||
* @return void
|
||||
*/
|
||||
public static function clear(?string $requestId = null): void
|
||||
public static function clean(?string $requestId = null): void
|
||||
{
|
||||
$requestId = $requestId ?? self::getCurrentRequestId();
|
||||
$requestId = self::getCurrentRequestId($requestId);
|
||||
if ($requestId === null) {
|
||||
return;
|
||||
}
|
||||
@@ -106,37 +211,63 @@ class RequestContext
|
||||
unset(self::$context[$requestId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求的所有上下文数据
|
||||
*
|
||||
* @param string|null $requestId
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function getAll(?string $requestId = null): array
|
||||
{
|
||||
$requestId = $requestId ?? self::getCurrentRequestId();
|
||||
if ($requestId === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return self::$context[$requestId] ?? [];
|
||||
}
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 批量设置上下文数据
|
||||
* 更新请求的基本URL
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @param string|null $requestId
|
||||
* @param Request $request
|
||||
* @return void
|
||||
*/
|
||||
public static function setMultiple(array $data, ?string $requestId = null): void
|
||||
public static function updateBaseUrl($request)
|
||||
{
|
||||
$requestId = $requestId ?? self::getCurrentRequestId();
|
||||
if ($requestId === null) {
|
||||
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);
|
||||
}
|
||||
|
||||
self::$context[$requestId] ??= [];
|
||||
self::$context[$requestId] = array_merge(self::$context[$requestId], $data);
|
||||
/**
|
||||
* 替换请求的基本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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace App\Tasks;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Models\TaskWorker;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
|
||||
@@ -6,8 +6,6 @@ use App\Models\ProjectTask;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
class AppPushTask extends AbstractTask
|
||||
{
|
||||
public function __construct()
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
namespace App\Tasks;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Models\ProjectTask;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
@@ -44,6 +42,7 @@ class AutoArchivedTask extends AbstractTask
|
||||
->whereNotNull('project_tasks.complete_at')
|
||||
->where('project_tasks.complete_at', '<=', Carbon::now()->subDays($archivedDay))
|
||||
->where('project_tasks.archived_userid', 0)
|
||||
->where('project_tasks.parent_id', 0)
|
||||
->whereNull('project_tasks.archived_at')
|
||||
->where('projects.archive_method', '!=', 'custom')
|
||||
->take(100)
|
||||
@@ -65,6 +64,7 @@ class AutoArchivedTask extends AbstractTask
|
||||
->join('projects', 'projects.id', '=', 'project_tasks.project_id')
|
||||
->whereNotNull('project_tasks.complete_at')
|
||||
->where('project_tasks.archived_userid', 0)
|
||||
->where('project_tasks.parent_id', 0)
|
||||
->whereNull('project_tasks.archived_at')
|
||||
->where('projects.archive_method', 'custom')
|
||||
->whereRaw("DATEDIFF(NOW(), {$prefix}project_tasks.complete_at) >= {$prefix}projects.archive_days")
|
||||
|
||||
@@ -2,74 +2,105 @@
|
||||
|
||||
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\Apps;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Ihttp;
|
||||
use App\Module\TextExtractor;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use DB;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
|
||||
/**
|
||||
* 推送会话消息
|
||||
* Class BotReceiveMsgTask
|
||||
* 机器人消息接收处理任务
|
||||
*
|
||||
* @package App\Tasks
|
||||
*/
|
||||
class BotReceiveMsgTask extends AbstractTask
|
||||
{
|
||||
protected $userid; // 机器人ID
|
||||
protected $msgId; // 消息ID
|
||||
protected $mention; // 是否提及
|
||||
protected $mention; // 是否提及(机器人被@,不含@所有人)
|
||||
protected $mentionOther; // 是否提及其他人
|
||||
protected $client = []; // 客户端信息(版本、语言、平台)
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* 初始化机器人消息处理任务的相关参数
|
||||
*
|
||||
* @param int $userid 机器人用户ID
|
||||
* @param int $msgId 消息ID
|
||||
* @param array $mentions 提及的用户ID数组
|
||||
* @param array $client 客户端信息(版本、语言、平台等)
|
||||
*/
|
||||
public function __construct($userid, $msgId, $mentions, $client = [])
|
||||
{
|
||||
parent::__construct(...func_get_args());
|
||||
$this->userid = $userid;
|
||||
$this->msgId = $msgId;
|
||||
$this->mention = array_intersect([$userid], $mentions) ? 1 : 0; // 是否提及(不含@所有人)
|
||||
$this->mentionOther = array_diff($mentions, [0, $userid]) ? 1 : 0; // 是否提及其他人
|
||||
$this->mention = array_intersect([$userid], $mentions) ? 1 : 0;
|
||||
$this->mentionOther = array_diff($mentions, [0, $userid]) ? 1 : 0;
|
||||
$this->client = is_array($client) ? $client : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务开始执行
|
||||
* 验证机器人用户和消息的有效性,然后处理机器人接收到的消息
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
// 判断是否是机器人用户
|
||||
$botUser = User::whereUserid($this->userid)->whereBot(1)->first();
|
||||
if (empty($botUser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断消息是否存在
|
||||
$msg = WebSocketDialogMsg::with(['user'])->find($this->msgId);
|
||||
if (empty($msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记消息已读
|
||||
$msg->readSuccess($botUser->userid);
|
||||
if (!$msg->user?->bot) {
|
||||
$this->botReceiveBusiness($msg, $botUser);
|
||||
|
||||
// 判断消息是否是机器人发送的则不处理,避免循环
|
||||
if (!$msg->user || $msg->user->bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理机器人消息
|
||||
$this->processMessage($msg, $botUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务结束回调
|
||||
* 当前为空实现,可在此处添加清理逻辑
|
||||
*/
|
||||
public function end()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人处理消息
|
||||
* @param WebSocketDialogMsg $msg
|
||||
* @param User $botUser
|
||||
* 处理机器人接收到的消息
|
||||
* 根据消息类型和机器人类型执行相应的处理逻辑
|
||||
*
|
||||
* @param WebSocketDialogMsg $msg 接收到的消息对象
|
||||
* @param User $botUser 机器人用户对象
|
||||
* @return void
|
||||
*/
|
||||
private function botReceiveBusiness(WebSocketDialogMsg $msg, User $botUser)
|
||||
private function processMessage(WebSocketDialogMsg $msg, User $botUser)
|
||||
{
|
||||
// 位置消息(仅支持签到机器人)
|
||||
if ($msg->type === 'location') {
|
||||
@@ -86,8 +117,14 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
|
||||
// 提取指令
|
||||
$command = $this->extractCommand($msg, $this->mention);
|
||||
if (empty($command)) {
|
||||
$sendText = $msg->extractMessageContent();
|
||||
$replyText = null;
|
||||
if ($msg->reply_id && $replyMsg = WebSocketDialogMsg::find($msg->reply_id)) {
|
||||
$replyText = $replyMsg->extractMessageContent();
|
||||
}
|
||||
|
||||
// 没有提取到指令,则不处理
|
||||
if (empty($sendText) && empty($replyText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,22 +134,17 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是群聊,未提及丹提及其他人
|
||||
if ($dialog->type === 'group' && !$this->mention && $this->mentionOther) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 推送Webhook
|
||||
$this->botWebhookBusiness($command, $msg, $botUser, $dialog);
|
||||
$this->handleWebhookRequest($sendText, $replyText, $msg, $dialog, $botUser);
|
||||
|
||||
// 仅支持用户会话
|
||||
// 如果不是用户对话,则只处理到这里
|
||||
if ($dialog->type !== 'user') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 签到机器人
|
||||
if ($botUser->email === 'check-in@bot.system') {
|
||||
$content = UserBot::checkinBotQuickMsg($command, $msg->userid);
|
||||
$content = UserBot::checkinBotQuickMsg($sendText, $msg->userid);
|
||||
if ($content) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
|
||||
'type' => 'content',
|
||||
@@ -123,7 +155,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
|
||||
// 隐私机器人
|
||||
if ($botUser->email === 'anon-msg@bot.system') {
|
||||
$array = UserBot::anonBotQuickMsg($command);
|
||||
$array = UserBot::anonBotQuickMsg($sendText);
|
||||
if ($array) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
|
||||
'type' => 'content',
|
||||
@@ -134,7 +166,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
|
||||
// 管理机器人
|
||||
if (str_starts_with($command, '/')) {
|
||||
if (str_starts_with($sendText, '/')) {
|
||||
// 判断是否是机器人管理员
|
||||
if ($botUser->email === 'bot-manager@bot.system') {
|
||||
$isManager = true;
|
||||
@@ -149,7 +181,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
|
||||
// 指令处理
|
||||
$array = Base::newTrim(explode(" ", "{$command} "));
|
||||
$array = Base::newTrim(explode(" ", "{$sendText} "));
|
||||
$type = $array[0];
|
||||
$data = [];
|
||||
$content = "";
|
||||
@@ -185,7 +217,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
case '/hello':
|
||||
case '/info':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
$data = $this->getBotInfo($botId, $msg->userid);
|
||||
if (!$data) {
|
||||
$content = "机器人不存在。";
|
||||
}
|
||||
@@ -195,32 +227,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;
|
||||
|
||||
@@ -234,7 +245,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
$content = "机器人名称由2-20个字符组成。";
|
||||
break;
|
||||
}
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
$data = $this->getBotInfo($botId, $msg->userid);
|
||||
if ($data) {
|
||||
$data->nickname = $nameString;
|
||||
$data->az = Base::getFirstCharter($nameString);
|
||||
@@ -251,7 +262,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
*/
|
||||
case '/deletebot':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
$data = $this->getBotInfo($botId, $msg->userid);
|
||||
if ($data) {
|
||||
$data->deleteUser('delete bot');
|
||||
} else {
|
||||
@@ -264,7 +275,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
*/
|
||||
case '/token':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
$data = $this->getBotInfo($botId, $msg->userid);
|
||||
if ($data) {
|
||||
User::generateToken($data);
|
||||
} else {
|
||||
@@ -277,7 +288,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
*/
|
||||
case '/revoke':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
$data = $this->getBotInfo($botId, $msg->userid);
|
||||
if ($data) {
|
||||
$data->encrypt = Base::generatePassword(6);
|
||||
$data->password = Doo::md5s(Base::generatePassword(32), $data->encrypt);
|
||||
@@ -293,7 +304,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
case '/clearday':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$clearDay = $isManager ? $array[2] : $array[1];
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
$data = $this->getBotInfo($botId, $msg->userid);
|
||||
if ($data) {
|
||||
$userBot = UserBot::whereBotId($botId)->whereUserid($msg->userid)->first();
|
||||
if ($userBot) {
|
||||
@@ -314,7 +325,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
case '/webhook':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$webhookUrl = $isManager ? $array[2] : $array[1];
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
$data = $this->getBotInfo($botId, $msg->userid);
|
||||
if (strlen($webhookUrl) > 255) {
|
||||
$content = "webhook地址最长仅支持255个字符。";
|
||||
} elseif ($data) {
|
||||
@@ -337,7 +348,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
case '/dialog':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$nameKey = $isManager ? $array[2] : $array[1];
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
$data = $this->getBotInfo($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'])
|
||||
@@ -349,7 +360,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
->orderByDesc('u.last_at')
|
||||
->take(20)
|
||||
->get()
|
||||
->map(function($item) use ($data) {
|
||||
->map(function ($item) use ($data) {
|
||||
return WebSocketDialog::synthesizeData($item, $data->userid);
|
||||
})
|
||||
->all();
|
||||
@@ -392,6 +403,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
default => '不支持的指令',
|
||||
};
|
||||
if ($type == '/api') {
|
||||
$msgData['email'] = $botUser->email;
|
||||
$msgData['version'] = Base::getVersion();
|
||||
} elseif ($type == '/help') {
|
||||
$msgData['manager'] = $isManager;
|
||||
@@ -402,123 +414,323 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人处理 Webhook
|
||||
* @param string $command
|
||||
* @param WebSocketDialogMsg $msg
|
||||
* @param User $botUser
|
||||
* @param WebSocketDialog $dialog
|
||||
* 处理机器人Webhook请求
|
||||
* 根据机器人类型(AI机器人或用户机器人)发送相应的Webhook请求
|
||||
*
|
||||
* @param string $sendText 发送的文本内容
|
||||
* @param string $replyText 回复的文本内容
|
||||
* @param WebSocketDialogMsg $msg 消息对象
|
||||
* @param WebSocketDialog $dialog 对话对象
|
||||
* @param User $botUser 机器人用户对象
|
||||
* @return void
|
||||
*/
|
||||
private function botWebhookBusiness(string $command, WebSocketDialogMsg $msg, User $botUser, WebSocketDialog $dialog)
|
||||
private function handleWebhookRequest($sendText, $replyText, WebSocketDialogMsg $msg, WebSocketDialog $dialog, User $botUser)
|
||||
{
|
||||
$serverUrl = 'http://' . env('APP_IPPR') . '.3';
|
||||
$webhookUrl = null;
|
||||
$userBot = null;
|
||||
$extras = [];
|
||||
$errorContent = null;
|
||||
if (preg_match('/^ai-(.*?)@bot\.system$/', $botUser->email, $matches)) {
|
||||
// AI机器人
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$type = $matches[1];
|
||||
$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 ($dialog->session_id) {
|
||||
$extras['context_key'] = 'session_' . $dialog->session_id;
|
||||
}
|
||||
if ($type === 'wenxin') {
|
||||
$extras['api_key'] .= ':' . $setting['wenxin_secret'];
|
||||
}
|
||||
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) {
|
||||
$replyMsg = WebSocketDialogMsg::find($msg->reply_id);
|
||||
$replyCommand = '';
|
||||
if ($replyMsg) {
|
||||
$replyCommand = $this->extractCommand($replyMsg);
|
||||
if ($replyCommand) {
|
||||
$replyCommand = "<quoted>" . Base::cutStr($replyCommand, 2000) . "</quoted>\n\nThe content within the above <quoted> tags is a citation.\n\n";
|
||||
$extras = ['timestamp' => time()];
|
||||
|
||||
try {
|
||||
if ($botUser->isAiBot($type)) {
|
||||
// AI机器人,不处理带有留言的转发消息,因为他要处理那条留言消息
|
||||
if (Base::val($msg->msg, 'forward_data.leave')) {
|
||||
return;
|
||||
}
|
||||
// 如果是群聊,没有@自己,则不处理
|
||||
if ($dialog->type === 'group' && !$this->mention) {
|
||||
return;
|
||||
}
|
||||
// 检查客户端版本
|
||||
if (in_array($this->client['platform'], ['win', 'mac', 'web']) && !Base::judgeClientVersion("0.41.11", $this->client['version'])) {
|
||||
throw new Exception('当前客户端版本低(所需版本≥v0.41.11)。');
|
||||
}
|
||||
// 判断AI应用是否安装
|
||||
if (!Apps::isInstalled('ai')) {
|
||||
throw new Exception('应用「AI Assistant」未安装');
|
||||
}
|
||||
// 整理机器人参数
|
||||
$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' => 'http://nginx',
|
||||
];
|
||||
if ($setting[$type . '_temperature']) {
|
||||
$extras['temperature'] = floatval($setting[$type . '_temperature']);
|
||||
}
|
||||
if ($msg->msg['model_name']) {
|
||||
$extras['model_name'] = $msg->msg['model_name'];
|
||||
}
|
||||
// 提取模型“思考”参数
|
||||
$thinkPatterns = [
|
||||
"/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/",
|
||||
"/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/"
|
||||
];
|
||||
$thinkMatch = [];
|
||||
foreach ($thinkPatterns as $pattern) {
|
||||
if (preg_match($pattern, $extras['model_name'], $thinkMatch)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$command = $replyCommand . $command;
|
||||
if ($thinkMatch && !empty($thinkMatch[1])) {
|
||||
$extras['model_name'] = $thinkMatch[1];
|
||||
$extras['max_tokens'] = 20000;
|
||||
$extras['thinking'] = 4096;
|
||||
$extras['temperature'] = 1.0;
|
||||
}
|
||||
// 设定会话ID
|
||||
if ($dialog->session_id) {
|
||||
$extras['context_key'] = 'session_' . $dialog->session_id;
|
||||
}
|
||||
// 设置文心一言的API密钥
|
||||
if ($type === 'wenxin') {
|
||||
$extras['api_key'] .= ':' . $setting['wenxin_secret'];
|
||||
}
|
||||
// 群聊清理上下文(群聊不使用上下文)
|
||||
if ($dialog->type === 'group') {
|
||||
$extras['before_clear'] = 1;
|
||||
}
|
||||
if ($type === 'ollama') {
|
||||
if (empty($extras['base_url'])) {
|
||||
throw new Exception('机器人未启用。');
|
||||
}
|
||||
if (empty($extras['api_key'])) {
|
||||
$extras['api_key'] = Base::strRandom(6);
|
||||
}
|
||||
}
|
||||
if (empty($extras['api_key'])) {
|
||||
throw new Exception('机器人未启用。');
|
||||
}
|
||||
$this->generateSystemPromptForAI($msg->userid, $dialog, $botUser, $extras);
|
||||
// 转换提及格式
|
||||
if ($replyText) {
|
||||
$sendText = <<<EOF
|
||||
<quoted_content>
|
||||
{$replyText}
|
||||
</quoted_content>
|
||||
|
||||
上述 quoted_content 标签中的内容为引用。
|
||||
|
||||
{$sendText}
|
||||
EOF;
|
||||
}
|
||||
$webhookUrl = "http://nginx/ai/chat";
|
||||
} else {
|
||||
// 用户机器人
|
||||
if ($botUser->isUserBot() && str_starts_with($sendText, '/')) {
|
||||
// 用户机器人不处理指令类型命令
|
||||
return;
|
||||
}
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->first();
|
||||
if (!$userBot || !$userBot->shouldDispatchWebhook(UserBot::WEBHOOK_EVENT_MESSAGE)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->AIGenerateSystemMessageOrBeforeText($msg->userid, $dialog, $extras);
|
||||
$webhookUrl = "{$serverUrl}/ai/chat";
|
||||
} else {
|
||||
// 用户机器人
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->first();
|
||||
$webhookUrl = $userBot?->webhook_url;
|
||||
}
|
||||
if ($errorContent) {
|
||||
} catch (\Exception $e) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
|
||||
'type' => 'content',
|
||||
'content' => $errorContent,
|
||||
'content' => $e->getMessage(),
|
||||
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
|
||||
return;
|
||||
}
|
||||
if (!preg_match("/^https?:\/\//", $webhookUrl)) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
try {
|
||||
$data = [
|
||||
'text' => $command,
|
||||
'token' => User::generateToken($botUser),
|
||||
'dialog_id' => $dialog->id,
|
||||
'dialog_type' => $dialog->type,
|
||||
'msg_id' => $msg->id,
|
||||
'msg_uid' => $msg->userid,
|
||||
'mention' => $this->mention ? 1 : 0,
|
||||
'bot_uid' => $botUser->userid,
|
||||
'version' => Base::getVersion(),
|
||||
'extras' => Base::array2json($extras)
|
||||
|
||||
// 基本请求数据
|
||||
$data = [
|
||||
'event' => UserBot::WEBHOOK_EVENT_MESSAGE,
|
||||
'text' => $sendText,
|
||||
'reply_text' => $replyText,
|
||||
'token' => User::generateToken($botUser),
|
||||
'session_id' => $dialog->session_id,
|
||||
'dialog_id' => $dialog->id,
|
||||
'dialog_type' => $dialog->type,
|
||||
'msg_id' => $msg->id,
|
||||
'msg_uid' => $msg->userid,
|
||||
'mention' => $this->mention ? 1 : 0,
|
||||
'bot_uid' => $botUser->userid,
|
||||
'extras' => Base::array2json($extras),
|
||||
'version' => Base::getVersion(),
|
||||
'timestamp' => time(),
|
||||
];
|
||||
// 添加用户信息
|
||||
$userInfo = User::find($msg->userid);
|
||||
if ($userInfo) {
|
||||
$data['msg_user'] = [
|
||||
'userid' => $userInfo->userid,
|
||||
'email' => $userInfo->email,
|
||||
'nickname' => $userInfo->nickname,
|
||||
'profession' => $userInfo->profession,
|
||||
'lang' => $userInfo->lang,
|
||||
'token' => User::generateTokenNoDevice($userInfo, now()->addHour()),
|
||||
];
|
||||
$res = Ihttp::ihttp_post($webhookUrl, $data, 30);
|
||||
if ($userBot) {
|
||||
$userBot->webhook_num++;
|
||||
$userBot->save();
|
||||
}
|
||||
|
||||
$result = null;
|
||||
if ($userBot) {
|
||||
$result = $userBot->dispatchWebhook(UserBot::WEBHOOK_EVENT_MESSAGE, $data);
|
||||
} else {
|
||||
try {
|
||||
$result = Ihttp::ihttp_post($webhookUrl, $data, 30);
|
||||
} catch (\Throwable $th) {
|
||||
info(Base::array2json([
|
||||
'webhook_url' => $webhookUrl,
|
||||
'data' => $data,
|
||||
'error' => $th->getMessage(),
|
||||
]));
|
||||
}
|
||||
if ($res['data'] && $data = Base::json2array($res['data'])) {
|
||||
if ($data['code'] != 200 && $data['message']) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $res['data']['message']], $botUser->userid, false, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
if ($result && isset($result['data'])) {
|
||||
$responseData = Base::json2array($result['data']);
|
||||
if (($responseData['code'] ?? 0) === 200 && !empty($responseData['message'])) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', [
|
||||
'text' => $responseData['message']
|
||||
], $botUser->userid, false, false, true);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
info(Base::array2json([
|
||||
'bot_userid' => $botUser->userid,
|
||||
'dialog' => $dialog->id,
|
||||
'msg' => $msg->id,
|
||||
'webhook_url' => $webhookUrl,
|
||||
'error' => $th->getMessage(),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器人信息
|
||||
* @param $botId
|
||||
* @param $userid
|
||||
* @return User
|
||||
* 为AI机器人生成系统提示词
|
||||
* 根据对话类型(用户对话、项目群、任务群、部门群等)生成相应的系统提示词
|
||||
*
|
||||
* @param int|null $userid 用户ID
|
||||
* @param WebSocketDialog $dialog 对话对象
|
||||
* @param User $botUser 机器人用户对象
|
||||
* @param array $extras 额外参数数组,通过引用传递以修改system_message
|
||||
* @return void
|
||||
*/
|
||||
private function botOne($botId, $userid)
|
||||
private function generateSystemPromptForAI($userid, WebSocketDialog $dialog, User $botUser, array &$extras)
|
||||
{
|
||||
// 用户自定义提示词(私聊场景优先使用)
|
||||
$customPrompt = null;
|
||||
if ($dialog->type === 'user') {
|
||||
$customPrompt = WebSocketDialogConfig::where([
|
||||
'dialog_id' => $dialog->id,
|
||||
'userid' => $userid,
|
||||
'type' => 'ai_prompt',
|
||||
])->value('value');
|
||||
}
|
||||
|
||||
$prompt = [];
|
||||
|
||||
// 1. 基础角色(自定义提示词优先)
|
||||
if ($customPrompt) {
|
||||
$prompt[] = $customPrompt;
|
||||
} elseif (!empty($extras['system_message'])) {
|
||||
$prompt[] = $extras['system_message'];
|
||||
}
|
||||
|
||||
// 2. 上下文信息
|
||||
$currentTime = Carbon::now()->toDateTimeString();
|
||||
$contextLines = [
|
||||
"您是:{$botUser->nickname}(ID: {$botUser->userid})",
|
||||
"当前对话ID:{$dialog->id}",
|
||||
"当前系统时间:{$currentTime}"
|
||||
];
|
||||
|
||||
if ($dialog->type === 'group') {
|
||||
switch ($dialog->group_type) {
|
||||
case 'project':
|
||||
$projectInfo = Project::whereDialogId($dialog->id)->first();
|
||||
if ($projectInfo) {
|
||||
$contextLines[] = "场景:项目群聊「{$projectInfo->name}」(ID: {$projectInfo->id})";
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task':
|
||||
$taskInfo = ProjectTask::with(['content'])->whereDialogId($dialog->id)->first();
|
||||
if ($taskInfo) {
|
||||
$contextLines[] = "场景:任务群聊「{$taskInfo->name}」(ID: {$taskInfo->id})";
|
||||
}
|
||||
break;
|
||||
|
||||
case 'department':
|
||||
$userDepartment = UserDepartment::whereDialogId($dialog->id)->first();
|
||||
if ($userDepartment) {
|
||||
$contextLines[] = "场景:部门群聊「{$userDepartment->name}」";
|
||||
}
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
$contextLines[] = "场景:全体成员群聊";
|
||||
break;
|
||||
}
|
||||
|
||||
// 3. 聊天历史(仅群聊)
|
||||
$chatHistory = $this->getRecentChatHistory($dialog, 15);
|
||||
if ($chatHistory) {
|
||||
$prompt[] = implode("\n", $contextLines);
|
||||
$prompt[] = "最近的对话记录:\n{$chatHistory}";
|
||||
} else {
|
||||
$prompt[] = implode("\n", $contextLines);
|
||||
}
|
||||
} else {
|
||||
$prompt[] = implode("\n", $contextLines);
|
||||
}
|
||||
|
||||
$extras['system_message'] = implode("\n----\n", array_filter($prompt));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近的聊天记录
|
||||
* @param WebSocketDialog $dialog 对话对象
|
||||
* @param int $limit 获取的聊天记录条数
|
||||
* @return string|null 格式化后的聊天记录字符串,无记录时返回null
|
||||
*/
|
||||
private function getRecentChatHistory(WebSocketDialog $dialog, $limit = 10): ?string
|
||||
{
|
||||
// 构建查询条件
|
||||
$conditions = [
|
||||
['dialog_id', '=', $dialog->id],
|
||||
['id', '<', $this->msgId],
|
||||
];
|
||||
|
||||
// 如果有会话ID,添加会话过滤条件
|
||||
if ($dialog->session_id > 0) {
|
||||
$conditions[] = ['session_id', '=', $dialog->session_id];
|
||||
}
|
||||
|
||||
// 查询最近$limit条消息并格式化
|
||||
$chatMessages = WebSocketDialogMsg::with(['user'])
|
||||
->where($conditions)
|
||||
->orderByDesc('id')
|
||||
->take($limit)
|
||||
->get()
|
||||
->map(function (WebSocketDialogMsg $message) {
|
||||
$userName = $message->user?->nickname ?? '未知用户';
|
||||
$content = $message->extractMessageContent(500);
|
||||
if (!$content) {
|
||||
return null;
|
||||
}
|
||||
// 使用XML标签格式,确保AI能清晰识别边界
|
||||
// 对用户名进行HTML转义,防止特殊字符破坏格式
|
||||
$safeUserName = htmlspecialchars($userName, ENT_QUOTES, 'UTF-8');
|
||||
return "<message userid=\"{$message->userid}\" nickname=\"{$safeUserName}\">\n{$content}\n</message>";
|
||||
})
|
||||
->reverse() // 反转集合,让时间顺序正确(最早的在前)
|
||||
->filter() // 过滤掉空内容的消息
|
||||
->values() // 重新索引数组
|
||||
->toArray();
|
||||
|
||||
return empty($chatMessages) ? null : implode("\n", $chatMessages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器人信息
|
||||
* 根据机器人ID和用户ID获取机器人的详细信息,包括清理设置和webhook配置
|
||||
*
|
||||
* @param int $botId 机器人ID
|
||||
* @param int $userid 用户ID
|
||||
* @return User|null 机器人用户对象,如果不存在则返回null
|
||||
*/
|
||||
private function getBotInfo($botId, $userid)
|
||||
{
|
||||
$botId = intval($botId);
|
||||
$userid = intval($userid);
|
||||
@@ -538,150 +750,4 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取消息指令(提取消息内容)
|
||||
* @param WebSocketDialogMsg $msg
|
||||
* @param bool $mention
|
||||
* @return string
|
||||
*/
|
||||
private function extractCommand(WebSocketDialogMsg $msg, 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);
|
||||
}
|
||||
} else {
|
||||
$command = trim(strip_tags($original));
|
||||
}
|
||||
if (empty($command)) {
|
||||
return '';
|
||||
}
|
||||
return $command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AI系统提示词或前置消息
|
||||
* @param int|null $userid
|
||||
* @param WebSocketDialog $dialog
|
||||
* @param array $extras
|
||||
* @return void
|
||||
*/
|
||||
private function AIGenerateSystemMessageOrBeforeText(int|null $userid, WebSocketDialog $dialog, array &$extras)
|
||||
{
|
||||
$system_message = null;
|
||||
$before_text = [];
|
||||
switch ($dialog->type) {
|
||||
case "user":
|
||||
$aiPrompt = WebSocketDialogConfig::where([
|
||||
'dialog_id' => $dialog->id,
|
||||
'userid' => $userid,
|
||||
'type' => 'ai_prompt',
|
||||
])->value('value');
|
||||
if ($aiPrompt) {
|
||||
$system_message = $aiPrompt;
|
||||
}
|
||||
break;
|
||||
case "group":
|
||||
switch ($dialog->group_type) {
|
||||
case 'user':
|
||||
break;
|
||||
case 'project':
|
||||
$projectInfo = Project::whereDialogId($dialog->id)->first();
|
||||
if ($projectInfo) {
|
||||
$projectText = "当前我在项目【{$projectInfo->name}】中";
|
||||
if ($projectInfo->archived_at) {
|
||||
$projectText .= ",此项目已经归档";
|
||||
}
|
||||
$before_text[] = $projectText;
|
||||
if ($projectInfo->desc) {
|
||||
$before_text[] = "项目描述:{$projectInfo->desc}";
|
||||
}
|
||||
$before_text[] = <<<EOF
|
||||
如果你判断我想要添加任务,请按照以下格式回复:
|
||||
|
||||
::: 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) {
|
||||
$taskText = "当前我在任务【{$taskInfo->name}】中";
|
||||
if ($taskInfo->archived_at) {
|
||||
$taskText .= ",此任务已经归档";
|
||||
} elseif ($taskInfo->complete_at) {
|
||||
$taskText .= ",此任务已经完成";
|
||||
} elseif ($taskInfo->end_at && Carbon::parse($taskInfo->end_at)->lt(Carbon::now())) {
|
||||
$taskText .= ",此任务已经过期";
|
||||
}
|
||||
$before_text[] = $taskText;
|
||||
if ($taskInfo->content) {
|
||||
$taskDesc = $taskInfo->content?->getContentInfo();
|
||||
if ($taskDesc) {
|
||||
$converter = new HtmlConverter(['strip_tags' => true]);
|
||||
$descContent = Base::cutStr($converter->convert($taskDesc['content']), 2000);
|
||||
$before_text[] = <<<EOF
|
||||
任务描述:
|
||||
```md
|
||||
{$descContent}
|
||||
```
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
$subTask = ProjectTask::select(['id', 'name', 'complete_at', 'end_at'])->whereParentId($taskInfo->id)->get();
|
||||
if ($subTask->isNotEmpty()) {
|
||||
$subTaskContent = $subTask->map(function($item) {
|
||||
$status = "";
|
||||
if ($item->complete_at) {
|
||||
$status = " (已完成)";
|
||||
} elseif ($item->end_at && Carbon::parse($item->end_at)->lt(Carbon::now())) {
|
||||
$status = " (已过期)";
|
||||
}
|
||||
return " - {$item->name} {$status}";
|
||||
})->join("\n");
|
||||
if ($subTaskContent) {
|
||||
$before_text[] = <<<EOF
|
||||
子任务列表:
|
||||
{$subTaskContent}
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
$before_text[] = <<<EOF
|
||||
如果你判断我想要添加子任务,请按照以下格式回复:
|
||||
|
||||
::: create-subtask-list
|
||||
title: 子任务标题1
|
||||
title: 子任务标题2
|
||||
:::
|
||||
EOF;
|
||||
}
|
||||
break;
|
||||
case 'all':
|
||||
$before_text[] = "当前我团队【全体成员】的群聊中";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($system_message) {
|
||||
$extras['system_message'] = $system_message;
|
||||
}
|
||||
if ($before_text) {
|
||||
$extras['before_text'] = Base::newTrim($before_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ use App\Module\Timer;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
class CheckinRemindTask extends AbstractTask
|
||||
{
|
||||
public function __construct()
|
||||
|
||||
@@ -9,8 +9,6 @@ use Carbon\Carbon;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
class CloseMeetingRoomTask extends AbstractTask
|
||||
{
|
||||
public function __construct()
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Models\UserBot;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use Carbon\Carbon;
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Tasks;
|
||||
use App\Models\File;
|
||||
use App\Models\TaskWorker;
|
||||
use App\Models\Tmp;
|
||||
use App\Models\UserDevice;
|
||||
use App\Models\UmengLog;
|
||||
use App\Models\WebSocketTmpMsg;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
@@ -33,89 +35,86 @@ class DeleteTmpTask extends AbstractTask
|
||||
public function start()
|
||||
{
|
||||
switch ($this->data) {
|
||||
/**
|
||||
* 表pre_tmp_msgs
|
||||
*/
|
||||
case 'wg_tmp_msgs':
|
||||
{
|
||||
WebSocketTmpMsg::where('created_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($msgs) {
|
||||
/** @var WebSocketTmpMsg $msg */
|
||||
foreach ($msgs as $msg) {
|
||||
$msg->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
/**
|
||||
* 表pre_tmp
|
||||
*/
|
||||
case 'tmp':
|
||||
{
|
||||
Tmp::where('created_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($tmps) {
|
||||
/** @var Tmp $tmp */
|
||||
foreach ($tmps as $tmp) {
|
||||
$tmp->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
/**
|
||||
* 表pre_task_worker
|
||||
*/
|
||||
case 'task_worker':
|
||||
{
|
||||
TaskWorker::onlyTrashed()
|
||||
->where('deleted_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->forceDelete();
|
||||
}
|
||||
break;
|
||||
|
||||
/**
|
||||
* 表pre_file
|
||||
*/
|
||||
case 'file':
|
||||
{
|
||||
$day = intval(env("AUTO_EMPTY_FILE_RECYCLE", 365));
|
||||
if ($day <= 0) {
|
||||
return;
|
||||
}
|
||||
File::onlyTrashed()
|
||||
->where('deleted_at', '<', Carbon::now()->subHours($day))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($files) {
|
||||
/** @var File $file */
|
||||
foreach ($files as $file) {
|
||||
$file->forceDeleteFile();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
/**
|
||||
* tmp_file 删除临时文件
|
||||
*/
|
||||
case 'tmp_file':
|
||||
{
|
||||
$day = intval(env("AUTO_EMPTY_TEMP_FILE", 30));
|
||||
if ($day <= 0) {
|
||||
return;
|
||||
}
|
||||
$files = Base::recursiveFiles(public_path('uploads/tmp'));
|
||||
foreach ($files as $file) {
|
||||
$time = @filemtime($file);
|
||||
if ($time && $time < time() - 3600 * 24 * $day) {
|
||||
unlink($file);
|
||||
case 'tmp_msgs':
|
||||
WebSocketTmpMsg::where('created_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($msgs) {
|
||||
/** @var WebSocketTmpMsg $msg */
|
||||
foreach ($msgs as $msg) {
|
||||
$msg->delete();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'tmp':
|
||||
Tmp::where('created_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($tmps) {
|
||||
/** @var Tmp $tmp */
|
||||
foreach ($tmps as $tmp) {
|
||||
$tmp->delete();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'task_worker':
|
||||
TaskWorker::onlyTrashed()
|
||||
->where('deleted_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->forceDelete();
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
$day = intval(env("AUTO_EMPTY_FILE_RECYCLE", 365));
|
||||
if ($day <= 0) {
|
||||
return;
|
||||
}
|
||||
File::onlyTrashed()
|
||||
->where('deleted_at', '<', Carbon::now()->subHours($day))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($files) {
|
||||
/** @var File $file */
|
||||
foreach ($files as $file) {
|
||||
$file->forceDeleteFile();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'tmp_file':
|
||||
$day = intval(env("AUTO_EMPTY_TEMP_FILE", 30));
|
||||
if ($day <= 0) {
|
||||
return;
|
||||
}
|
||||
$files = Base::recursiveFiles(public_path('uploads/tmp'));
|
||||
foreach ($files as $file) {
|
||||
$time = @filemtime($file);
|
||||
if ($time && $time < time() - 3600 * 24 * $day) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user_device':
|
||||
UserDevice::where('expired_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($devices) {
|
||||
/** @var UserDevice $device */
|
||||
foreach ($devices as $device) {
|
||||
UserDevice::forget($device);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'umeng_log':
|
||||
UmengLog::where('created_at', '<', Carbon::now()->subHours($this->hours))
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($logs) {
|
||||
/** @var UmengLog $log */
|
||||
foreach ($logs as $log) {
|
||||
$log->delete();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
namespace App\Tasks;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Module\AI;
|
||||
use App\Module\Base;
|
||||
use App\Module\Extranet;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
|
||||
/**
|
||||
* 获取笑话、心灵鸡汤
|
||||
*
|
||||
@@ -29,25 +26,27 @@ class JokeSoupTask extends AbstractTask
|
||||
|
||||
public function start()
|
||||
{
|
||||
// 判断每分钟执行一次
|
||||
if (Cache::get(self::keyName("YmdHi")) == date("YmdHi")) {
|
||||
// 判断每小时执行一次
|
||||
if (Cache::get(self::keyName("YmdH")) == date("YmdH")) {
|
||||
return;
|
||||
}
|
||||
Cache::put(self::keyName("YmdHi"), date("YmdHi"), Carbon::now()->addDay());
|
||||
//
|
||||
$array = Base::json2array(Cache::get(self::keyName("jokes")));
|
||||
$data = Extranet::randJoke();
|
||||
if ($data) {
|
||||
$array[] = $data;
|
||||
Cache::put(self::keyName("YmdH"), date("YmdH"), Carbon::now()->addDay());
|
||||
|
||||
// 开始生成笑话和心灵鸡汤
|
||||
$result = AI::generateJokeAndSoup();
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget(self::keyName("YmdH"));
|
||||
return;
|
||||
}
|
||||
Cache::forever(self::keyName("jokes"), Base::array2json(array_slice($array, -200)));
|
||||
//
|
||||
$array = Base::json2array(Cache::get(self::keyName("soups")));
|
||||
$data = Extranet::soups();
|
||||
if ($data) {
|
||||
$array[] = $data;
|
||||
|
||||
// 笑话和心灵鸡汤的缓存
|
||||
foreach (['jokes', 'soups'] as $key) {
|
||||
if ($result['data'][$key] && is_array($result['data'][$key])) {
|
||||
$array = Base::json2array(Cache::get(self::keyName($key)));
|
||||
$array = array_merge($array, $result['data'][$key]);
|
||||
Cache::forever(self::keyName($key), Base::array2json(array_slice($array, -200)));
|
||||
}
|
||||
}
|
||||
Cache::forever(self::keyName("soups"), Base::array2json(array_slice($array, -200)));
|
||||
}
|
||||
|
||||
public function end()
|
||||
|
||||
@@ -4,9 +4,6 @@ namespace App\Tasks;
|
||||
|
||||
use App\Models\WebSocket;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
|
||||
/**
|
||||
* 上线、离线通知
|
||||
* Class LineTask
|
||||
|
||||
@@ -8,9 +8,6 @@ use App\Models\ProjectTask;
|
||||
use App\Models\ProjectTaskUser;
|
||||
use Carbon\Carbon;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
|
||||
/**
|
||||
* 任务重复周期
|
||||
*/
|
||||
@@ -29,6 +26,10 @@ class LoopTask extends AbstractTask
|
||||
])->chunkById(100, function ($list) {
|
||||
/** @var ProjectTask $item */
|
||||
foreach ($list as $item) {
|
||||
if ($item->parent_id > 0) {
|
||||
// 如果是子任务则不处理
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$task = $item->copyTask();
|
||||
// 工作流
|
||||
@@ -39,7 +40,7 @@ class LoopTask extends AbstractTask
|
||||
foreach ($projectFlowItem as $flowItem) {
|
||||
if ($flowItem->status == 'start') {
|
||||
$task->flow_item_id = $flowItem->id;
|
||||
$task->flow_item_name = $flowItem->status . "|" . $flowItem->name;
|
||||
$task->flow_item_name = $flowItem->status . "|" . $flowItem->name . "|" . $flowItem->color;
|
||||
if ($flowItem->userids) {
|
||||
$userids = array_values(array_unique($flowItem->userids));
|
||||
foreach ($userids as $uid) {
|
||||
@@ -63,6 +64,18 @@ class LoopTask extends AbstractTask
|
||||
$task->start_at = Carbon::parse($task->loop_at);
|
||||
$task->end_at = $task->start_at->clone()->addSeconds($diffSecond);
|
||||
}
|
||||
// 处理子任务
|
||||
$subTasks = ProjectTask::whereParentId($item->id)->get();
|
||||
if (!$subTasks->isEmpty()) {
|
||||
foreach ($subTasks as $subTask) {
|
||||
$newSubTask = $subTask->copyTask();
|
||||
$newSubTask->parent_id = $task->id;
|
||||
$newSubTask->start_at = $task->start_at;
|
||||
$newSubTask->end_at = $task->end_at;
|
||||
$newSubTask->save();
|
||||
}
|
||||
}
|
||||
//
|
||||
$task->refreshLoop(true);
|
||||
$task->addLog("创建任务来自周期任务ID:{$item->id}", [], $task->userid);
|
||||
// 清空旧周期
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
namespace App\Tasks;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Models\WebSocket;
|
||||
use App\Models\WebSocketTmpMsg;
|
||||
use App\Module\Base;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
namespace App\Tasks;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Models\UmengAlias;
|
||||
use App\Module\Base;
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ use Carbon\Carbon;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
class UnclaimedTaskRemindTask extends AbstractTask
|
||||
{
|
||||
public function __construct()
|
||||
|
||||
51
app/Tasks/UpdateSessionTitleViaAiTask.php
Normal file
51
app/Tasks/UpdateSessionTitleViaAiTask.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Models\WebSocketDialogSession;
|
||||
use App\Module\AI;
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* 通过AI接口更新对话标题
|
||||
*/
|
||||
class UpdateSessionTitleViaAiTask extends AbstractTask
|
||||
{
|
||||
protected $sessionId;
|
||||
protected $msgText;
|
||||
|
||||
public function __construct($sessionId, $msgText)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->sessionId = $sessionId;
|
||||
$this->msgText = $msgText;
|
||||
}
|
||||
|
||||
public function start()
|
||||
{
|
||||
if (empty($this->sessionId) || empty($this->msgText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$session = WebSocketDialogSession::whereId($this->sessionId)->first();
|
||||
if (!$session) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result = AI::generateTitle($this->msgText);
|
||||
if (Base::isError($result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$newTitle = $result['data']['title'];
|
||||
if ($newTitle && $newTitle != $session->title) {
|
||||
$session->title = Base::cutStr($newTitle, 100);
|
||||
$session->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
@@ -119,14 +117,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,
|
||||
@@ -136,7 +140,7 @@ class WebSocketDialogMsgTask extends AbstractTask
|
||||
];
|
||||
// 机器人收到消处理
|
||||
$botUser = User::whereUserid($userid)->whereBot(1)->first();
|
||||
if ($botUser) {
|
||||
if ($botUser) { // 避免机器人处理自己发送的消息
|
||||
$this->endArray[] = new BotReceiveMsgTask($botUser->userid, $msg->id, $mentions, $this->client);
|
||||
}
|
||||
}
|
||||
@@ -186,8 +190,10 @@ class WebSocketDialogMsgTask extends AbstractTask
|
||||
$setting = Base::setting('appPushSetting');
|
||||
if ($setting['push'] === 'open') {
|
||||
$umengTitle = User::userid2nickname($msg->userid);
|
||||
$umengBody = WebSocketDialogMsg::previewMsg($msg);
|
||||
if ($dialog->type == 'group') {
|
||||
$umengTitle = "{$dialog->getGroupName()} ($umengTitle)";
|
||||
$umengBody = $umengTitle . ': ' . $umengBody;
|
||||
$umengTitle = $dialog->getGroupName();
|
||||
}
|
||||
$langs = User::select(['userid', 'lang'])
|
||||
->whereIn('userid', $umengUserid)
|
||||
@@ -201,7 +207,7 @@ class WebSocketDialogMsgTask extends AbstractTask
|
||||
Doo::setLanguage($lang);
|
||||
$umengMsg = [
|
||||
'title' => $umengTitle,
|
||||
'body' => WebSocketDialogMsg::previewMsg($msg),
|
||||
'body' => $umengBody,
|
||||
'description' => "MID:{$msg->id}",
|
||||
'seconds' => 3600,
|
||||
'badge' => 1,
|
||||
|
||||
88
app/Tasks/ZincSearchSyncTask.php
Normal file
88
app/Tasks/ZincSearchSyncTask.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Module\Apps;
|
||||
use App\Module\ZincSearch\ZincSearchDialogMsg;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* 同步聊天数据到ZincSearch
|
||||
*/
|
||||
class ZincSearchSyncTask extends AbstractTask
|
||||
{
|
||||
private $action;
|
||||
|
||||
private $data;
|
||||
|
||||
public function __construct($action = null, $data = null)
|
||||
{
|
||||
parent::__construct(...func_get_args());
|
||||
$this->action = $action;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function start()
|
||||
{
|
||||
if (!Apps::isInstalled("search")) {
|
||||
// 如果没有安装搜索模块,则不执行
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->action) {
|
||||
case 'sync':
|
||||
// 同步消息数据
|
||||
ZincSearchDialogMsg::sync(WebSocketDialogMsg::fillInstance($this->data));
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
// 删除消息数据
|
||||
ZincSearchDialogMsg::delete(WebSocketDialogMsg::fillInstance($this->data));
|
||||
break;
|
||||
|
||||
case 'userSync':
|
||||
// 同步用户数据
|
||||
ZincSearchDialogMsg::userSync(WebSocketDialogUser::fillInstance($this->data));
|
||||
break;
|
||||
|
||||
case 'deleteUser':
|
||||
// 删除用户数据
|
||||
ZincSearchDialogMsg::delete(WebSocketDialogUser::fillInstance($this->data));
|
||||
break;
|
||||
|
||||
default:
|
||||
// 增量更新
|
||||
$this->incrementalUpdate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增量更新
|
||||
* @return void
|
||||
*/
|
||||
private function incrementalUpdate()
|
||||
{
|
||||
// 120分钟执行一次
|
||||
$time = intval(Cache::get("ZincSearchSyncTask:Time"));
|
||||
if (time() - $time < 120 * 60) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行开始,120分钟后缓存标记失效
|
||||
Cache::put("ZincSearchSyncTask:Time", time(), Carbon::now()->addMinutes(120));
|
||||
|
||||
// 开始执行同步
|
||||
@shell_exec("php /var/www/artisan zinc:sync-user-msg --i");
|
||||
|
||||
// 执行完成,5分钟后缓存标记失效(5分钟任务可重复执行)
|
||||
Cache::put("ZincSearchSyncTask:Time", time(), Carbon::now()->addMinutes(5));
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -155,8 +155,8 @@ install() {
|
||||
cat >${sitePath}/ssl.conf <<EOF
|
||||
server_name ${domain};
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/nginx/conf.d/site/ssl/${domain}.crt;
|
||||
ssl_certificate_key /etc/nginx/conf.d/site/ssl/${domain}.key;
|
||||
ssl_certificate /var/www/docker/nginx/site/ssl/${domain}.crt;
|
||||
ssl_certificate_key /var/www/docker/nginx/site/ssl/${domain}.key;
|
||||
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
395
bin/version.js
vendored
395
bin/version.js
vendored
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user