Compare commits
788 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0c09a2445c | ||
|
|
e6983e858d | ||
|
|
f8b69df955 | ||
|
|
15370a93c7 | ||
|
|
bc18aeeadc |
@@ -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
|
||||
|
||||
|
||||
49
.github/workflows/publish.yml
vendored
49
.github/workflows/publish.yml
vendored
@@ -115,9 +115,50 @@ jobs:
|
||||
})
|
||||
return data.id
|
||||
|
||||
build-client:
|
||||
pack-vendor:
|
||||
needs: [ check-version, create-release ]
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.0'
|
||||
extensions: mbstring, intl, gd, xml, zip, swoole
|
||||
tools: composer:v2
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer install
|
||||
|
||||
- name: Create Vendor Archive
|
||||
run: tar -czf vendor.tar.gz vendor/
|
||||
|
||||
- name: Upload Vendor Archive
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
const data = await fs.promises.readFile('vendor.tar.gz');
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.RELEASE_ID,
|
||||
name: 'vendor.tar.gz',
|
||||
data: data
|
||||
});
|
||||
|
||||
build-client:
|
||||
needs: [ check-version, create-release, pack-vendor ]
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
@@ -231,7 +272,7 @@ jobs:
|
||||
./cmd electron win
|
||||
|
||||
publish-release:
|
||||
needs: [ check-version, create-release, build-client ]
|
||||
needs: [ check-version, create-release, pack-vendor, build-client ]
|
||||
if: needs.check-version.outputs.should_release == 'true' && github.ref == 'refs/heads/pro'
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -258,7 +299,7 @@ jobs:
|
||||
env:
|
||||
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
|
||||
run: |
|
||||
pushd electron
|
||||
pushd electron || exit
|
||||
npm install
|
||||
popd
|
||||
popd || exit
|
||||
node ./electron/build.js published
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -29,3 +29,4 @@ vars.yaml
|
||||
laravels.conf
|
||||
laravels.pid
|
||||
README_LOCAL.md
|
||||
dootask.lock
|
||||
|
||||
528
CHANGELOG.md
528
CHANGELOG.md
@@ -2,6 +2,534 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [1.2.5]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 无法打包文件加载的情况
|
||||
- 修复@弹窗无法滚动
|
||||
|
||||
### Features
|
||||
|
||||
- 添加 setCapsuleConfig 方法以更新胶囊配置
|
||||
- 添加应用移动端胶囊布局
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化粘贴提及消息
|
||||
- 优化消息类型的判断
|
||||
- 签到记录窗口添加打开签到机器人
|
||||
- 文件名长度限制最长为100字
|
||||
- 允许打包下载一个文件夹
|
||||
- 优化桌面端出现打开久之后访问错误的情况
|
||||
- 优化抽屉样式
|
||||
- 更新应用胶囊配置和优化微应用加载
|
||||
- 优化 css 语法
|
||||
- 优化抽屉窗口
|
||||
- 优化微应用
|
||||
- 优化微应用关闭窗口逻辑
|
||||
- 优化消息重复
|
||||
|
||||
## [1.1.66]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 表格消息文字颜色冲突
|
||||
- 修复无法导出的问题
|
||||
|
||||
### Features
|
||||
|
||||
- 添加待办完成状态的支持
|
||||
- 工作流支持自定义颜色
|
||||
- 重构基础模块
|
||||
- 更新请求上下文处理
|
||||
|
||||
## [1.1.56]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复无法删除webhook的问题
|
||||
- 用户头像加载失败的情况
|
||||
|
||||
### Features
|
||||
|
||||
- 优化请求上下文处理
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化错误提示
|
||||
- 优化应用菜单
|
||||
- 优化机器人消息接收处理任务
|
||||
- 签到新增高德和腾讯地图
|
||||
- 优化国际化
|
||||
- 优化 AI 设置
|
||||
- 优化应用弹窗
|
||||
- 优化会员选择器
|
||||
- 优化会员搜索接口
|
||||
- 优化提及窗口
|
||||
- 优化机器人消息
|
||||
- 机器人支持新会话
|
||||
- 优化应用方法
|
||||
- 机器人 webhook 添加用户信息
|
||||
- 优化应用
|
||||
|
||||
## [1.1.15]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复客户度右键复制图片失败的情况
|
||||
- 修复部分emoji表情无法提交的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化预览消息
|
||||
- 优化应用参数
|
||||
|
||||
## [1.1.8]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复机器人发送消息接口
|
||||
- 修复应用无法在窗口独立显示
|
||||
|
||||
## [1.1.3]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 转发消息同时留言时ai会回复两条的情况
|
||||
- 修复应用 {system_theme} 参数无效的问题
|
||||
- 修复应用 selectUsers 方法的问题
|
||||
- 修复应用地址转换不正确的问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化应用
|
||||
- 优化创建新会话数据
|
||||
- 新增使用系统机器人发送消息
|
||||
- 优化应用中心
|
||||
- 获取我的部门列表接口
|
||||
|
||||
## [1.0.88]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复修改删除标签未同步任务标签的问题
|
||||
- 修复部分屏幕无法完全显示项目管理员菜单
|
||||
- 修复项目成员无法认领任务的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化应用商城
|
||||
- 优化一些样式
|
||||
- 优化桌面端服务
|
||||
- 优化标签选择
|
||||
- 优化标签操作日志
|
||||
- 支持管理自己创建的标签
|
||||
- 调整项目最多支持添加50个模板、100个标签
|
||||
- 优化翻译
|
||||
- 优化邀请加入项目
|
||||
- 优化项目邀请链接
|
||||
- 优化聊天发送会员、任务、文件支持搜索ID
|
||||
- 优化发送消息结果
|
||||
- 优化通知内容
|
||||
- 优化群消息推送内容
|
||||
|
||||
## [1.0.65]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化应用商城
|
||||
|
||||
## [1.0.61]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复客户端无法打开部分应用的问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化客户端缓存
|
||||
- 优化已知问题
|
||||
- 优化iPadOS兼容性
|
||||
- 优化设备登录
|
||||
|
||||
## [1.0.51]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复应用商店参数失效问题
|
||||
|
||||
### Features
|
||||
|
||||
- 微应用支持iframe模式
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化导出签到功能
|
||||
- 优化导出审批功能
|
||||
- 优化导出任务功能
|
||||
|
||||
## [1.0.45]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复已经在消息中打开项目对话时无法在其他地方打开项目沟通
|
||||
- 修复搜索标签后搜索框消失的情况
|
||||
- 修复部分标签背景色不显示的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化本地资源加载方式
|
||||
- 优化微应用参数变量的支持
|
||||
|
||||
## [1.0.37]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复客户端无法打开工作报告
|
||||
- 修复部分机子无法打开OKR的情况
|
||||
|
||||
## [1.0.31]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复移动端审批列表无法滚动到底部的情况
|
||||
- 修复重复周期 子任务没有复制过去
|
||||
|
||||
### Features
|
||||
|
||||
- 桌面端使用web服务启动
|
||||
|
||||
## [1.0.0]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复录音文件转文字后无法切换翻译的问题
|
||||
|
||||
### Features
|
||||
|
||||
- 新增应用商店
|
||||
- 检查应用是否已安装
|
||||
|
||||
### Performance
|
||||
|
||||
- 更新AI默认模型列表
|
||||
|
||||
## [0.47.7]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复任务详情查看历史空白的情况
|
||||
- 修复我的机器人不回复的情况
|
||||
- 修复设待办后数据不立即显示的问题
|
||||
|
||||
### Features
|
||||
|
||||
- 添加删除附件日志记录
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化从任务页面发送消息
|
||||
- 优化已归档/已删除任务列表支持按状态检索
|
||||
- 优化长按消息菜单位置
|
||||
- 优化登录设备名称
|
||||
|
||||
## [0.46.74]
|
||||
|
||||
### Features
|
||||
|
||||
- 新增系统分享搜索功能
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化通用菜单
|
||||
- 优化视频压缩
|
||||
- 优化全文搜索
|
||||
- 优化长按菜单
|
||||
|
||||
## [0.46.16]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复调整任务排序后出现空白的情况
|
||||
- 修复移动任务时负责人和协助人可以同时选择的情况
|
||||
- 修复无法从任务页面打开聊天的情况
|
||||
- 修复移动端焦点抖动的问题
|
||||
|
||||
### Features
|
||||
|
||||
- 新增任务发送功能
|
||||
- 新增会员详情窗口
|
||||
- 添加从团队管理打开会话窗口
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化移动任务
|
||||
- 优化自己的对话不限修改撤回时间
|
||||
- 优化访问链接
|
||||
- 优化日历
|
||||
- 优化长按事件
|
||||
- 优化移动端任务窗口布局
|
||||
- 优化长按操作
|
||||
- 优化转发确认选项保持上一次选择
|
||||
- 优化移动端布局
|
||||
- 优化禁止选择会员效果
|
||||
- 优化长按菜单位置
|
||||
- 优化移动端打开会话等待效果
|
||||
- 优化会议弹窗
|
||||
- 任务详情点任务聊天时不要发送消息
|
||||
- 优化国际化
|
||||
|
||||
## [0.45.64]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复部分页面出现空白的情况
|
||||
- 修复输入框无法点击添加链接的情况
|
||||
- 修复AI机器人不存在的情况
|
||||
|
||||
### Features
|
||||
|
||||
- 新增转发至AI开启新会话
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化数据结构
|
||||
- 优化图片存储名
|
||||
- 优化消息窗口显示
|
||||
- 优化目录结构
|
||||
- 优化日历
|
||||
- 优化任务时间范围选择
|
||||
|
||||
## [0.45.33]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复未读数错误暴增的情况
|
||||
- 修复地址可能存在localhost的情况
|
||||
- 修复消息编辑和发布时序号对不上
|
||||
- 修复草稿出现上一次内容的情况
|
||||
- 修复本地群消息通知没有会员昵称的问题
|
||||
- 修复了拉人进群无法踢出去的问题
|
||||
- 提及出现白色字的情况
|
||||
|
||||
### Features
|
||||
|
||||
- 添加移动端提示可能要发送的图片
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息窗口
|
||||
- 优化消息长按菜单
|
||||
- 优化内置浏览器
|
||||
- 优化App隐私政策提示
|
||||
- 优化对话独立窗口显示
|
||||
- 优化移动端选择交互
|
||||
- 优化移动端选中消息文本
|
||||
- 优化撤回消息逻辑
|
||||
- 优化提及搜索
|
||||
- 优化机器人Webhook消息
|
||||
|
||||
## [0.44.91]
|
||||
|
||||
### Features
|
||||
|
||||
- 添加我的机器人管理
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化初始化逻辑
|
||||
- 优化docker配置
|
||||
|
||||
## [0.44.82]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复搜索结果显示即将到期
|
||||
|
||||
### Features
|
||||
|
||||
- 新增独立窗口打开会话
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化AI支持文件类型
|
||||
|
||||
## [0.44.74]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化AI解析文件
|
||||
- 优化 WebSocket 消息
|
||||
- 优化数据
|
||||
|
||||
## [0.44.67]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复查看待办图片不符的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化数据
|
||||
- 优化未读消息数
|
||||
- 优化搜索组件
|
||||
- 已归档/已删除任务列表支持按状态检索
|
||||
- 优化消息流效果
|
||||
- 优化AI上下文
|
||||
- 优化工作流获取
|
||||
- 优化转发功能
|
||||
|
||||
## [0.44.53]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 工作流存在已离职人员
|
||||
|
||||
### Features
|
||||
|
||||
- 可点击标注图标查看标注人员
|
||||
- 支持分享工作报告到消息
|
||||
- 支持AI分析工作报告
|
||||
- 支持使用%发送工作报告
|
||||
- 新增自定义撤回及修改消息时限
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化转发消息
|
||||
- 优化工作报告列表
|
||||
- 优化引用消息
|
||||
- 优化全局提示
|
||||
- 优化草稿消息
|
||||
|
||||
## [0.44.19]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 看不到未读消息定位提醒
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息定位
|
||||
- 优化消息性能
|
||||
|
||||
## [0.44.15]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 会话内消息搜索布局错位
|
||||
- 流程设置翻译不统一
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息定位
|
||||
- 优化MD消息
|
||||
|
||||
## [0.44.3]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 定位签到失败的问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化发送语音效果
|
||||
- 录音转文字支持自定义语言
|
||||
- 优化ES模块
|
||||
- 优化emoji表情
|
||||
- 按住Ctrl/Command键可连续选择表情
|
||||
- Md消息支持html代码
|
||||
- 优化脚本
|
||||
- 优化安装命令
|
||||
- 优化ES索引名称
|
||||
|
||||
## [0.43.73]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 全屏预览图片关闭窗口
|
||||
- 点击排序导致任务不显示的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 新增录音转文字
|
||||
- 优化数据排序
|
||||
|
||||
## [0.43.49]
|
||||
|
||||
### Performance
|
||||
|
||||
- 添加全局搜索功能
|
||||
- 优化消息搜索
|
||||
- 团队管理支持调整部门区域尺寸
|
||||
- 任务详情支持调整聊天区域尺寸
|
||||
- 优化团队部门支持3级部门
|
||||
- 可见群组ID
|
||||
- 支持在团队管理打开群聊
|
||||
- 优化回复消息自动@逻辑
|
||||
- 转发预览隐藏表情回应部分
|
||||
- 优化任务日志
|
||||
- 已删除任务支持按标签搜索
|
||||
- 归档任务支持按标签搜索
|
||||
- 项目面板添加按标签筛选
|
||||
- 优化 AI 提示词
|
||||
- 优化 AI 设置
|
||||
|
||||
## [0.43.18]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 首次跟ai聊天没有记录的问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 工作报告支持查看仅未读
|
||||
- AI 支持引用文件
|
||||
- 优化图文消息
|
||||
- 优化文本信息复制
|
||||
- 优化样式
|
||||
- 无法再AI机器人页面看到模型的问题
|
||||
|
||||
## [0.43.7]
|
||||
|
||||
### Features
|
||||
|
||||
- 添加 Grok AI、Ollama AI
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化MD消息过长处理
|
||||
- 优化AI支持分析指定文件
|
||||
- 支持在AI对话中直接引用任务提问
|
||||
- 优化 AI 参数
|
||||
- 优化 Ollama AI
|
||||
- 优化设置
|
||||
- 优化AI设置
|
||||
- 优化AI消息
|
||||
|
||||
## [0.42.85]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 撤回消息是消息列表不更新的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 表情回复时更新对话列表
|
||||
- Onlyoffice 支持打开超过100m的文件
|
||||
- 优化点击上传列表效果
|
||||
- AI支持自定义模型列表
|
||||
|
||||
## [0.42.79]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复偶现的是子窗口出现身份丢失的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化查看长消息内容
|
||||
|
||||
## [0.42.74]
|
||||
|
||||
### Performance
|
||||
|
||||
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,6 +36,7 @@ class ApproveController extends AbstractController
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
Apps::isInstalledThrow('approve');
|
||||
$this->flow_url = env('FLOW_URL') ?: 'http://approve';
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,10 +12,10 @@ use App\Models\FileLink;
|
||||
use App\Models\FileUser;
|
||||
use App\Models\User;
|
||||
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;
|
||||
@@ -87,6 +87,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
return Base::retError($msg, $data);
|
||||
}
|
||||
$fileLink->increment("num");
|
||||
} else {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
@@ -106,6 +107,7 @@ class FileController extends AbstractController
|
||||
*
|
||||
* @apiParam {String} [link] 通过分享地址搜索(如:https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==)
|
||||
* @apiParam {String} [key] 关键词
|
||||
* @apiParam {Number} [take] 获取数量(默认:50,最大:100)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -118,7 +120,7 @@ class FileController extends AbstractController
|
||||
$link = trim(Request::input('link'));
|
||||
$key = trim(Request::input('key'));
|
||||
$id = 0;
|
||||
$take = 50;
|
||||
$take = Base::getPaginate(100, 50, 'take');
|
||||
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
|
||||
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
|
||||
$take = 1;
|
||||
@@ -132,7 +134,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();
|
||||
// 搜索共享的
|
||||
@@ -195,8 +201,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) {
|
||||
@@ -501,6 +507,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,
|
||||
@@ -575,10 +585,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':
|
||||
@@ -630,6 +642,8 @@ class FileController extends AbstractController
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
File::isNeedInstallApp('office');
|
||||
//
|
||||
$config = Request::input('config');
|
||||
$token = \Firebase\JWT\JWT::encode($config, env('APP_KEY') ,'HS256');
|
||||
return Base::retSuccess('成功', [
|
||||
@@ -655,6 +669,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 +680,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));
|
||||
@@ -775,6 +791,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('历史数据不存在或已被删除');
|
||||
@@ -998,17 +1016,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 +1085,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 +1097,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 +1117,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', [
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Request;
|
||||
use Session;
|
||||
use Redirect;
|
||||
use Response;
|
||||
use Madzipper;
|
||||
use Carbon\Carbon;
|
||||
use App\Module\Down;
|
||||
use App\Module\Doo;
|
||||
use App\Models\File;
|
||||
use App\Models\User;
|
||||
@@ -29,6 +29,7 @@ use App\Models\ProjectColumn;
|
||||
use App\Models\ProjectInvite;
|
||||
use App\Models\ProjectFlowItem;
|
||||
use App\Models\ProjectTaskFile;
|
||||
use App\Models\ProjectTaskTag;
|
||||
use App\Models\ProjectTaskUser;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Exceptions\ApiException;
|
||||
@@ -424,7 +425,7 @@ class ProjectController extends AbstractController
|
||||
*/
|
||||
public function invite()
|
||||
{
|
||||
User::auth();
|
||||
$user = User::auth();
|
||||
//
|
||||
$project_id = intval(Request::input('project_id'));
|
||||
$refresh = Request::input('refresh', 'no');
|
||||
@@ -440,17 +441,17 @@ class ProjectController extends AbstractController
|
||||
if (empty($projectInvite)) {
|
||||
$projectInvite = ProjectInvite::createInstance([
|
||||
'project_id' => $project->id,
|
||||
'code' => Base::generatePassword(64),
|
||||
'code' => base64_encode("{$project->id},{$user->userid}," . Base::generatePassword()),
|
||||
]);
|
||||
$projectInvite->save();
|
||||
} else {
|
||||
if ($refresh == 'yes') {
|
||||
$projectInvite->code = Base::generatePassword(64);
|
||||
$projectInvite->code = base64_encode("{$project->id},{$user->userid}," . Base::generatePassword());
|
||||
$projectInvite->save();
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('success', [
|
||||
'url' => Base::fillUrl('manage/project/invite?code=' . $projectInvite->code),
|
||||
'url' => Base::fillUrl('manage/project/invite/' . $projectInvite->code),
|
||||
'num' => $projectInvite->num
|
||||
]);
|
||||
}
|
||||
@@ -941,7 +942,9 @@ class ProjectController extends AbstractController
|
||||
* @apiName task__lists
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.name: ID、任务名称
|
||||
* - keys.name: ID、任务名称、任务描述
|
||||
* - keys.tag: 标签名称
|
||||
* - keys.status: 任务状态 (completed: 已完成、uncompleted: 未完成、flow-xx: 流程状态ID)
|
||||
*
|
||||
* @apiParam {Number} [project_id] 项目ID
|
||||
* @apiParam {Number} [parent_id] 主任务ID(project_id && parent_id ≤ 0 时 仅查询自己参与的任务)
|
||||
@@ -994,7 +997,29 @@ class ProjectController extends AbstractController
|
||||
if (Base::isNumber($keys['name'])) {
|
||||
$builder->where("project_tasks.id", intval($keys['name']));
|
||||
} else {
|
||||
$builder->where("project_tasks.name", "like", "%{$keys['name']}%");
|
||||
$builder->where(function ($query) use ($keys) {
|
||||
$query->where("project_tasks.name", "like", "%{$keys['name']}%");
|
||||
$query->orWhere("project_tasks.desc", "like", "%{$keys['name']}%");
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($keys['tag']) {
|
||||
$builder->whereHas('taskTag', function ($query) use ($keys) {
|
||||
$query->where('project_task_tags.name', $keys['tag']);
|
||||
});
|
||||
}
|
||||
if ($keys['status']) {
|
||||
if ($keys['status'] == 'completed') {
|
||||
$builder->whereNotNull('project_tasks.complete_at');
|
||||
} elseif ($keys['status'] == 'uncompleted') {
|
||||
$builder->whereNull('project_tasks.complete_at');
|
||||
} elseif (str_starts_with($keys['status'], 'flow-')) {
|
||||
$flow = str_replace('flow-', '', $keys['status']);
|
||||
if (Base::isNumber($flow)) {
|
||||
$builder->where('project_tasks.flow_item_id', intval($flow));
|
||||
} elseif ($flow) {
|
||||
$builder->where('project_tasks.flow_item_name', 'like', "%{$flow}%");
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
@@ -1233,26 +1258,27 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||
//
|
||||
go(function () use ($user, $userid, $time, $type, $botUser, $dialog) {
|
||||
Coroutine::sleep(0.1);
|
||||
$doo = Doo::load();
|
||||
go(function () use ($doo, $user, $userid, $time, $type, $botUser, $dialog) {
|
||||
Coroutine::sleep(1);
|
||||
$headings = [];
|
||||
$headings[] = Doo::translate('任务ID');
|
||||
$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('创建人');
|
||||
$headings[] = Doo::translate('状态');
|
||||
$headings[] = $doo->translate('任务ID');
|
||||
$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('创建人');
|
||||
$headings[] = $doo->translate('状态');
|
||||
$datas = [];
|
||||
//
|
||||
$content = [];
|
||||
@@ -1266,7 +1292,7 @@ class ProjectController extends AbstractController
|
||||
->where('project_task_users.owner', 1)
|
||||
->whereIn('project_task_users.userid', $userid)
|
||||
->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay(), $type);
|
||||
$builder->orderByDesc('project_tasks.id')->chunk(100, function ($tasks) use (&$datas) {
|
||||
$builder->orderByDesc('project_tasks.id')->chunk(100, function ($tasks) use ($doo, &$datas) {
|
||||
/** @var ProjectTask $task */
|
||||
foreach ($tasks as $task) {
|
||||
$flowChanges = ProjectTaskFlowChange::whereTaskId($task->id)->get();
|
||||
@@ -1305,9 +1331,9 @@ class ProjectController extends AbstractController
|
||||
$planTotalTime = $endTime - $startTime;
|
||||
$residueTime = $planTotalTime - $totalTime;
|
||||
if ($residueTime < 0) {
|
||||
$overTime = Doo::translate(Timer::timeFormat(abs($residueTime)));
|
||||
$overTime = $doo->translate(Timer::timeFormat(abs($residueTime)));
|
||||
}
|
||||
$planTime = Doo::translate(Timer::timeDiff($startTime, $endTime));
|
||||
$planTime = $doo->translate(Timer::timeDiff($startTime, $endTime));
|
||||
}
|
||||
$actualTime = $task->complete_at ? $totalTime : 0; // 实际完成用时
|
||||
$statusText = '未完成';
|
||||
@@ -1351,13 +1377,13 @@ class ProjectController extends AbstractController
|
||||
$task->complete_at ?: '-',
|
||||
$task->archived_at ?: '-',
|
||||
$planTime,
|
||||
$actualTime ? Doo::translate(Timer::timeFormat($actualTime)) : '-',
|
||||
$actualTime ? $doo->translate(Timer::timeFormat($actualTime)) : '-',
|
||||
$overTime,
|
||||
$developTime > 0 ? Doo::translate(Timer::timeFormat($developTime)) : '-',
|
||||
$testTime > 0 ? Doo::translate(Timer::timeFormat($testTime)) : '-',
|
||||
$developTime > 0 ? $doo->translate(Timer::timeFormat($developTime)) : '-',
|
||||
$testTime > 0 ? $doo->translate(Timer::timeFormat($testTime)) : '-',
|
||||
Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})",
|
||||
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
|
||||
Doo::translate($statusText),
|
||||
$doo->translate($statusText),
|
||||
];
|
||||
}
|
||||
});
|
||||
@@ -1370,7 +1396,7 @@ class ProjectController extends AbstractController
|
||||
'type' => 'content',
|
||||
'title' => $content[0]['content'],
|
||||
'content' => $content,
|
||||
], $botUser->userid, false, false, true);
|
||||
], $botUser->userid, true, false, true);
|
||||
return;
|
||||
}
|
||||
//
|
||||
@@ -1391,7 +1417,7 @@ class ProjectController extends AbstractController
|
||||
} else {
|
||||
$fileName .= '的任务统计';
|
||||
}
|
||||
$fileName = Doo::translate($fileName) . '_' . Timer::time() . '.xls';
|
||||
$fileName = $doo->translate($fileName) . '_' . Timer::time() . '.xls';
|
||||
$filePath = "temp/task/export/" . date("Ym", Timer::time());
|
||||
$export = new BillMultipleExport($sheets);
|
||||
$res = $export->store($filePath . "/" . $fileName);
|
||||
@@ -1404,7 +1430,7 @@ class ProjectController extends AbstractController
|
||||
'type' => 'content',
|
||||
'title' => $content[0]['content'],
|
||||
'content' => $content,
|
||||
], $botUser->userid, false, false, true);
|
||||
], $botUser->userid, true, false, true);
|
||||
return;
|
||||
}
|
||||
//
|
||||
@@ -1420,18 +1446,17 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
//
|
||||
if (file_exists($zipPath)) {
|
||||
$base64 = base64_encode(Base::array2string([
|
||||
$key = Down::cache_encode([
|
||||
'file' => $zipFile,
|
||||
]));
|
||||
$fileUrl = Base::fillUrl('api/project/task/down?key=' . urlencode($base64));
|
||||
Session::put('task::export:userid', $user->userid);
|
||||
]);
|
||||
$fileUrl = Base::fillUrl('api/project/task/down?key=' . $key);
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'file_download',
|
||||
'title' => '导出任务统计已完成',
|
||||
'name' => $fileName,
|
||||
'size' => filesize($zipPath),
|
||||
'url' => $fileUrl,
|
||||
], $botUser->userid, false, false, true);
|
||||
], $botUser->userid, true, false, true);
|
||||
} else {
|
||||
$content[] = [
|
||||
'content' => "打包失败,请稍后再试...",
|
||||
@@ -1441,9 +1466,15 @@ class ProjectController extends AbstractController
|
||||
'type' => 'content',
|
||||
'title' => $content[0]['content'],
|
||||
'content' => $content,
|
||||
], $botUser->userid, false, false, true);
|
||||
], $botUser->userid, true, false, true);
|
||||
}
|
||||
});
|
||||
//
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'content' => '正在导出任务统计,请稍等...',
|
||||
], $botUser->userid, true, false, true);
|
||||
//
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
|
||||
@@ -1463,103 +1494,156 @@ class ProjectController extends AbstractController
|
||||
{
|
||||
$user = User::auth('admin');
|
||||
//
|
||||
$headings = [];
|
||||
$headings[] = Doo::translate('任务ID');
|
||||
$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('创建人');
|
||||
$data = [];
|
||||
$botUser = User::botGetOrCreate('system-msg');
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('系统机器人不存在');
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||
//
|
||||
ProjectTask::with(['taskTag'])
|
||||
->whereNull('complete_at')
|
||||
->whereNotNull('end_at')
|
||||
->where('end_at', '<=', Carbon::now())
|
||||
->orderBy('end_at')
|
||||
->chunk(100, function ($tasks) use (&$data) {
|
||||
/** @var ProjectTask $task */
|
||||
foreach ($tasks as $task) {
|
||||
$taskStartTime = Carbon::parse($task->start_at ?: $task->created_at)->timestamp;
|
||||
$totalTime = time() - $taskStartTime; //开发测试总用时
|
||||
$planTime = '-';//任务计划用时
|
||||
$overTime = '-';//超时时间
|
||||
if ($task->end_at) {
|
||||
$startTime = Carbon::parse($task->start_at)->timestamp;
|
||||
$endTime = Carbon::parse($task->end_at)->timestamp;
|
||||
$planTotalTime = $endTime - $startTime;
|
||||
$residueTime = $planTotalTime - $totalTime;
|
||||
if ($residueTime < 0) {
|
||||
$overTime = Doo::translate(Timer::timeFormat(abs($residueTime)));
|
||||
$doo = Doo::load();
|
||||
go(function () use ($doo, $botUser, $dialog, $user) {
|
||||
Coroutine::sleep(1);
|
||||
//
|
||||
$headings = [];
|
||||
$headings[] = $doo->translate('任务ID');
|
||||
$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('创建人');
|
||||
$data = [];
|
||||
//
|
||||
$content = [];
|
||||
$content[] = [
|
||||
'content' => '导出超期任务已完成',
|
||||
'style' => 'font-weight: bold;padding-bottom: 4px;',
|
||||
];
|
||||
//
|
||||
ProjectTask::with(['taskTag'])
|
||||
->whereNull('complete_at')
|
||||
->whereNotNull('end_at')
|
||||
->where('end_at', '<=', Carbon::now())
|
||||
->orderBy('end_at')
|
||||
->chunk(100, function ($tasks) use ($doo, &$data) {
|
||||
/** @var ProjectTask $task */
|
||||
foreach ($tasks as $task) {
|
||||
$taskStartTime = Carbon::parse($task->start_at ?: $task->created_at)->timestamp;
|
||||
$totalTime = time() - $taskStartTime; //开发测试总用时
|
||||
$planTime = '-';//任务计划用时
|
||||
$overTime = '-';//超时时间
|
||||
if ($task->end_at) {
|
||||
$startTime = Carbon::parse($task->start_at)->timestamp;
|
||||
$endTime = Carbon::parse($task->end_at)->timestamp;
|
||||
$planTotalTime = $endTime - $startTime;
|
||||
$residueTime = $planTotalTime - $totalTime;
|
||||
if ($residueTime < 0) {
|
||||
$overTime = $doo->translate(Timer::timeFormat(abs($residueTime)));
|
||||
}
|
||||
$planTime = $doo->translate(Timer::timeDiff($startTime, $endTime));
|
||||
}
|
||||
$planTime = Doo::translate(Timer::timeDiff($startTime, $endTime));
|
||||
$ownerIds = $task->taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
$ownerNames = [];
|
||||
foreach ($ownerIds as $ownerId) {
|
||||
$ownerNames[] = Base::filterEmoji(User::userid2nickname($ownerId)) . " (ID: {$ownerId})";
|
||||
}
|
||||
$data[] = [
|
||||
$task->id,
|
||||
$task->parent_id ?: '-',
|
||||
Base::filterEmoji($task->project?->name) ?: '-',
|
||||
Base::filterEmoji($task->name),
|
||||
$task->taskTag->map(function ($tag) {
|
||||
return Base::filterEmoji($tag->name);
|
||||
})->join(', ') ?: '-',
|
||||
$task->start_at ?: '-',
|
||||
$task->end_at ?: '-',
|
||||
$planTime,
|
||||
$overTime,
|
||||
implode(', ', $ownerNames),
|
||||
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
|
||||
];
|
||||
}
|
||||
$ownerIds = $task->taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
$ownerNames = [];
|
||||
foreach ($ownerIds as $ownerId) {
|
||||
$ownerNames[] = Base::filterEmoji(User::userid2nickname($ownerId)) . " (ID: {$ownerId})";
|
||||
}
|
||||
$data[] = [
|
||||
$task->id,
|
||||
$task->parent_id ?: '-',
|
||||
Base::filterEmoji($task->project?->name) ?: '-',
|
||||
Base::filterEmoji($task->name),
|
||||
$task->taskTag->map(function ($tag) {
|
||||
return Base::filterEmoji($tag->name);
|
||||
})->join(', ') ?: '-',
|
||||
$task->start_at ?: '-',
|
||||
$task->end_at ?: '-',
|
||||
$planTime,
|
||||
$overTime,
|
||||
implode(', ', $ownerNames),
|
||||
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
|
||||
];
|
||||
}
|
||||
});
|
||||
if (empty($data)) {
|
||||
return Base::retError('没有任何数据');
|
||||
}
|
||||
});
|
||||
if (empty($data)) {
|
||||
$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($data)->setStyles(["A1:J1" => ["font" => ["bold" => true]]])
|
||||
];
|
||||
//
|
||||
$fileName = $title . '_' . Timer::time() . '.xls';
|
||||
$filePath = "temp/task/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, '.xls') . ".zip";
|
||||
$zipPath = storage_path($zipFile);
|
||||
if (file_exists($zipPath)) {
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
if (file_exists($zipPath)) {
|
||||
$key = Down::cache_encode([
|
||||
'file' => $zipFile,
|
||||
]);
|
||||
$fileUrl = Base::fillUrl('api/project/task/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($data)->setStyles(["A1:J1" => ["font" => ["bold" => true]]])
|
||||
];
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'content' => '正在导出超期任务,请稍等...',
|
||||
], $botUser->userid, true, false, true);
|
||||
//
|
||||
$fileName = $title . '_' . Timer::time() . '.xls';
|
||||
$filePath = "temp/task/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, '.xls') . ".zip";
|
||||
$zipPath = storage_path($zipFile);
|
||||
if (file_exists($zipPath)) {
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
if (file_exists($zipPath)) {
|
||||
$base64 = base64_encode(Base::array2string([
|
||||
'file' => $zipFile,
|
||||
]));
|
||||
Session::put('task::export:userid', $user->userid);
|
||||
return Base::retSuccess('success', [
|
||||
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
|
||||
'url' => Base::fillUrl('api/project/task/down?key=' . urlencode($base64)),
|
||||
]);
|
||||
} else {
|
||||
return Base::retError('打包失败,请稍后再试...');
|
||||
}
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1575,15 +1659,10 @@ class ProjectController extends AbstractController
|
||||
*/
|
||||
public function task__down()
|
||||
{
|
||||
$userid = Session::get('task::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));
|
||||
}
|
||||
@@ -1759,6 +1838,13 @@ class ProjectController extends AbstractController
|
||||
//
|
||||
ProjectPermission::userTaskPermission(Project::userProject($task->project_id), ProjectPermission::TASK_REMOVE, $task);
|
||||
//
|
||||
$task->addLog('删除附件:' . $file->name, [
|
||||
'file_id' => $file->id,
|
||||
'name' => $file->name,
|
||||
'size' => $file->size,
|
||||
'path' => $file->getRawOriginal('path'),
|
||||
'thumb' => $file->getRawOriginal('thumb'),
|
||||
]);
|
||||
$task->pushMsg('filedelete', $file);
|
||||
$file->delete();
|
||||
//
|
||||
@@ -1800,6 +1886,7 @@ class ProjectController extends AbstractController
|
||||
'update_at' => Carbon::parse($file->updated_at)->toDateTimeString()
|
||||
]);
|
||||
}
|
||||
File::isNeedInstallApp($file->ext);
|
||||
//
|
||||
$data = $file->toArray();
|
||||
$data['path'] = $file->getRawOriginal('path');
|
||||
@@ -1834,9 +1921,7 @@ class ProjectController extends AbstractController
|
||||
$down = Request::input('down', 'yes');
|
||||
//
|
||||
$file = ProjectTaskFile::find($file_id);
|
||||
if (empty($file)) {
|
||||
abort(403, "This file not exist.");
|
||||
}
|
||||
abort_if(empty($file), 403, "This file not exist.");
|
||||
//
|
||||
try {
|
||||
ProjectTask::userTask($file->task_id, null);
|
||||
@@ -2029,14 +2114,17 @@ class ProjectController extends AbstractController
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id);
|
||||
//
|
||||
$project = Project::userProject($task->project_id);
|
||||
$permissionKey = ProjectPermission::TASK_UPDATE;
|
||||
if (Arr::exists($param, 'times')) {
|
||||
$permissionKey = ProjectPermission::TASK_TIME;
|
||||
} else if (Arr::exists($param, 'flow_item_id')) {
|
||||
$permissionKey = ProjectPermission::TASK_STATUS;
|
||||
if ($task->hasOwner()) {
|
||||
// 已经存在负责人,则需要检查权限(即:没有任务负责人时,不检查权限)
|
||||
$project = Project::userProject($task->project_id);
|
||||
$permissionKey = ProjectPermission::TASK_UPDATE;
|
||||
if (Arr::exists($param, 'times')) {
|
||||
$permissionKey = ProjectPermission::TASK_TIME;
|
||||
} else if (Arr::exists($param, 'flow_item_id')) {
|
||||
$permissionKey = ProjectPermission::TASK_STATUS;
|
||||
}
|
||||
ProjectPermission::userTaskPermission($project, $permissionKey, $task);
|
||||
}
|
||||
ProjectPermission::userTaskPermission($project, $permissionKey, $task);
|
||||
//
|
||||
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($task_id)->get();
|
||||
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
@@ -2279,7 +2367,7 @@ class ProjectController extends AbstractController
|
||||
$task->updateTask($data, $updateMarking);
|
||||
//
|
||||
$data = ProjectTask::oneTask($task->id)->toArray();
|
||||
$data["flow_item_name"] = $newFlowItem->status . "|" . $newFlowItem->name;
|
||||
$data["flow_item_name"] = $newFlowItem->status . "|" . $newFlowItem->name . "|" . $newFlowItem->color;
|
||||
$data['update_marking'] = $updateMarking ?: json_decode('{}');
|
||||
$task->pushMsg('update', $data);
|
||||
//
|
||||
@@ -2298,8 +2386,8 @@ class ProjectController extends AbstractController
|
||||
* @apiGroup project
|
||||
* @apiName task__flow
|
||||
*
|
||||
* @apiParam {Number} task_id 任务ID
|
||||
* @apiParam {Number} project_id 项目ID - 存在时只返回这个项目的
|
||||
* @apiParam {Number} [task_id] 任务ID
|
||||
* @apiParam {Number} [project_id] 项目ID(存在时只返回这个项目的工作流,主要用于任务移动到其他项目时)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -2336,7 +2424,7 @@ class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
//
|
||||
$turns = ProjectFlowItem::select(['id', 'name', 'status', 'turns'])->whereFlowId($projectFlow->id)->orderBy('sort')->get();
|
||||
$turns = ProjectFlowItem::select(['id', 'name', 'status', 'turns', 'color'])->whereFlowId($projectFlow->id)->orderBy('sort')->get();
|
||||
if (empty($projectFlowItem)) {
|
||||
$data = [
|
||||
'task_id' => $projectTask->id,
|
||||
@@ -2393,6 +2481,11 @@ class ProjectController extends AbstractController
|
||||
* @apiParam {Number} flow_item_id 工作流id
|
||||
* @apiParam {Array} owner 负责人
|
||||
* @apiParam {Array} assist 协助人
|
||||
* @apiParam {String} [completed] 是否已完成
|
||||
* - 没有 工作流id 时此参数才生效
|
||||
* - 有值表示已完成
|
||||
* - 空值表示未完成
|
||||
* - 不存在不改变状态
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -2409,7 +2502,7 @@ class ProjectController extends AbstractController
|
||||
$flow_item_id = intval(Request::input('flow_item_id'));
|
||||
$owner = Request::input('owner', []);
|
||||
$assist = Request::input('assist', []);
|
||||
$completeAt = trim(Request::input('complete_at', ''));
|
||||
$completed = Request::exists('completed') ? (bool)Request::input('completed') : null;
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id);
|
||||
//
|
||||
@@ -2430,13 +2523,13 @@ class ProjectController extends AbstractController
|
||||
if (empty($flowItem)) {
|
||||
return Base::retError('任务状态不存在');
|
||||
}
|
||||
} else if (!$flow_item_id && !$completeAt) {
|
||||
} else {
|
||||
if (projectFlowItem::whereProjectId($project->id)->count() > 0) {
|
||||
return Base::retError('请选择移动后状态', [], 102);
|
||||
}
|
||||
}
|
||||
//
|
||||
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist, $completeAt);
|
||||
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist, $completed);
|
||||
//
|
||||
$data = [];
|
||||
$mainTask = ProjectTask::userTask($task_id)?->toArray();
|
||||
@@ -2586,7 +2679,7 @@ class ProjectController extends AbstractController
|
||||
$builder->with(['projectTask:id,parent_id,name'])->whereProjectId($project->id)->whereTaskOnly(0);
|
||||
}
|
||||
//
|
||||
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(100, 20));
|
||||
$list = $builder->orderByDesc('created_at')->orderByDesc('id')->paginate(Base::getPaginate(100, 20));
|
||||
$list->transform(function (ProjectLog $log) use ($task_id) {
|
||||
$timestamp = Carbon::parse($log->created_at)->timestamp;
|
||||
if ($task_id === 0) {
|
||||
@@ -2808,8 +2901,8 @@ class ProjectController extends AbstractController
|
||||
$template->update($data);
|
||||
} else {
|
||||
$templateCount = ProjectTaskTemplate::where('project_id', $projectId)->count();
|
||||
if ($templateCount >= 20) {
|
||||
return Base::retError('每个项目最多添加20个模板');
|
||||
if ($templateCount >= 50) {
|
||||
return Base::retError('每个项目最多添加50个模板');
|
||||
}
|
||||
$template = ProjectTaskTemplate::create($data);
|
||||
}
|
||||
@@ -2892,7 +2985,7 @@ class ProjectController extends AbstractController
|
||||
/**
|
||||
* @api {post} api/project/tag/save 51. 保存标签
|
||||
*
|
||||
* @apiDescription 需要token身份(修改:项目负责人;添加:项目所有成员)
|
||||
* @apiDescription 需要token身份(修改:项目负责人、标签创建者;添加:项目所有成员)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName tag__save
|
||||
@@ -2933,22 +3026,69 @@ class ProjectController extends AbstractController
|
||||
'color' => $color,
|
||||
'userid' => $user->userid
|
||||
];
|
||||
$project = Project::userProject($projectId, true, $id > 0 ? true : null);
|
||||
$project = Project::userProject($projectId);
|
||||
if ($id > 0) {
|
||||
$tag = ProjectTag::where('id', $id)
|
||||
->where('project_id', $projectId)
|
||||
->first();
|
||||
if (!$project->owner && $tag->userid != $user->userid) {
|
||||
return Base::retError('没有权限修改标签');
|
||||
}
|
||||
if (!$tag) {
|
||||
return Base::retError('标签不存在或已被删除');
|
||||
}
|
||||
$tag->update($data);
|
||||
AbstractModel::transaction(function () use ($data, $tag, $project) {
|
||||
$tagWhere = [
|
||||
'project_id' => $tag->project_id,
|
||||
'name' => $tag->name,
|
||||
];
|
||||
// 获取使用该标签的任务ID
|
||||
$taskIds = ProjectTaskTag::where($tagWhere)->pluck('task_id')->toArray();
|
||||
// 更新任务
|
||||
if (!empty($taskIds)) {
|
||||
ProjectTask::whereIn('id', $taskIds)->update(['updated_at' => Carbon::now()]);
|
||||
}
|
||||
// 更新任务标签
|
||||
ProjectTaskTag::where($tagWhere)->update([
|
||||
'color' => $data['color'],
|
||||
'name' => $data['name'],
|
||||
]);
|
||||
// 更新标签
|
||||
$project->addLog("修改标签", [
|
||||
'change' => [
|
||||
[
|
||||
'type' => 'tag',
|
||||
'name' => $tag->name,
|
||||
'color' => $tag->color
|
||||
],
|
||||
[
|
||||
'type' => 'tag',
|
||||
'name' => $data['name'],
|
||||
'color' => $data['color']
|
||||
]
|
||||
],
|
||||
]);
|
||||
$tag->update($data);
|
||||
});
|
||||
} else {
|
||||
$tagCount = ProjectTag::where('project_id', $projectId)->count();
|
||||
if ($tagCount >= 20) {
|
||||
return Base::retError('每个项目最多添加20个标签');
|
||||
if ($tagCount >= 100) {
|
||||
return Base::retError('每个项目最多添加100个标签');
|
||||
}
|
||||
if (ProjectTag::where([
|
||||
'project_id' => $projectId,
|
||||
'name' => $name,
|
||||
])->exists()) {
|
||||
return Base::retError('标签已存在');
|
||||
}
|
||||
$project->addLog("添加标签", [
|
||||
'change' => [
|
||||
'type' => 'tag',
|
||||
'name' => $name,
|
||||
'color' => $color
|
||||
]
|
||||
]);
|
||||
$tag = ProjectTag::create($data);
|
||||
$project->addLog("添加标签【" . $tag->name . "】");
|
||||
}
|
||||
return Base::retSuccess('保存成功', $tag);
|
||||
}
|
||||
@@ -2956,7 +3096,7 @@ class ProjectController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/project/tag/delete 52. 删除标签
|
||||
*
|
||||
* @apiDescription 需要token身份(限:项目负责人)
|
||||
* @apiDescription 需要token身份(限:项目负责人、标签创建者)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName tag__delete
|
||||
@@ -2969,7 +3109,7 @@ class ProjectController extends AbstractController
|
||||
*/
|
||||
public function tag__delete()
|
||||
{
|
||||
User::auth();
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = intval(Request::input('id'));
|
||||
if (!$id) {
|
||||
@@ -2979,9 +3119,35 @@ class ProjectController extends AbstractController
|
||||
if (!$tag) {
|
||||
return Base::retError('标签不存在或已被删除');
|
||||
}
|
||||
Project::userProject($tag->project_id, true, true);
|
||||
$tag->delete();
|
||||
return Base::retSuccess('删除成功');
|
||||
$project = Project::userProject($tag->project_id);
|
||||
if (!$project->owner && $tag->userid != $user->userid) {
|
||||
return Base::retError('没有权限删除标签');
|
||||
}
|
||||
//
|
||||
return AbstractModel::transaction(function () use ($tag, $project) {
|
||||
$tagWhere = [
|
||||
'project_id' => $tag->project_id,
|
||||
'name' => $tag->name,
|
||||
];
|
||||
// 获取使用该标签的任务ID
|
||||
$taskIds = ProjectTaskTag::where($tagWhere)->pluck('task_id')->toArray();
|
||||
// 更新任务
|
||||
if (!empty($taskIds)) {
|
||||
ProjectTask::whereIn('id', $taskIds)->update(['updated_at' => Carbon::now()]);
|
||||
}
|
||||
// 删除任务标签
|
||||
ProjectTaskTag::where($tagWhere)->delete();
|
||||
// 删除标签
|
||||
$project->addLog("删除标签", [
|
||||
'change' => [
|
||||
'type' => 'tag',
|
||||
'name' => $tag->name,
|
||||
'color' => $tag->color
|
||||
],
|
||||
]);
|
||||
$tag->delete();
|
||||
return Base::retSuccess('删除成功');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,8 +6,10 @@ use App\Exceptions\ApiException;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\Report;
|
||||
use App\Models\ReportLink;
|
||||
use App\Models\ReportReceive;
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Tasks\PushTask;
|
||||
@@ -28,11 +30,13 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/my 01. 我发送的汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName my
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.key: 关键词
|
||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||
* - keys.created_at: 汇报时间
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
@@ -49,6 +53,15 @@ class ReportController extends AbstractController
|
||||
$builder = Report::with(['receivesUser'])->whereUserid($user->userid);
|
||||
$keys = Request::input('keys');
|
||||
if (is_array($keys)) {
|
||||
if ($keys['key']) {
|
||||
if (str_contains($keys['key'], '@')) {
|
||||
$builder->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
|
||||
});
|
||||
} else {
|
||||
$builder->where("title", "LIKE", "%{$keys['key']}%");
|
||||
}
|
||||
}
|
||||
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
|
||||
$builder->whereType($keys['type']);
|
||||
}
|
||||
@@ -64,13 +77,16 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/receive 02. 我接收的汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName receive
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.key: 关键词
|
||||
* - keys.department_id: 部门ID
|
||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||
* - keys.status: 状态,unread:未读,read:已读
|
||||
* - keys.created_at: 汇报时间
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50
|
||||
@@ -89,15 +105,29 @@ class ReportController extends AbstractController
|
||||
$keys = Request::input('keys');
|
||||
if (is_array($keys)) {
|
||||
if ($keys['key']) {
|
||||
$builder->where(function($query) use ($keys) {
|
||||
$query->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
if (str_contains($keys['key'], '@')) {
|
||||
$builder->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
|
||||
})->orWhere("title", "LIKE", "%{$keys['key']}%");
|
||||
});
|
||||
} elseif (Base::isNumber($keys['key'])) {
|
||||
$builder->where("userid", intval($keys['key']));
|
||||
} else {
|
||||
$builder->where("title", "LIKE", "%{$keys['key']}%");
|
||||
}
|
||||
}
|
||||
if ($keys['department_id']) {
|
||||
$builder->whereHas('sendUser', function ($query) use ($keys) {
|
||||
$query->where("users.department", "LIKE", "%,{$keys['department_id']},%");
|
||||
});
|
||||
}
|
||||
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
|
||||
$builder->whereType($keys['type']);
|
||||
}
|
||||
if (in_array($keys['status'], ['unread', 'read'])) {
|
||||
$builder->whereHas("receivesUser", function ($query) use ($user, $keys) {
|
||||
$query->where("report_receives.userid", $user->userid)->where("report_receives.read", $keys['status'] === 'unread' ? 0 : 1);
|
||||
});
|
||||
}
|
||||
if (is_array($keys['created_at'])) {
|
||||
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', Base::newCarbon($keys['created_at'][0])->startOfDay());
|
||||
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', Base::newCarbon($keys['created_at'][1])->endOfDay());
|
||||
@@ -115,6 +145,7 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/store 03. 保存并发送工作汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName store
|
||||
@@ -190,7 +221,6 @@ class ReportController extends AbstractController
|
||||
$report->updateInstance([
|
||||
"title" => $input["title"],
|
||||
"type" => $input["type"],
|
||||
"content" => htmlspecialchars($input["content"]),
|
||||
]);
|
||||
} else {
|
||||
// 生成唯一标识
|
||||
@@ -204,11 +234,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"]) {
|
||||
@@ -240,6 +284,7 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/template 04. 生成汇报模板
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName template
|
||||
@@ -411,11 +456,13 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/detail 05. 报告详情
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName detail
|
||||
*
|
||||
* @apiParam {Number} [id] 报告id
|
||||
* @apiParam {Number} [id] 报告ID
|
||||
* @apiParam {String} [code] 报告分享代码,与ID二选一,优先ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -424,30 +471,43 @@ class ReportController extends AbstractController
|
||||
public function detail(): array
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = intval(trim(Request::input("id")));
|
||||
if (empty($id))
|
||||
$code = trim(Request::input("code"));
|
||||
//
|
||||
if (empty($id) && empty($code)) {
|
||||
return Base::retError("缺少ID参数");
|
||||
|
||||
$one = Report::getOne($id);
|
||||
$one->type_val = $one->getRawOriginal("type");
|
||||
|
||||
// 标记为已读
|
||||
if (!empty($one->receivesUser)) {
|
||||
foreach ($one->receivesUser as $item) {
|
||||
if ($item->userid === $user->userid && $item->pivot->read === 0) {
|
||||
$one->receivesUser()->updateExistingPivot($user->userid, [
|
||||
"read" => 1,
|
||||
]);
|
||||
}
|
||||
//
|
||||
if (!empty($id)) {
|
||||
$one = Report::getOne($id);
|
||||
$one->type_val = $one->getRawOriginal("type");
|
||||
// 标记为已读
|
||||
if (!empty($one->receivesUser)) {
|
||||
foreach ($one->receivesUser as $item) {
|
||||
if ($item->userid === $user->userid && $item->pivot->read === 0) {
|
||||
$one->receivesUser()->updateExistingPivot($user->userid, [
|
||||
"read" => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$link = ReportLink::whereCode($code)->first();
|
||||
if (empty($link)) {
|
||||
return Base::retError("报告不存在或已被删除");
|
||||
}
|
||||
$one = Report::getOne($link->rid);
|
||||
$one->report_link = $link;
|
||||
$link->increment("num");
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", $one);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/mark 06. 标记已读/未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName mark
|
||||
@@ -488,8 +548,71 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人
|
||||
* @api {get} api/report/share 07. 分享报告到消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName share
|
||||
*
|
||||
* @apiParam {Number} id 报告id(组)
|
||||
* @apiParam {Array} dialogids 转发给的对话ID
|
||||
* @apiParam {Array} userids 转发给的成员ID
|
||||
* @apiParam {String} leave_message 转发留言
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function share()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = Request::input('id');
|
||||
$dialogids = Request::input('dialogids');
|
||||
$userids = Request::input('userids');
|
||||
$leave_message = Request::input('leave_message');
|
||||
//
|
||||
if (is_array($id)) {
|
||||
if (count(Base::arrayRetainInt($id)) > 20) {
|
||||
return Base::retError("最多只能操作20条数据");
|
||||
}
|
||||
$builder = Report::whereIn("id", Base::arrayRetainInt($id));
|
||||
} else {
|
||||
$builder = Report::whereId(intval($id));
|
||||
}
|
||||
$reportMsgs = [];
|
||||
$builder ->chunkById(100, function ($list) use (&$reportMsgs, $user) {
|
||||
/** @var Report $item */
|
||||
foreach ($list as $item) {
|
||||
$reportLink = ReportLink::generateLink($item->id, $user->userid);
|
||||
$reportMsgs[] = "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/{$reportLink['code']}\" target=\"_blank\">%{$item->title}</a>";
|
||||
}
|
||||
});
|
||||
if (empty($reportMsgs)) {
|
||||
return Base::retError("报告不存在或已被删除");
|
||||
}
|
||||
$reportTag = count($reportMsgs) > 1 ? 'li' : 'p';
|
||||
$reportAttr = $reportTag === 'li' ? ' data-list="ordered"' : '';
|
||||
$reportMsgs = array_map(function ($item) use ($reportAttr, $reportTag) {
|
||||
return "<{$reportTag}{$reportAttr}>{$item}</{$reportTag}>";
|
||||
}, $reportMsgs);
|
||||
if ($reportTag === 'li') {
|
||||
array_unshift($reportMsgs, "<ol>");
|
||||
$reportMsgs[] = "</ol>";
|
||||
}
|
||||
if ($leave_message) {
|
||||
$reportMsgs[] = "<p>{$leave_message}</p>";
|
||||
}
|
||||
$msgText = implode("", $reportMsgs);
|
||||
//
|
||||
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $msgText);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/last_submitter 08. 获取最后一次提交的接收人
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName last_submitter
|
||||
@@ -505,8 +628,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/unread 08. 获取未读
|
||||
* @api {get} api/report/unread 09. 获取未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName unread
|
||||
@@ -529,8 +653,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/read 09. 标记汇报已读,可批量
|
||||
* @api {get} api/report/read 10. 标记汇报已读,可批量
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName read
|
||||
|
||||
@@ -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
|
||||
@@ -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', 'voice2text', 'translation', '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 返回信息(错误描述)
|
||||
@@ -70,7 +73,11 @@ class SystemController extends AbstractController
|
||||
'anon_message',
|
||||
'voice2text',
|
||||
'translation',
|
||||
'convert_video',
|
||||
'compress_video',
|
||||
'e2e_message',
|
||||
'msg_rev_limit',
|
||||
'msg_edit_limit',
|
||||
'auto_archived',
|
||||
'archived_day',
|
||||
'task_visible',
|
||||
@@ -84,7 +91,6 @@ class SystemController extends AbstractController
|
||||
'image_compress',
|
||||
'image_quality',
|
||||
'image_save_local',
|
||||
'start_home',
|
||||
'file_upload_limit',
|
||||
'unclaimed_task_reminder',
|
||||
'unclaimed_task_reminder_time',
|
||||
@@ -101,10 +107,10 @@ class SystemController extends AbstractController
|
||||
}
|
||||
}
|
||||
if ($all['voice2text'] == 'open' && !Setting::AIOpen()) {
|
||||
return Base::retError('开启语音转文字功能需要在应用中开启 ChatGPT AI 机器人。');
|
||||
return Base::retError('开启语音转文字功能需要先设置 AI 助理。');
|
||||
}
|
||||
if ($all['translation'] == 'open' && !Setting::AIOpen()) {
|
||||
return Base::retError('开启翻译功能需要在应用中开启 ChatGPT AI 机器人。');
|
||||
return Base::retError('开启翻译功能需要先设置 AI 助理。');
|
||||
}
|
||||
if ($all['system_alias'] == env('APP_NAME')) {
|
||||
$all['system_alias'] = '';
|
||||
@@ -134,7 +140,11 @@ class SystemController extends AbstractController
|
||||
$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,11 +152,9 @@ 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();
|
||||
//
|
||||
@@ -278,7 +286,49 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/aibot 04. 获取会议设置、保存AI机器人设置(限管理员)
|
||||
* @api {get} api/system/setting/ai 04. AI助手设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName setting__ai
|
||||
*
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - save: 保存设置(参数:['ai_provider', 'ai_api_key', 'ai_api_url', 'ai_proxy'])
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function setting__ai()
|
||||
{
|
||||
User::auth('admin');
|
||||
//
|
||||
$type = trim(Request::input('type'));
|
||||
if ($type == 'save') {
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
return Base::retError('当前环境禁止修改');
|
||||
}
|
||||
$all = Base::newTrim(Request::input());
|
||||
foreach ($all as $key => $value) {
|
||||
if (!in_array($key, [
|
||||
'ai_provider',
|
||||
'ai_api_key',
|
||||
'ai_api_url',
|
||||
'ai_proxy',
|
||||
])) {
|
||||
unset($all[$key]);
|
||||
}
|
||||
}
|
||||
$setting = Base::setting('aiSetting', Base::newTrim($all));
|
||||
} else {
|
||||
$setting = Base::setting('aiSetting');
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/aibot 05. 获取会议设置、保存AI机器人设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -287,6 +337,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 +347,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,10 +365,18 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -323,7 +386,66 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/checkin 05. 获取签到设置、保存签到设置(限管理员)
|
||||
* @api {get} api/system/setting/aibot_models 06. 获取AI模型
|
||||
*
|
||||
* @apiDescription 获取所有AI机器人模型设置
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName aibot_models
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function setting__aibot_models()
|
||||
{
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$setting = array_filter($setting, function($value, $key) {
|
||||
return str_ends_with($key, '_models') || str_ends_with($key, '_model');
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/aibot_defmodels 07. 获取AI默认模型
|
||||
*
|
||||
* @apiDescription 获取AI机器人默认模型
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName setting__aibot_defmodels
|
||||
*
|
||||
* @apiParam {String} type AI类型
|
||||
* @apiParam {String} [base_url] 基础URL(仅 type=ollama 时有效)
|
||||
* @apiParam {String} [key] Key(仅 type=ollama 时有效)
|
||||
* @apiParam {String} [agency] 使用代理(仅 type=ollama 时有效)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function setting__aibot_defmodels()
|
||||
{
|
||||
$type = trim(Request::input('type'));
|
||||
if ($type == 'ollama') {
|
||||
$baseUrl = trim(Request::input('base_url'));
|
||||
$key = trim(Request::input('key'));
|
||||
$agency = trim(Request::input('agency'));
|
||||
if (empty($baseUrl)) {
|
||||
return Base::retError('请先填写 Base URL');
|
||||
}
|
||||
return AI::ollamaModels($baseUrl, $key, $agency);
|
||||
}
|
||||
$models = Setting::AIBotDefaultModels($type);
|
||||
if (empty($models)) {
|
||||
return Base::retError('未找到默认模型');
|
||||
}
|
||||
return Base::retSuccess('success', [
|
||||
'models' => $models
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/checkin 08. 获取签到设置、保存签到设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -359,8 +481,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 +503,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 +555,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;
|
||||
@@ -429,7 +576,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/apppush 06. 获取APP推送设置、保存APP推送设置(限管理员)
|
||||
* @api {get} api/system/setting/apppush 09. 获取APP推送设置、保存APP推送设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -474,7 +621,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/thirdaccess 07. 第三方帐号(限管理员)
|
||||
* @api {get} api/system/setting/thirdaccess 10. 第三方帐号(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -544,7 +691,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/file 08. 文件设置(限管理员)
|
||||
* @api {get} api/system/setting/file 11. 文件设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -584,7 +731,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/demo 09. 获取演示帐号
|
||||
* @api {get} api/system/demo 12. 获取演示帐号
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -608,7 +755,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/priority 10. 任务优先级
|
||||
* @api {post} api/system/priority 13. 任务优先级
|
||||
*
|
||||
* @apiDescription 获取任务优先级、保存任务优先级
|
||||
* @apiVersion 1.0.0
|
||||
@@ -657,7 +804,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/column/template 11. 创建项目模板
|
||||
* @api {post} api/system/column/template 14. 创建项目模板
|
||||
*
|
||||
* @apiDescription 获取创建项目模板、保存创建项目模板
|
||||
* @apiVersion 1.0.0
|
||||
@@ -704,7 +851,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/license 12. License
|
||||
* @api {post} api/system/license 15. License
|
||||
*
|
||||
* @apiDescription 获取License信息、保存License(限管理员)
|
||||
* @apiVersion 1.0.0
|
||||
@@ -735,6 +882,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 +891,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'])) {
|
||||
@@ -773,7 +921,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/info 13. 获取终端详细信息
|
||||
* @api {get} api/system/get/info 16. 获取终端详细信息
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -792,8 +940,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 +948,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/ip 14. 获取IP地址
|
||||
* @api {get} api/system/get/ip 17. 获取IP地址
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -817,7 +963,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/cnip 15. 是否中国IP地址
|
||||
* @api {get} api/system/get/cnip 18. 是否中国IP地址
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -834,41 +980,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 19. 上传图片
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -877,7 +989,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 +1046,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/imgview 19. 浏览图片空间
|
||||
* @api {get} api/system/get/imgview 20. 浏览图片空间
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1031,16 +1143,16 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/fileupload 20. 上传文件
|
||||
* @api {post} api/system/fileupload 21. 上传文件
|
||||
*
|
||||
* @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 +1187,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/updatelog 21. 获取更新日志
|
||||
* @api {get} api/system/get/updatelog 22. 获取更新日志
|
||||
*
|
||||
* @apiDescription 获取更新日志
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1108,7 +1220,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 +1230,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/email/check 22. 邮件发送测试(限管理员)
|
||||
* @api {get} api/system/email/check 23. 邮件发送测试(限管理员)
|
||||
*
|
||||
* @apiDescription 测试配置邮箱是否能发送邮件
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1164,7 +1276,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/checkin/export 23. 导出签到数据(限管理员)
|
||||
* @api {get} api/system/checkin/export 24. 导出签到数据(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1180,7 +1292,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 +1322,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 25. 下载导出的签到数据
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1345,21 +1510,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 26. 获取版本号
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1367,23 +1527,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 +1558,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 27. 预加载的资源
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1436,11 +1605,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 +1626,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) {
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Models\UserBot;
|
||||
use App\Models\WebSocket;
|
||||
use App\Models\UmengAlias;
|
||||
use App\Models\UserDelete;
|
||||
use App\Models\UserDevice;
|
||||
use App\Models\UserTransfer;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Models\UserCheckinFace;
|
||||
@@ -154,7 +155,7 @@ class UsersController extends AbstractController
|
||||
//
|
||||
if (!Project::withTrashed()->whereUserid($user->userid)->wherePersonal(1)->exists()) {
|
||||
Project::createProject([
|
||||
'name' => Doo::translate('个人项目'),
|
||||
'name' => "📝 " . Doo::translate('个人项目'),
|
||||
'desc' => Doo::translate('注册时系统自动创建项目,你可以自由删除。'),
|
||||
'personal' => 1,
|
||||
], $user->userid);
|
||||
@@ -267,7 +268,23 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/reg/needinvite 06. 是否需要邀请码
|
||||
* @api {get} api/users/logout 06. 退出登录
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName logout
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
UserDevice::forget();
|
||||
return Base::retSuccess('退出成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/reg/needinvite 07. 是否需要邀请码
|
||||
*
|
||||
* @apiDescription 用于判断注册是否需要邀请码
|
||||
* @apiVersion 1.0.0
|
||||
@@ -286,7 +303,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/info 07. 获取我的信息
|
||||
* @api {get} api/users/info 08. 获取我的信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -336,7 +353,52 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/editdata 08. 修改自己的资料
|
||||
* @api {get} api/users/info/departments 09. 获取我的部门列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName info__departments
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
* @apiSuccessExample {json} data:
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "部门1",
|
||||
"parent_id": 0,
|
||||
"owner_userid": 1
|
||||
},
|
||||
]
|
||||
*/
|
||||
public function info__departments()
|
||||
{
|
||||
$user = User::auth();
|
||||
|
||||
// 获取部门列表
|
||||
$list = UserDepartment::select(['id', 'owner_userid', 'parent_id', 'name'])
|
||||
->whereIn('id', $user->department)
|
||||
->take(10)
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
// 将 owner_userid 等于当前用户的部门排在前面
|
||||
usort($list, function($a, $b) use ($user) {
|
||||
if ($a['owner_userid'] == $user->userid && $b['owner_userid'] != $user->userid) {
|
||||
return -1;
|
||||
} elseif ($a['owner_userid'] != $user->userid && $b['owner_userid'] == $user->userid) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return Base::retSuccess('success', $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/editdata 10. 修改自己的资料
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -428,7 +490,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/editpass 09. 修改自己的密码
|
||||
* @api {get} api/users/editpass 11. 修改自己的密码
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -469,7 +531,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/search 10. 搜索会员列表
|
||||
* @api {get} api/users/search 12. 搜索会员列表
|
||||
*
|
||||
* @apiDescription 搜索会员列表
|
||||
* @apiVersion 1.0.0
|
||||
@@ -477,18 +539,22 @@ class UsersController extends AbstractController
|
||||
* @apiName searchinfo
|
||||
*
|
||||
* @apiParam {Object} keys 搜索条件
|
||||
* - keys.key 昵称、拼音、邮箱关键字
|
||||
* - keys.key 会员ID、昵称、拼音、邮箱关键字
|
||||
* - keys.disable 0-排除离职(默认),1-仅离职,2-含离职
|
||||
* - keys.bot 0-排除机器人(默认),1-仅机器人,2-含机器人
|
||||
* - keys.project_id 在指定项目ID
|
||||
* - keys.no_project_id 不在指定项目ID
|
||||
* - keys.dialog_id 在指定对话ID
|
||||
* - keys.departments 部门ID(多个用逗号分隔)
|
||||
* @apiParam {Object} sorts 排序方式
|
||||
* - sorts.az 按字母:asc|desc
|
||||
* @apiParam {Number} updated_time 在这个时间戳之后更新的
|
||||
* @apiParam {Number} state 获取在线状态
|
||||
* - 0: 不获取(默认)
|
||||
* - 1: 获取会员在线状态,返回数据多一个online值
|
||||
* @apiParam {Number} [with_department] 是否返回部门信息
|
||||
* - 0: 不返回部门信息(默认)
|
||||
* - 1: 返回部门信息(返回数据多一个department_info字段),department_info={id, name, parent_id, owner_userid}
|
||||
*
|
||||
* @apiParam {Number} [take] 获取数量,10-100
|
||||
* @apiParam {Number} [page] 当前页,默认:1(赋值分页模式,take参数无效)
|
||||
@@ -511,12 +577,15 @@ class UsersController extends AbstractController
|
||||
$sorts = Request::input('sorts');
|
||||
$updatedTime = intval(Request::input('updated_time'));
|
||||
$state = intval(Request::input('state', 0));
|
||||
$withDepartment = intval(Request::input('with_department', 0));
|
||||
$keys = is_array($keys) ? $keys : [];
|
||||
$sorts = is_array($sorts) ? $sorts : [];
|
||||
//
|
||||
if ($keys['key']) {
|
||||
if (str_contains($keys['key'], "@")) {
|
||||
$builder->where("email", "like", "%{$keys['key']}%");
|
||||
} elseif (Base::isNumber($keys['key'])) {
|
||||
$builder->where("userid", intval($keys['key']));
|
||||
} else {
|
||||
$builder->where(function($query) use ($keys) {
|
||||
$query->where("nickname", "like", "%{$keys['key']}%")
|
||||
@@ -580,7 +649,7 @@ class UsersController extends AbstractController
|
||||
$list = $builder->orderBy('userid')->take(Base::getPaginate(100, 10, 'take'))->get();
|
||||
}
|
||||
//
|
||||
$list->transform(function (User $userInfo) use ($user, $state) {
|
||||
$list->transform(function (User $userInfo) use ($user, $state, $withDepartment) {
|
||||
$tags = [];
|
||||
$dep = $userInfo->getDepartmentName();
|
||||
$dep = array_values(array_filter(explode(",", $dep), function($item) {
|
||||
@@ -605,13 +674,16 @@ class UsersController extends AbstractController
|
||||
if ($state === 1) {
|
||||
$userInfo->online = $userInfo->getOnlineStatus();
|
||||
}
|
||||
if ($withDepartment) {
|
||||
$userInfo->department_info = UserDepartment::getDepartmentsByIds($userInfo->department);
|
||||
}
|
||||
return $userInfo;
|
||||
});
|
||||
return Base::retSuccess('success', $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/search/ai 11. 获取AI机器人
|
||||
* @api {get} api/users/search/ai 13. 获取AI机器人
|
||||
*
|
||||
* @apiDescription 搜索会员列表
|
||||
* @apiVersion 1.0.0
|
||||
@@ -630,7 +702,7 @@ class UsersController extends AbstractController
|
||||
//
|
||||
$type = trim(Request::input('type'));
|
||||
$botName = "ai-{$type}";
|
||||
if (!UserBot::isAiBot("{$botName}@bot.system")) {
|
||||
if (!UserBot::systemBotName($botName)) {
|
||||
return Base::retError('AI机器人不存在');
|
||||
}
|
||||
//
|
||||
@@ -642,7 +714,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/basic 12. 获取指定会员基础信息
|
||||
* @api {get} api/users/basic 14. 获取指定会员基础信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -685,7 +757,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/lists 13. 会员列表(限管理员)
|
||||
* @api {get} api/users/lists 15. 会员列表(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -834,7 +906,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/operation 14. 操作会员(限管理员)
|
||||
* @api {get} api/users/operation 16. 操作会员(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -925,8 +997,7 @@ class UsersController extends AbstractController
|
||||
return UserCheckinMac::saveMac($userInfo->userid, $array);
|
||||
|
||||
case 'checkin_face':
|
||||
$faceimg = $data['checkin_face'] ? $data['checkin_face'] : '';
|
||||
|
||||
$faceimg = $data['checkin_face'] ?: '';
|
||||
return UserCheckinFace::saveFace($userInfo->userid, $userInfo->nickname, $faceimg, "管理员上传");
|
||||
|
||||
case 'department':
|
||||
@@ -1092,7 +1163,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/verification 15. 邮箱验证
|
||||
* @api {get} api/users/email/verification 17. 邮箱验证
|
||||
*
|
||||
* @apiDescription 不需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1140,7 +1211,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/umeng/alias 16. 设置友盟别名
|
||||
* @api {get} api/users/umeng/alias 18. 设置友盟别名
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1154,6 +1225,7 @@ class UsersController extends AbstractController
|
||||
* @apiParam {String} [userAgent] 浏览器信息
|
||||
* @apiParam {String} [deviceModel] 设备型号
|
||||
* @apiParam {String} [isNotified] 是否有通知权限(0不通知、1通知)
|
||||
* @apiParam {Number} [isDebug] 是否调试(0不调试、1调试)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1162,6 +1234,10 @@ class UsersController extends AbstractController
|
||||
public function umeng__alias()
|
||||
{
|
||||
$data = Request::input();
|
||||
// 判断是否调试
|
||||
if (intval($data['isDebug'])) {
|
||||
return Base::retError('调试模式下不允许使用');
|
||||
}
|
||||
// 表单验证
|
||||
Base::validator($data, [
|
||||
'alias.required' => '别名不能为空',
|
||||
@@ -1192,6 +1268,7 @@ class UsersController extends AbstractController
|
||||
$row->update([
|
||||
'ua' => $data['userAgent'],
|
||||
'device' => $data['deviceModel'],
|
||||
'device_hash' => UserDevice::check(),
|
||||
'version' => $version,
|
||||
'is_notified' => $isNotified,
|
||||
'updated_at' => Carbon::now()
|
||||
@@ -1201,6 +1278,7 @@ class UsersController extends AbstractController
|
||||
$row = UmengAlias::createInstance(array_merge($inArray, [
|
||||
'ua' => $data['userAgent'],
|
||||
'device' => $data['deviceModel'],
|
||||
'device_hash' => UserDevice::check(),
|
||||
'version' => $version,
|
||||
'is_notified' => $isNotified,
|
||||
]));
|
||||
@@ -1212,7 +1290,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/open 17. 【会议】创建会议、加入会议
|
||||
* @api {get} api/users/meeting/open 19. 【会议】创建会议、加入会议
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1330,7 +1408,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/link 18. 【会议】获取分享链接
|
||||
* @api {get} api/users/meeting/link 20. 【会议】获取分享链接
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1359,7 +1437,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/tourist 19. 【会议】游客信息
|
||||
* @api {get} api/users/meeting/tourist 21. 【会议】游客信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1382,7 +1460,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/invitation 20. 【会议】发送邀请
|
||||
* @api {get} api/users/meeting/invitation 22. 【会议】发送邀请
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1429,7 +1507,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/send 21. 发送邮箱验证码
|
||||
* @api {get} api/users/email/send 23. 发送邮箱验证码
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1469,7 +1547,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/edit 22. 修改邮箱
|
||||
* @api {get} api/users/email/edit 24. 修改邮箱
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1514,7 +1592,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/delete/account 23. 删除帐号
|
||||
* @api {get} api/users/delete/account 25. 删除帐号
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1576,7 +1654,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/department/list 24. 部门列表(限管理员)
|
||||
* @api {get} api/users/department/list 26. 部门列表(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1595,7 +1673,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/department/add 25. 新建、修改部门(限管理员)
|
||||
* @api {get} api/users/department/add 27. 新建、修改部门(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1653,19 +1731,22 @@ class UsersController extends AbstractController
|
||||
if (empty($parentDepartment)) {
|
||||
return Base::retError('上级部门不存在或已被删除');
|
||||
}
|
||||
if ($parentDepartment->parent_id > 0) {
|
||||
return Base::retError('上级部门层级错误');
|
||||
if (count($parentDepartment->parents()) > 2) {
|
||||
return Base::retError('部门层级最多只能创建3级');
|
||||
}
|
||||
if (UserDepartment::whereParentId($parent_id)->count() > 20) {
|
||||
if ($id > 0 && UserDepartment::whereParentId($id)->whereId($parent_id)->exists()) {
|
||||
return Base::retError('不能选择自己的子部门作为上级部门');
|
||||
}
|
||||
if (UserDepartment::whereParentId($parent_id)->count() >= 20) {
|
||||
return Base::retError('每个部门最多只能创建20个子部门');
|
||||
}
|
||||
if ($id > 0 && UserDepartment::whereParentId($id)->exists()) {
|
||||
return Base::retError('含有子部门无法修改上级部门');
|
||||
}
|
||||
}
|
||||
if (empty($owner_userid) || !User::whereUserid($owner_userid)->exists()) {
|
||||
return Base::retError('请选择正确的部门负责人');
|
||||
}
|
||||
if (UserDepartment::whereOwnerUserid($owner_userid)->count() >= 10) {
|
||||
return Base::retError('每个用户最多只能负责10个部门');
|
||||
}
|
||||
//
|
||||
$userDepartment->saveDepartment([
|
||||
'name' => $name,
|
||||
@@ -1674,11 +1755,11 @@ class UsersController extends AbstractController
|
||||
], $dialog_useid);
|
||||
Cache::forever("UserDepartment::rand", Base::generatePassword());
|
||||
//
|
||||
return Base::retSuccess($parent_id > 0 ? '保存成功' : '新建成功');
|
||||
return Base::retSuccess($id > 0 ? '保存成功' : '新建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/department/del 26. 删除部门(限管理员)
|
||||
* @api {get} api/users/department/del 28. 删除部门(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1701,6 +1782,9 @@ class UsersController extends AbstractController
|
||||
if (empty($userDepartment)) {
|
||||
return Base::retError('部门不存在或已被删除');
|
||||
}
|
||||
if (UserDepartment::whereParentId($id)->exists()) {
|
||||
return Base::retError('含有子部门无法删除');
|
||||
}
|
||||
$userDepartment->deleteDepartment();
|
||||
Cache::forever("UserDepartment::rand", Base::generatePassword());
|
||||
//
|
||||
@@ -1708,7 +1792,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/checkin/get 27. 获取签到设置
|
||||
* @api {get} api/users/checkin/get 29. 获取签到设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1735,7 +1819,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/checkin/save 28. 保存签到设置
|
||||
* @api {post} api/users/checkin/save 30. 保存签到设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1810,7 +1894,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/checkin/list 29. 获取签到数据
|
||||
* @api {get} api/users/checkin/list 31. 获取签到数据
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1857,7 +1941,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/socket/status 30. 获取socket状态
|
||||
* @api {get} api/users/socket/status 32. 获取socket状态
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1880,7 +1964,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/key/client 31. 客户端KEY
|
||||
* @api {get} api/users/key/client 33. 客户端KEY
|
||||
*
|
||||
* @apiDescription 获取客户端KEY,用于加密数据发送给服务端
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1922,7 +2006,51 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/bot/info 32. 机器人信息
|
||||
* @api {get} api/users/bot/list 34. 机器人列表
|
||||
*
|
||||
* @apiDescription 需要token身份,获取我的机器人列表
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__list
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function bot__list()
|
||||
{
|
||||
// 获取当前认证用户
|
||||
$user = User::auth();
|
||||
|
||||
// 使用连表查询一次性获取所有机器人数据
|
||||
$bots = User::join('user_bots', 'user_bots.bot_id', '=', 'users.userid')
|
||||
->where('user_bots.userid', $user->userid)
|
||||
->select([
|
||||
'users.userid',
|
||||
'users.nickname',
|
||||
'users.userimg',
|
||||
'user_bots.clear_day',
|
||||
'user_bots.webhook_url'
|
||||
])
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->toArray();
|
||||
foreach ($bots as &$bot) {
|
||||
$bot['id'] = $bot['userid'];
|
||||
$bot['name'] = $bot['nickname'];
|
||||
$bot['avatar'] = $bot['userimg'];
|
||||
$bot['system_name'] = UserBot::systemBotName($bot['name']);
|
||||
unset($bot['userid'], $bot['nickname'], $bot['userimg']);
|
||||
}
|
||||
|
||||
// 返回成功响应,将机器人列表包装在list字段中
|
||||
return Base::retSuccess('success', [
|
||||
'list' => $bots
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/bot/info 35. 机器人信息
|
||||
*
|
||||
* @apiDescription 需要token身份,获取我的机器人信息
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1973,16 +2101,21 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/bot/edit 33. 编辑机器人
|
||||
* @api {post} api/users/bot/edit 36. 添加、编辑机器人
|
||||
*
|
||||
* @apiDescription 需要token身份,编辑 我的机器人 或 管理员修改系统机器人 信息
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__edit
|
||||
*
|
||||
* @apiParam {Number} id 机器人ID
|
||||
* @apiParam {Number} [id] 机器人ID(编辑时必填,留空为添加)
|
||||
* @apiParam {String} [name] 机器人名称
|
||||
* @apiParam {String} [avatar] 机器人头像
|
||||
* @apiParam {Number} [session] 开启新会话功能(仅 我的机器人)
|
||||
* - 1:开启、0:关闭, 默认:0
|
||||
* - 此参数仅在添加机器人时有效
|
||||
* - 开启后,机器人对话窗口会出现新会话菜单和历史会话菜单
|
||||
* - 开启后,webhook_url 消息会多一个 session_id 字段
|
||||
* @apiParam {Number} [clear_day] 清理天数(仅 我的机器人)
|
||||
* @apiParam {String} [webhook_url] Webhook地址(仅 我的机器人)
|
||||
*
|
||||
@@ -1995,10 +2128,20 @@ class UsersController extends AbstractController
|
||||
$user = User::auth();
|
||||
//
|
||||
$botId = intval(Request::input('id'));
|
||||
$botUser = User::whereUserid($botId)->whereBot(1)->first();
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('机器人不存在');
|
||||
$session = intval(Request::input('session'));
|
||||
if (empty($botId)) {
|
||||
$res = UserBot::newBot($user->userid, trim(Request::input('name')), (bool)$session);
|
||||
if (Base::isError($res)) {
|
||||
return $res;
|
||||
}
|
||||
$botUser = $res['data'];
|
||||
} else {
|
||||
$botUser = User::whereUserid($botId)->whereBot(1)->first();
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('机器人不存在');
|
||||
}
|
||||
}
|
||||
//
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
|
||||
if (empty($userBot)) {
|
||||
if (UserBot::systemBotName($botUser->email)) {
|
||||
@@ -2016,10 +2159,10 @@ class UsersController extends AbstractController
|
||||
$upUser = [];
|
||||
$upBot = [];
|
||||
//
|
||||
if (isset($data['name'])) {
|
||||
if (Arr::exists($data, 'name')) {
|
||||
$upUser['nickname'] = trim($data['name']);
|
||||
}
|
||||
if (isset($data['avatar'])) {
|
||||
if (Arr::exists($data, 'avatar')) {
|
||||
$avatar = $data['avatar'];
|
||||
$avatar = $avatar ? Base::unFillUrl(is_array($avatar) ? $avatar[0]['path'] : $avatar) : '';
|
||||
if (str_contains($avatar, 'avatar/')) {
|
||||
@@ -2027,10 +2170,10 @@ class UsersController extends AbstractController
|
||||
}
|
||||
$upUser['userimg'] = $avatar;
|
||||
}
|
||||
if (isset($data['clear_day'])) {
|
||||
if (Arr::exists($data, 'clear_day')) {
|
||||
$upBot['clear_day'] = min(max(intval($data['clear_day']), 1), 999);
|
||||
}
|
||||
if (isset($data['webhook_url'])) {
|
||||
if (Arr::exists($data, 'webhook_url')) {
|
||||
$upBot['webhook_url'] = trim($data['webhook_url']);
|
||||
}
|
||||
//
|
||||
@@ -2055,17 +2198,68 @@ class UsersController extends AbstractController
|
||||
$data['clear_day'] = $userBot->clear_day;
|
||||
$data['webhook_url'] = $userBot->webhook_url;
|
||||
}
|
||||
return Base::retSuccess('修改成功', $data);
|
||||
return Base::retSuccess($botId ? '修改成功' : '添加成功', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/share/list 34. 获取分享列表
|
||||
* @api {get} api/users/bot/delete 37. 删除机器人
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__delete
|
||||
*
|
||||
* @apiParam {Number} id 机器人ID
|
||||
* @apiParam {String} remark 删除备注
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function bot__delete()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$botId = intval(Request::input('id'));
|
||||
$remark = trim(Request::input('remark'));
|
||||
//
|
||||
if (empty($remark)) {
|
||||
return Base::retError('请输入删除备注');
|
||||
}
|
||||
if (mb_strlen($remark) > 255) {
|
||||
return Base::retError('删除备注长度限制255个字');
|
||||
}
|
||||
//
|
||||
$botUser = User::whereUserid($botId)->whereBot(1)->first();
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('机器人不存在');
|
||||
}
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
|
||||
if (empty($userBot)) {
|
||||
if (UserBot::systemBotName($botUser->email)) {
|
||||
// 系统机器人(仅限管理员)
|
||||
return Base::retError('系统机器人不能删除');
|
||||
} else {
|
||||
// 其他用户的机器人(仅限主人)
|
||||
return Base::retError('不是你的机器人');
|
||||
}
|
||||
}
|
||||
//
|
||||
if (!$botUser->deleteUser($remark)) {
|
||||
return Base::retError('删除失败');
|
||||
}
|
||||
return Base::retSuccess('删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/share/list 38. 获取分享列表
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName share__list
|
||||
*
|
||||
* @apiParam {String} [type] 分享类型:file-文件,text-列表 默认file
|
||||
* @apiParam {String} [key] 搜索关键词(用于搜索会话)
|
||||
* @apiParam {Number} [pid] 父级文件id,用于获取子目录和上传到指定目录的id
|
||||
* @apiParam {Number} [upload_file_id] 上传文件id
|
||||
*
|
||||
@@ -2077,6 +2271,7 @@ class UsersController extends AbstractController
|
||||
{
|
||||
$user = User::auth();
|
||||
$type = Request::input('type', 'file');
|
||||
$key = Request::input('key');
|
||||
$pid = intval(Request::input('pid', -1));
|
||||
$uploadFileId = intval(Request::input('upload_file_id', -1));
|
||||
// 上传文件
|
||||
@@ -2109,10 +2304,14 @@ class UsersController extends AbstractController
|
||||
'icon' => url("images/file/light/folder.png"),
|
||||
'extend' => ['upload_file_id' => 0],
|
||||
'name' => Doo::translate('文件'),
|
||||
'sort' => Carbon::parse("9999")->timestamp,
|
||||
];
|
||||
}
|
||||
$dialogList = WebSocketDialog::getDialogList($user->userid);
|
||||
foreach ($dialogList['data'] as $dialog) {
|
||||
$dialogTake = 50;
|
||||
$dialogList = WebSocketDialog::searchDialog($user->userid, $key, $dialogTake);
|
||||
$dialogIds = [];
|
||||
$itemUrl = $type == "file" ? Base::fillUrl("api/dialog/msg/sendfiles") : Base::fillUrl("api/dialog/msg/sendtext");
|
||||
foreach ($dialogList as $dialog) {
|
||||
if ($dialog['avatar']) {
|
||||
$avatar = url($dialog['avatar']);
|
||||
} else if ($dialog['type'] == 'user') {
|
||||
@@ -2129,7 +2328,8 @@ class UsersController extends AbstractController
|
||||
'type' => 'item',
|
||||
'name' => $dialog['name'],
|
||||
'icon' => $avatar,
|
||||
'url' => $type == "file" ? Base::fillUrl("api/dialog/msg/sendfiles") : Base::fillUrl("api/dialog/msg/sendtext"),
|
||||
'url' => $itemUrl,
|
||||
'sort' => Carbon::parse($dialog['last_at'])->timestamp,
|
||||
'extend' => [
|
||||
'dialog_ids' => $dialog['id'],
|
||||
'text_type' => 'text',
|
||||
@@ -2137,6 +2337,33 @@ class UsersController extends AbstractController
|
||||
'silence' => 'no'
|
||||
]
|
||||
];
|
||||
$dialogIds[] = $dialog['id'];
|
||||
}
|
||||
if ($key && count($dialogList) < $dialogTake) {
|
||||
$dialogUsers = User::searchUser($key, $dialogTake - count($dialogList));
|
||||
foreach ($dialogUsers as $item) {
|
||||
$dialog = WebSocketDialog::getUserDialog($user->userid, $item->userid, now()->addDay());
|
||||
if ($dialog && !in_array($dialog->id, $dialogIds)) {
|
||||
$lists[] = [
|
||||
'type' => 'item',
|
||||
'name' => $item->nickname,
|
||||
'icon' => $item->userimg,
|
||||
'url' => $itemUrl,
|
||||
'sort' => Carbon::parse($item->line_at)->timestamp,
|
||||
'extend' => [
|
||||
'dialog_ids' => $dialog->id,
|
||||
'text_type' => 'text',
|
||||
'reply_id' => 0,
|
||||
'silence' => 'no'
|
||||
]
|
||||
];
|
||||
$dialogIds[] = $dialog->id;
|
||||
}
|
||||
}
|
||||
// 根据 $lists sort 从大到小排序
|
||||
usort($lists, function ($a, $b) {
|
||||
return $b['sort'] <=> $a['sort'];
|
||||
});
|
||||
}
|
||||
}
|
||||
// 返回
|
||||
@@ -2144,7 +2371,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/annual/report 35. 年度报告
|
||||
* @api {get} api/users/annual/report 39. 年度报告
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
@@ -2311,4 +2538,97 @@ class UsersController extends AbstractController
|
||||
//
|
||||
return Base::retSuccess('success', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/device/list 40. 获取设备列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName device__list
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function device__list()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$list = UserDevice::whereUserid($user->userid)->orderByDesc('id')->take(UserDevice::$deviceLimit)->get();
|
||||
//
|
||||
return Base::retSuccess('success', [
|
||||
'list' => $list
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/device/logout 41. 登出设备(删除设备)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName device__logout
|
||||
*
|
||||
* @apiParam {Number} id 设备id
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function device__logout()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = intval(Request::input('id'));
|
||||
if (empty($id)) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
$userDevice = UserDevice::whereUserid($user->userid)->whereId($id)->first();
|
||||
if (empty($userDevice)) {
|
||||
return Base::retError('设备不存在或已被删除');
|
||||
}
|
||||
UserDevice::forget($userDevice->id);
|
||||
//
|
||||
return Base::retSuccess('操作成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/device/edit 42. 编辑设备
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName device__edit
|
||||
*
|
||||
* @apiParam {Object} detail 设备信息
|
||||
* @apiParam {String} detail.device_name 设备名称
|
||||
* @apiParam {String} detail.app_brand 设备品牌
|
||||
* @apiParam {String} detail.app_model 设备型号
|
||||
* @apiParam {String} detail.app_os 设备操作系统
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function device__edit()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$detail = Request::input();
|
||||
$detail = array_intersect_key($detail, array_flip([ 'device_name', 'app_brand', 'app_model','app_os']));
|
||||
if (empty($detail)) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
//
|
||||
$row = UserDevice::record();
|
||||
if (empty($row)) {
|
||||
return Base::retError('设备不存在或已被删除');
|
||||
}
|
||||
$deviceInfo = array_merge(Base::json2array($row->detail), $detail);
|
||||
$row->detail = Base::array2json($deviceInfo);
|
||||
$row->save();
|
||||
//
|
||||
return Base::retSuccess('保存成功');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
/**
|
||||
@@ -85,6 +83,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 +248,12 @@ 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 DeleteBotMsgTask());
|
||||
// 周期任务
|
||||
@@ -258,6 +266,8 @@ class IndexController extends InvokeController
|
||||
Task::deliver(new UnclaimedTaskRemindTask());
|
||||
// 关闭会议室
|
||||
Task::deliver(new CloseMeetingRoomTask());
|
||||
// ZincSearch 同步
|
||||
Task::deliver(new ZincSearchSyncTask());
|
||||
|
||||
return "success";
|
||||
}
|
||||
@@ -321,7 +331,7 @@ class IndexController extends InvokeController
|
||||
"file" => Request::file('file'),
|
||||
"type" => 'publish',
|
||||
"path" => $draftPath,
|
||||
"fileName" => true,
|
||||
"saveName" => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -342,9 +352,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 +430,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 +472,7 @@ class IndexController extends InvokeController
|
||||
action: "eeuiAppSendMessage",
|
||||
data: [
|
||||
{
|
||||
action: 'setPageData',
|
||||
action: 'setPageData', // 设置页面数据
|
||||
data: {
|
||||
showProgress: true,
|
||||
titleFixed: true,
|
||||
@@ -476,7 +480,7 @@ class IndexController extends InvokeController
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'createTarget',
|
||||
action: 'createTarget', // 创建目标(访问新地址)
|
||||
url: "{$redirectUrl}",
|
||||
}
|
||||
]
|
||||
@@ -490,7 +494,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 +502,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;
|
||||
@@ -117,7 +118,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 +642,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 +980,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,34 @@ 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 ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件内容
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ class Project extends AbstractModel
|
||||
'important' => 1
|
||||
], function () use ($userid) {
|
||||
return [
|
||||
'important' => 1,
|
||||
'bot' => User::isBot($userid) ? 1 : 0,
|
||||
];
|
||||
});
|
||||
@@ -419,29 +420,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 +471,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 +592,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 流转模式
|
||||
@@ -38,6 +39,7 @@ use App\Module\Base;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereSort($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereColor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereTurns($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereUserids($value)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -36,7 +36,6 @@ namespace App\Models;
|
||||
class ProjectTag extends AbstractModel
|
||||
{
|
||||
protected $hidden = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
|
||||
@@ -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,9 +1143,6 @@ class ProjectTask extends AbstractModel
|
||||
*/
|
||||
public function copyTask()
|
||||
{
|
||||
if ($this->parent_id > 0) {
|
||||
throw new ApiException('子任务禁止复制');
|
||||
}
|
||||
return AbstractModel::transaction(function() {
|
||||
// 复制任务
|
||||
$task = $this->replicate();
|
||||
@@ -1182,6 +1203,7 @@ class ProjectTask extends AbstractModel
|
||||
'important' => 1
|
||||
], function () use ($userid) {
|
||||
return [
|
||||
'important' => 1,
|
||||
'bot' => User::isBot($userid) ? 1 : 0,
|
||||
];
|
||||
});
|
||||
@@ -1328,6 +1350,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 +1362,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 +1375,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 +1431,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 +1445,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,
|
||||
@@ -1801,16 +1847,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,31 +1904,90 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AI上下文
|
||||
* @return array
|
||||
*/
|
||||
public function AIContext()
|
||||
{
|
||||
$contexts = [];
|
||||
if ($this->archived_at) {
|
||||
$contexts[] = "任务状态:已归档";
|
||||
$contexts[] = "归档时间:" . $this->archived_at;
|
||||
} elseif ($this->complete_at) {
|
||||
$contexts[] = "任务状态:已完成";
|
||||
$contexts[] = "完成时间:" . $this->complete_at;
|
||||
} elseif ($this->end_at && Carbon::parse($this->end_at)->lt(Carbon::now())) {
|
||||
$contexts[] = "任务状态:已过期";
|
||||
$contexts[] = "任务截止时间:" . $this->end_at;
|
||||
} else {
|
||||
$contexts[] = "任务状态:进行中";
|
||||
if ($this->start_at) {
|
||||
$contexts[] = "任务开始时间:" . $this->start_at;
|
||||
}
|
||||
if ($this->end_at) {
|
||||
$contexts[] = "任务截止时间:" . $this->end_at;
|
||||
}
|
||||
}
|
||||
$contexts[] = "当前系统时间:" . Carbon::now()->toDateTimeString();
|
||||
if ($this->content) {
|
||||
$taskDesc = $this->content?->getContentInfo();
|
||||
if ($taskDesc) {
|
||||
$descContent = Base::cutStr(Base::html2markdown($taskDesc['content'], ['strip_tags' => true]), 2000);
|
||||
$contexts[] = <<<EOF
|
||||
任务描述:
|
||||
```md
|
||||
{$descContent}
|
||||
```
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
$subTask = ProjectTask::select(['id', 'name', 'complete_at', 'end_at'])->whereParentId($this->id)->get();
|
||||
if ($subTask->isNotEmpty()) {
|
||||
$subTaskContent = $subTask->map(function($item) {
|
||||
if ($item->complete_at) {
|
||||
$status = " (已完成)";
|
||||
} elseif ($item->end_at && Carbon::parse($item->end_at)->lt(Carbon::now())) {
|
||||
$status = " (已过期)";
|
||||
} else {
|
||||
$status = " (进行中)";
|
||||
}
|
||||
return " - {$item->name} {$status}";
|
||||
})->join("\n");
|
||||
if ($subTaskContent) {
|
||||
$contexts[] = <<<EOF
|
||||
子任务列表:
|
||||
{$subTaskContent}
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务
|
||||
* @param $task_id
|
||||
|
||||
@@ -6,8 +6,10 @@ namespace App\Models;
|
||||
* App\Models\ProjectTaskTag
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $tag_id
|
||||
* @property int|null $project_id 项目ID
|
||||
* @property int|null $task_id 任务ID
|
||||
* @property int|null $userid 用户ID(创建者)
|
||||
* @property string|null $name 标题
|
||||
* @property string|null $color 颜色
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
@@ -26,8 +28,10 @@ namespace App\Models;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereTagId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTaskTag extends AbstractModel
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -74,7 +74,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;
|
||||
@@ -95,6 +96,24 @@ class Report extends AbstractModel
|
||||
return $this->appendattrs['receives'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取汇报内容
|
||||
* @param $id
|
||||
* @return self|null
|
||||
*/
|
||||
public static function idOrCodeToContent($id)
|
||||
{
|
||||
if (Base::isNumber($id)) {
|
||||
return self::find($id);
|
||||
} elseif ($id) {
|
||||
$reportLink = ReportLink::whereCode($id)->first();
|
||||
if ($reportLink) {
|
||||
return self::find($reportLink->rid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条记录
|
||||
* @param $id
|
||||
|
||||
86
app/Models/ReportLink.php
Normal file
86
app/Models/ReportLink.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\ReportLink
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $rid 报告ID
|
||||
* @property int|null $num 累计访问
|
||||
* @property string|null $code 链接码
|
||||
* @property int|null $userid 会员ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Report|null $report
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereRid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ReportLink extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function report(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(Report::class, 'id', 'report_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成链接
|
||||
* @param $rid
|
||||
* @param $userid
|
||||
* @param $refresh
|
||||
* @return array
|
||||
*/
|
||||
public static function generateLink($rid, $userid, $refresh = false)
|
||||
{
|
||||
$report = Report::find($rid);
|
||||
if (empty($report)) {
|
||||
throw new ApiException('报告不存在或已被删除');
|
||||
}
|
||||
if ($report->userid != $userid) {
|
||||
if (!ReportReceive::whereRid($rid)->whereUserid($userid)->exists()) {
|
||||
throw new ApiException('您没有权限查看该报告');
|
||||
}
|
||||
}
|
||||
$reportLink = ReportLink::whereRid($rid)->whereUserid($userid)->first();
|
||||
if (empty($reportLink)) {
|
||||
$reportLink = ReportLink::createInstance([
|
||||
'rid' => $rid,
|
||||
'userid' => $userid,
|
||||
'code' => base64_encode("{$rid},{$userid}," . Base::generatePassword()),
|
||||
]);
|
||||
$reportLink->save();
|
||||
} else {
|
||||
if ($refresh == 'yes') {
|
||||
$reportLink->code = base64_encode("{$rid},{$userid}," . Base::generatePassword());
|
||||
$reportLink->save();
|
||||
}
|
||||
}
|
||||
return [
|
||||
'id' => $rid,
|
||||
'url' => Base::fillUrl('single/report/detail/' . $reportLink->code),
|
||||
'code' => $reportLink->code,
|
||||
'num' => $reportLink->num
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Timer;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\Setting
|
||||
@@ -45,6 +48,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 +59,54 @@ 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 'aiSetting':
|
||||
$value['ai_provider'] = $value['ai_provider'] ?: 'openai';
|
||||
$value['ai_api_key'] = $value['ai_api_key'] ?: '';
|
||||
$value['ai_api_url'] = $value['ai_api_url'] ?: '';
|
||||
$value['ai_proxy'] = $value['ai_proxy'] ?: '';
|
||||
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);
|
||||
}
|
||||
if (empty($content)) {
|
||||
$content = self::AIBotDefaultModels($aiName);
|
||||
}
|
||||
$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 +116,126 @@ 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'];
|
||||
return !!Base::settingFind('aiSetting', 'ai_api_key');
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 机器人默认模型
|
||||
* @param string $ai
|
||||
* @return array
|
||||
*/
|
||||
public static function AIBotDefaultModels($ai = 'openai')
|
||||
{
|
||||
return match ($ai) {
|
||||
'openai' => [
|
||||
'gpt-4.1 | GPT-4.1',
|
||||
'gpt-4o | GPT-4o',
|
||||
'gpt-4 | GPT-4',
|
||||
'gpt-4o-mini | GPT-4o Mini',
|
||||
'gpt-4-turbo | GPT-4 Turbo',
|
||||
'o3 (thinking) | GPT-o3',
|
||||
'o1 | GPT-o1',
|
||||
'o4-mini | GPT-o4 Mini',
|
||||
'o3-mini | GPT-o3 Mini',
|
||||
'o1-mini | GPT-o1 Mini',
|
||||
'gpt-3.5-turbo | GPT-3.5 Turbo',
|
||||
'gpt-3.5-turbo-16k | GPT-3.5 Turbo 16K',
|
||||
'gpt-3.5-turbo-0125 | GPT-3.5 Turbo 0125',
|
||||
'gpt-3.5-turbo-1106 | GPT-3.5 Turbo 1106'
|
||||
],
|
||||
'claude' => [
|
||||
'claude-opus-4-0 (thinking) | Claude Opus 4',
|
||||
'claude-sonnet-4-0 (thinking) | Claude Sonnet 4',
|
||||
'claude-3-7-sonnet-latest (thinking) | Claude Sonnet 3.7',
|
||||
'claude-3-5-sonnet-latest | Claude Sonnet 3.5',
|
||||
'claude-3-5-haiku-latest | Claude Haiku 3.5',
|
||||
'claude-3-opus-latest | Claude Opus 3'
|
||||
],
|
||||
'deepseek' => [
|
||||
'deepseek-chat | DeepSeek V3',
|
||||
'deepseek-reasoner | DeepSeek R1'
|
||||
],
|
||||
'gemini' => [
|
||||
'gemini-2.5-pro-preview-05-06 (thinking) | Gemini 2.5 Pro Preview',
|
||||
'gemini-2.0-flash | Gemini 2.0 Flash',
|
||||
'gemini-2.0-flash-lite | Gemini 2.0 Flash-Lite',
|
||||
'gemini-1.5-flash | Gemini 1.5 Flash',
|
||||
'gemini-1.5-flash-8b | Gemini 1.5 Flash 8B',
|
||||
'gemini-1.5-pro | Gemini 1.5 Pro',
|
||||
'gemini-1.0-pro | Gemini 1.0 Pro'
|
||||
],
|
||||
'grok' => [
|
||||
'grok-3-latest | Grok 3',
|
||||
'grok-3-fast-latest | Grok 3 Fast',
|
||||
'grok-3-mini-latest | Grok 3 Mini',
|
||||
'grok-3-mini-fast-latest | Grok 3 Mini Fast',
|
||||
'grok-2-vision-latest | Grok 2 Vision',
|
||||
'grok-2-latest | Grok 2',
|
||||
],
|
||||
'zhipu' => [
|
||||
'glm-4 | GLM-4',
|
||||
'glm-4-plus | GLM-4 Plus',
|
||||
'glm-4-air | GLM-4 Air',
|
||||
'glm-4-airx | GLM-4 AirX',
|
||||
'glm-4-long | GLM-4 Long',
|
||||
'glm-4-flash | GLM-4 Flash',
|
||||
'glm-4v | GLM-4V',
|
||||
'glm-4v-plus | GLM-4V Plus',
|
||||
'glm-3-turbo | GLM-3 Turbo'
|
||||
],
|
||||
'qianwen' => [
|
||||
'qwen-max | QWEN Max',
|
||||
'qwen-max-latest | QWEN Max Latest',
|
||||
'qwen-turbo | QWEN Turbo',
|
||||
'qwen-turbo-latest | QWEN Turbo Latest',
|
||||
'qwen-plus | QWEN Plus',
|
||||
'qwen-plus-latest | QWEN Plus Latest',
|
||||
'qwen-long | QWEN Long'
|
||||
],
|
||||
'wenxin' => [
|
||||
'ernie-4.0-8k | Ernie 4.0 8K',
|
||||
'ernie-4.0-8k-latest | Ernie 4.0 8K Latest',
|
||||
'ernie-4.0-turbo-128k | Ernie 4.0 Turbo 128K',
|
||||
'ernie-4.0-turbo-8k | Ernie 4.0 Turbo 8K',
|
||||
'ernie-3.5-128k | Ernie 3.5 128K',
|
||||
'ernie-3.5-8k | Ernie 3.5 8K',
|
||||
'ernie-speed-128k | Ernie Speed 128K',
|
||||
'ernie-speed-8k | Ernie Speed 8K',
|
||||
'ernie-lite-8k | Ernie Lite 8K',
|
||||
'ernie-tiny-8k | Ernie Tiny 8K'
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 机器人模型转数组
|
||||
* @param $models
|
||||
* @param bool $retValue
|
||||
* @return array
|
||||
*/
|
||||
public static function 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 +274,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,68 @@ 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;
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($first['platform']) {
|
||||
case 'ios':
|
||||
$instance = new IOS($first['config']);
|
||||
break;
|
||||
case 'android':
|
||||
$instance = new Android($first['config']);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
$instance->send($first['data']);
|
||||
} catch (\Exception $e) {
|
||||
$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 {
|
||||
self::sendTask();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送内容处理
|
||||
* @param $string
|
||||
@@ -88,13 +138,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'] ?: ''); // 标题
|
||||
@@ -108,65 +158,71 @@ class UmengAlias extends AbstractModel
|
||||
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,
|
||||
],
|
||||
], $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,
|
||||
],
|
||||
'channel_properties' => [
|
||||
'oppo_channel_id' => 'dootask',
|
||||
'vivo_category' => 'IM',
|
||||
'huawei_channel_importance' => 'NORMAL',
|
||||
'huawei_channel_category' => 'IM',
|
||||
'channel_fcm' => 0,
|
||||
],
|
||||
]
|
||||
]);
|
||||
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
@@ -173,10 +172,9 @@ class User extends AbstractModel
|
||||
return UserDepartment::where('owner_userid', $this->userid)->exists();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取机器人所有者
|
||||
* @return int|mixed
|
||||
* @return int
|
||||
*/
|
||||
public function getBotOwner()
|
||||
{
|
||||
@@ -184,9 +182,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 +240,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 +442,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 +466,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 +529,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 +654,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 +737,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 +750,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use Carbon\Carbon;
|
||||
* @property int $id
|
||||
* @property int|null $userid 所属人ID
|
||||
* @property int|null $bot_id 机器人ID
|
||||
* @property int|null $session 开启新会话功能
|
||||
* @property int|null $clear_day 消息自动清理天数
|
||||
* @property \Illuminate\Support\Carbon|null $clear_at 下一次清理时间
|
||||
* @property string|null $webhook_url 消息webhook地址
|
||||
@@ -36,6 +37,7 @@ use Carbon\Carbon;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereClearDay($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereSession($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereWebhookNum($value)
|
||||
@@ -55,16 +57,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 +76,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 +99,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 +176,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 +236,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 +247,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 +436,49 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,47 @@ class UserDepartment extends AbstractModel
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门基本信息(缓存时间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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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', [
|
||||
@@ -666,6 +693,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 +755,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 +771,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 +820,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 +857,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 +934,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 +948,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,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
@@ -18,6 +19,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 +29,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 +306,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 +386,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 +406,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 +421,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 +436,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 +477,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 +490,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 +561,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 +677,25 @@ 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 = self::previewConvertTaskList($text);
|
||||
}
|
||||
}
|
||||
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text);
|
||||
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?>/", "[" . Doo::translate('动画表情') . "]", $text);
|
||||
@@ -759,9 +793,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':
|
||||
@@ -885,8 +925,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 +969,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 +977,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 +993,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 +1034,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 +1075,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 +1219,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
//
|
||||
$updateData = [
|
||||
'type' => $type,
|
||||
'mtype' => $mtype,
|
||||
'link' => $link,
|
||||
'msg' => array_merge($oldMsg, $msg),
|
||||
@@ -1229,6 +1310,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,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)
|
||||
|
||||
461
app/Module/AI.php
Normal file
461
app/Module/AI.php
Normal file
@@ -0,0 +1,461 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* AI 助手模块
|
||||
*/
|
||||
class AI
|
||||
{
|
||||
protected $post = [];
|
||||
protected $headers = [];
|
||||
protected $urlPath = '';
|
||||
protected $timeout = 30;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求 AI 接口
|
||||
* @param bool $resRaw 是否返回原始数据
|
||||
* @return array
|
||||
*/
|
||||
public function request($resRaw = false)
|
||||
{
|
||||
$aiSetting = Base::setting('aiSetting');
|
||||
if (!Setting::AIOpen()) {
|
||||
return Base::retError("AI 助手未开启");
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $aiSetting['ai_api_key'],
|
||||
];
|
||||
if ($aiSetting['ai_proxy']) {
|
||||
$headers['CURLOPT_PROXY'] = $aiSetting['ai_proxy'];
|
||||
$headers['CURLOPT_PROXYTYPE'] = str_contains($aiSetting['ai_proxy'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
|
||||
}
|
||||
$headers = array_merge($headers, $this->headers);
|
||||
|
||||
$url = $aiSetting['ai_api_url'] ?: 'https://api.openai.com/v1';
|
||||
$url = $url . ($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);
|
||||
}
|
||||
|
||||
/** ******************************************************************************************** */
|
||||
/** ******************************************************************************************** */
|
||||
/** ******************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 通过 openAI 语音转文字
|
||||
* @param string $filePath 语音文件路径
|
||||
* @param array $extParams 扩展参数
|
||||
* @param bool $noCache 是否禁用缓存
|
||||
* @return array
|
||||
*/
|
||||
public static function transcriptions($filePath, $extParams = [], $noCache = false)
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
return Base::retError("语音文件不存在");
|
||||
}
|
||||
$systemSetting = Base::setting('system');
|
||||
if ($systemSetting['voice2text'] !== 'open') {
|
||||
return Base::retError("语音转文字功能未开启");
|
||||
}
|
||||
|
||||
$cacheKey = "openAItranscriptions::" . md5($filePath . '_' . Base::array2json($extParams));
|
||||
if ($noCache) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addDays(), function () use ($extParams, $filePath) {
|
||||
$post = array_merge($extParams, [
|
||||
'file' => new \CURLFile($filePath),
|
||||
'model' => 'whisper-1',
|
||||
]);
|
||||
$header = [
|
||||
'Content-Type' => 'multipart/form-data',
|
||||
];
|
||||
|
||||
$ai = new self($post, $header);
|
||||
$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)
|
||||
{
|
||||
$systemSetting = Base::setting('system');
|
||||
if ($systemSetting['translation'] !== 'open') {
|
||||
return Base::retError("翻译功能未开启");
|
||||
}
|
||||
|
||||
$cacheKey = "openAItranslations::" . md5($text . '_' . $targetLanguage);
|
||||
if ($noCache) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addDays(7), function () use ($text, $targetLanguage) {
|
||||
$post = json_encode([
|
||||
"model" => "gpt-4.1-nano",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一名资深的专业翻译专家,专门从事项目任务管理系统的多语言本地化工作。
|
||||
|
||||
翻译任务:将提供的文本内容翻译为 {$targetLanguage}
|
||||
|
||||
专业要求:
|
||||
1. 术语一致性:确保项目管理、任务管理、团队协作等专业术语的准确翻译
|
||||
2. 上下文理解:根据项目管理场景选择最合适的表达方式
|
||||
3. 格式保持:严格保持原文的格式、结构、标点符号和排版
|
||||
4. 语言规范:使用目标语言的标准表达,符合该语言的语法和习惯
|
||||
5. 专业性:体现项目管理领域的专业水准和准确性
|
||||
6. 简洁性:避免冗余表达,保持语言简洁明了
|
||||
|
||||
注意事项:
|
||||
- 保留所有HTML标签、特殊符号、数字、日期格式
|
||||
- 对于专有名词(如软件名称、品牌名)保持原文
|
||||
- 确保翻译后的文本自然流畅,符合目标语言的表达习惯
|
||||
- 如遇到歧义表达,优先选择项目管理场景下的含义
|
||||
|
||||
请直接返回翻译结果,不要包含任何解释或标记。
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => "请将以下内容翻译为 {$targetLanguage}:\n\n{$text}"
|
||||
]
|
||||
],
|
||||
"temperature" => 0.2,
|
||||
"max_tokens" => max(1000, intval(mb_strlen($text) * 1.5))
|
||||
]);
|
||||
|
||||
$ai = new self($post);
|
||||
$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)
|
||||
{
|
||||
$cacheKey = "openAIGenerateTitle::" . md5($text);
|
||||
if ($noCache) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addHours(24), function () use ($text) {
|
||||
$post = json_encode([
|
||||
"model" => "gpt-4.1-nano",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一个专业的标题生成器,专门为项目任务管理系统的对话内容生成精准、简洁的标题。
|
||||
|
||||
要求:
|
||||
1. 标题要准确概括文本的核心内容和主要意图
|
||||
2. 标题长度控制在5-20个字符之间
|
||||
3. 语言简洁明了,避免冗余词汇
|
||||
4. 适合在项目管理场景中使用
|
||||
5. 不要包含引号或特殊符号
|
||||
6. 如果是技术讨论,突出技术要点
|
||||
7. 如果是项目管理内容,突出关键动作或目标
|
||||
8. 如果是需求讨论,突出需求的核心点
|
||||
|
||||
请直接返回标题,不要包含任何解释或其他内容。
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => "请为以下内容生成一个合适的标题:\n\n" . $text
|
||||
]
|
||||
],
|
||||
"temperature" => 0.3,
|
||||
"max_tokens" => 100
|
||||
]);
|
||||
|
||||
$ai = new self($post);
|
||||
$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)
|
||||
{
|
||||
$cacheKey = "openAIJokeAndSoup::" . md5(date('Y-m-d'));
|
||||
if ($noCache) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addHours(6), function () {
|
||||
$post = json_encode([
|
||||
"model" => "gpt-4.1-nano",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一个专业的内容生成器。
|
||||
|
||||
要求:
|
||||
1. 笑话要幽默风趣,适合职场环境,内容积极正面
|
||||
2. 心灵鸡汤要励志温暖,适合职场人士阅读
|
||||
3. 每个笑话和鸡汤都要简洁明了,尽量不超过100字
|
||||
4. 必须严格按照以下JSON格式返回,不要markdown格式,不要包含任何其他内容:
|
||||
|
||||
{
|
||||
"jokes": [
|
||||
"笑话内容1",
|
||||
"笑话内容2",
|
||||
...
|
||||
],
|
||||
"soups": [
|
||||
"心灵鸡汤内容1",
|
||||
"心灵鸡汤内容2",
|
||||
...
|
||||
]
|
||||
}
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => "请生成20个职场笑话和20个心灵鸡汤"
|
||||
]
|
||||
],
|
||||
"temperature" => 0.8
|
||||
]);
|
||||
|
||||
$ai = new self($post);
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ollama 模型
|
||||
* @param $baseUrl
|
||||
* @param $key
|
||||
* @param $agency
|
||||
* @return array
|
||||
*/
|
||||
public static function ollamaModels($baseUrl, $key = null, $agency = null)
|
||||
{
|
||||
$extra = [
|
||||
'Content-Type' => 'application/json',
|
||||
];
|
||||
if ($key) {
|
||||
$extra['Authorization'] = 'Bearer ' . $key;
|
||||
}
|
||||
if ($agency) {
|
||||
$extra['CURLOPT_PROXY'] = $agency;
|
||||
$extra['CURLOPT_PROXYTYPE'] = str_contains($agency, 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
|
||||
}
|
||||
$res = Ihttp::ihttp_request(rtrim($baseUrl, '/') . '/api/tags', [], $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("获取失败", $res);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['models'])) {
|
||||
return Base::retError("获取失败", $resData);
|
||||
}
|
||||
$models = [];
|
||||
foreach ($resData['models'] as $model) {
|
||||
if ($model['name'] !== $model['model']) {
|
||||
$models[] = "{$model['model']} | {$model['name']}";
|
||||
} else {
|
||||
$models[] = $model['model'];
|
||||
}
|
||||
}
|
||||
return Base::retSuccess("success", [
|
||||
'models' => $models,
|
||||
'original' => $resData['models']
|
||||
]);
|
||||
}
|
||||
}
|
||||
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 Robot',
|
||||
'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,8 @@ 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);
|
||||
}
|
||||
abort_if(Request::header('Content-Type') !== 'application/json', $abortCode, Doo::translate($msg));
|
||||
return Base::retError($msg, $data, $ret);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1476,14 +1480,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 +1855,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 +1886,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 +1908,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 +1935,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 +1947,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 +1992,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 +2029,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 +2122,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 +2136,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 +2194,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 +2205,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 +2260,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 +2588,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 +3045,27 @@ class Base
|
||||
*/
|
||||
public static function markdown2html($markdown)
|
||||
{
|
||||
$converter = new CommonMarkConverter();
|
||||
try {
|
||||
$converter = new CommonMarkConverter();
|
||||
return $converter->convert($markdown);
|
||||
} catch (CommonMarkException $e) {
|
||||
} catch (\League\CommonMark\Exception\CommonMarkException $e) {
|
||||
return $markdown;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* html 转 MD(markdown)
|
||||
* @param $html
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,10 @@ namespace App\Observers;
|
||||
|
||||
use App\Models\Deleted;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Tasks\ZincSearchSyncTask;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class WebSocketDialogUserObserver
|
||||
class WebSocketDialogUserObserver extends AbstractObserver
|
||||
{
|
||||
/**
|
||||
* Handle the WebSocketDialogUser "created" event.
|
||||
@@ -29,6 +30,7 @@ class WebSocketDialogUserObserver
|
||||
}
|
||||
}
|
||||
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||
self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +41,7 @@ class WebSocketDialogUserObserver
|
||||
*/
|
||||
public function updated(WebSocketDialogUser $webSocketDialogUser)
|
||||
{
|
||||
//
|
||||
self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +53,7 @@ class WebSocketDialogUserObserver
|
||||
public function deleted(WebSocketDialogUser $webSocketDialogUser)
|
||||
{
|
||||
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||
self::taskDeliver(new ZincSearchSyncTask('deleteUser', $webSocketDialogUser->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = $this->extractMessageContent($msg);
|
||||
$replyText = null;
|
||||
if ($msg->reply_id && $replyMsg = WebSocketDialogMsg::find($msg->reply_id)) {
|
||||
$replyText = $this->extractMessageContent($replyMsg);
|
||||
}
|
||||
|
||||
// 没有提取到指令,则不处理
|
||||
if (empty($sendText) && empty($replyText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,22 +134,22 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是群聊,未提及丹提及其他人
|
||||
if ($dialog->type === 'group' && !$this->mention && $this->mentionOther) {
|
||||
// 如果是群聊,@别人但是没有@自己,则不处理
|
||||
if ($dialog->type === 'group' && $this->mentionOther && !$this->mention) {
|
||||
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 +160,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 +171,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 +186,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
|
||||
// 指令处理
|
||||
$array = Base::newTrim(explode(" ", "{$command} "));
|
||||
$array = Base::newTrim(explode(" ", "{$sendText} "));
|
||||
$type = $array[0];
|
||||
$data = [];
|
||||
$content = "";
|
||||
@@ -185,7 +222,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 +232,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 +250,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 +267,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 +280,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 +293,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 +309,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 +330,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 +353,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 +365,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 +408,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
default => '不支持的指令',
|
||||
};
|
||||
if ($type == '/api') {
|
||||
$msgData['email'] = $botUser->email;
|
||||
$msgData['version'] = Base::getVersion();
|
||||
} elseif ($type == '/help') {
|
||||
$msgData['manager'] = $isManager;
|
||||
@@ -402,83 +419,136 @@ 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';
|
||||
$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 ($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";
|
||||
$webhookUrl = null;
|
||||
$extras = ['timestamp' => time()];
|
||||
|
||||
try {
|
||||
if ($botUser->isAiBot($type)) {
|
||||
// AI机器人
|
||||
if (Base::val($msg->msg, 'forward_data.leave')) {
|
||||
// AI机器人不处理带有留言的转发消息,因为他要处理那条留言消息
|
||||
return;
|
||||
}
|
||||
if (in_array($this->client['platform'], ['win', 'mac', 'web']) && !Base::judgeClientVersion("0.41.11", $this->client['version'])) {
|
||||
throw new Exception('当前客户端版本低(所需版本≥v0.41.11)。');
|
||||
}
|
||||
if (!Apps::isInstalled('ai')) {
|
||||
throw new Exception('应用「AI Robot」未安装');
|
||||
}
|
||||
// 整理机器人参数
|
||||
$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 ($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, $extras);
|
||||
// 转换提及格式
|
||||
$sendText = self::convertMentionForAI($sendText);
|
||||
$replyText = self::convertMentionForAI($replyText);
|
||||
if ($replyText) {
|
||||
$sendText = <<<EOF
|
||||
<quoted_content>
|
||||
{$replyText}
|
||||
</quoted_content>
|
||||
|
||||
The content within the above quoted_content tags is a citation.
|
||||
|
||||
{$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->webhook_num++;
|
||||
$userBot->save();
|
||||
$webhookUrl = $userBot->webhook_url;
|
||||
}
|
||||
}
|
||||
$this->AIGenerateSystemMessageOrBeforeText($msg->userid, $dialog, $extras);
|
||||
$webhookUrl = "{$serverUrl}/ai/chat";
|
||||
} else {
|
||||
// 用户机器人
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->first();
|
||||
$webhookUrl = $userBot?->webhook_url;
|
||||
}
|
||||
if ($errorContent) {
|
||||
if (!preg_match("/^https?:\/\//", $webhookUrl)) {
|
||||
return;
|
||||
}
|
||||
} 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,
|
||||
'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,
|
||||
@@ -488,14 +558,25 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
'version' => Base::getVersion(),
|
||||
'extras' => Base::array2json($extras)
|
||||
];
|
||||
$res = Ihttp::ihttp_post($webhookUrl, $data, 30);
|
||||
if ($userBot) {
|
||||
$userBot->webhook_num++;
|
||||
$userBot->save();
|
||||
// 添加用户信息
|
||||
$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()),
|
||||
];
|
||||
}
|
||||
if ($res['data'] && $data = Base::json2array($res['data'])) {
|
||||
// 请求Webhook
|
||||
$result = Ihttp::ihttp_post($webhookUrl, $data, 30);
|
||||
if ($result['data'] && $data = Base::json2array($result['data'])) {
|
||||
if ($data['code'] != 200 && $data['message']) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $res['data']['message']], $botUser->userid, false, false, true);
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', [
|
||||
'text' => $result['data']['message']
|
||||
], $botUser->userid, false, false, true);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
@@ -510,12 +591,302 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器人信息
|
||||
* @param $botId
|
||||
* @param $userid
|
||||
* @return User
|
||||
* 提取消息内容
|
||||
* 根据消息类型(文件、文本等)提取相应的内容文本
|
||||
*
|
||||
* @param WebSocketDialogMsg $msg 消息对象
|
||||
* @return string 提取出的消息文本内容
|
||||
*/
|
||||
private function botOne($botId, $userid)
|
||||
private function extractMessageContent(WebSocketDialogMsg $msg)
|
||||
{
|
||||
$reserves = [];
|
||||
switch ($msg->type) {
|
||||
case "file":
|
||||
// 提取文件消息
|
||||
$msgData = Base::json2array($msg->getRawOriginal('msg'));
|
||||
$result = $this->convertMentionFormat("path", $msgData['path'], $msgData['name'], $reserves);
|
||||
break;
|
||||
|
||||
case "text":
|
||||
// 提取文本消息
|
||||
$result = $msg->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 () {
|
||||
return "";
|
||||
},
|
||||
|
||||
// 任务
|
||||
"/<span class=\"mention task\" data-id=\"(\d+)\">#?(.*?)<\/span>/" => function ($match) use (&$reserves) {
|
||||
return $this->convertMentionFormat("task", $match[1], $match[2], $reserves);
|
||||
},
|
||||
|
||||
// 文件
|
||||
"/<a class=\"mention file\" href=\"([^\"']+?)\"[^>]*?>~?(.*?)<\/a>/" => function ($match) use (&$reserves) {
|
||||
if (preg_match("/single\/file\/(.*?)$/", $match[1], $subMatch)) {
|
||||
return $this->convertMentionFormat("file", $subMatch[1], $match[2], $reserves);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
// 报告
|
||||
"/<a class=\"mention report\" href=\"([^\"']+?)\"[^>]*?>%?(.*?)<\/a>/" => function ($match) use (&$reserves) {
|
||||
if (preg_match("/single\/report\/detail\/(.*?)$/", $match[1], $subMatch)) {
|
||||
return $this->convertMentionFormat("report", $subMatch[1], $match[2], $reserves);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
], $result);
|
||||
|
||||
// 转成 markdown
|
||||
if ($msg->msg['type'] !== 'md') {
|
||||
$result = Base::html2markdown($result);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// 其他类型消息不处理
|
||||
return '';
|
||||
}
|
||||
|
||||
// 处理 reserves
|
||||
foreach ($reserves as $rand => $mention) {
|
||||
$result = str_replace($rand, $mention, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换提及消息格式
|
||||
* 将提及的任务、文件、报告等转换为统一的格式 [type#key#name]
|
||||
*
|
||||
* @param string $type 提及类型(task、file、report、path)
|
||||
* @param string $key 提及对象的唯一标识
|
||||
* @param string $name 提及对象的显示名称
|
||||
* @return string 格式化后的提及字符串
|
||||
*/
|
||||
private function convertMentionFormat($type, $key, $name, &$reserves)
|
||||
{
|
||||
$key = str_replace(['#', '-->'], '', $key);
|
||||
$name = str_replace(['#', '-->'], '', $name);
|
||||
$rand = Base::generatePassword(12);
|
||||
$reserves[$rand] = "<!--{$type}#{$key}#{$name}-->";
|
||||
return $rand;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为AI机器人转换提及消息格式
|
||||
* 将提及的任务、文件、报告转换为AI可理解的格式,并提取相关内容
|
||||
*
|
||||
* @param string $original 原始消息文本
|
||||
* @return string 转换后的消息文本,包含相关内容的标签
|
||||
* @throws Exception 当提及的对象不存在或读取失败时抛出异常
|
||||
*/
|
||||
public static function convertMentionForAI($original)
|
||||
{
|
||||
$array = [];
|
||||
$original = preg_replace_callback('/<!--(.*?)#(.*?)#(.*?)-->/', function ($match) use (&$array) {
|
||||
// 初始化 tag 内容
|
||||
$pathTag = null;
|
||||
$pathName = null;
|
||||
$pathContent = null;
|
||||
|
||||
// 根据 type 提取 tag 内容
|
||||
switch ($match[1]) {
|
||||
// 任务
|
||||
case 'task':
|
||||
$taskInfo = ProjectTask::with(['content'])->whereId(intval($match[2]))->first();
|
||||
if (!$taskInfo) {
|
||||
throw new Exception("任务不存在或已被删除");
|
||||
}
|
||||
$pathTag = "task_content";
|
||||
$pathName = addslashes($taskInfo->name) . " (ID:{$taskInfo->id})";
|
||||
$pathContent = implode("\n", $taskInfo->AIContext());
|
||||
break;
|
||||
|
||||
// 文件
|
||||
case 'file':
|
||||
$fileInfo = FileContent::idOrCodeToContent($match[2]);
|
||||
if (!$fileInfo || !isset($fileInfo->content['url'])) {
|
||||
throw new Exception("文件不存在或已被删除");
|
||||
}
|
||||
$urlPath = public_path($fileInfo->content['url']);
|
||||
if (!file_exists($urlPath)) {
|
||||
throw new Exception("文件不存在或已被删除");
|
||||
}
|
||||
$fileResult = TextExtractor::extractFile($urlPath);
|
||||
if (Base::isError($fileResult)) {
|
||||
throw new Exception("文件读取失败:" . $fileResult['msg']);
|
||||
}
|
||||
$pathTag = "file_content";
|
||||
$pathName = addslashes($match[3]) . " (ID:{$fileInfo->id})";
|
||||
$pathContent = $fileResult['data'];
|
||||
break;
|
||||
|
||||
// 文件路径
|
||||
case 'path':
|
||||
$urlPath = public_path($match[2]);
|
||||
if (!file_exists($urlPath)) {
|
||||
throw new Exception("文件不存在或已被删除");
|
||||
}
|
||||
$fileResult = TextExtractor::extractFile($urlPath);
|
||||
if (Base::isError($fileResult)) {
|
||||
throw new Exception("文件读取失败:" . $fileResult['msg']);
|
||||
}
|
||||
$pathTag = "file_content";
|
||||
$pathName = addslashes($match[3]);
|
||||
$pathContent = $fileResult['data'];
|
||||
break;
|
||||
|
||||
// 报告
|
||||
case 'report':
|
||||
$reportInfo = Report::idOrCodeToContent($match[2]);
|
||||
if (!$reportInfo) {
|
||||
throw new Exception("报告不存在或已被删除");
|
||||
}
|
||||
$pathTag = "report_content";
|
||||
$pathName = addslashes($match[3]) . " (ID:{$reportInfo->id})";
|
||||
$pathContent = Base::html2markdown($reportInfo->content);
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果提取到 tag 内容,则添加到 contents 数组中
|
||||
if ($pathTag) {
|
||||
$array[] = "<{$pathTag} path=\"{$pathName}\">\n{$pathContent}\n</{$pathTag}>";
|
||||
return "`{$pathName}` (see below for {$pathTag} tag)";
|
||||
}
|
||||
|
||||
return "";
|
||||
}, $original);
|
||||
|
||||
// 添加 tag 内容
|
||||
if ($array) {
|
||||
$original .= "\n\n" . implode("\n\n", $array);
|
||||
}
|
||||
|
||||
return $original;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为AI机器人生成系统提示词
|
||||
* 根据对话类型(用户对话、项目群、任务群、部门群等)生成相应的系统提示词
|
||||
*
|
||||
* @param int|null $userid 用户ID
|
||||
* @param WebSocketDialog $dialog 对话对象
|
||||
* @param array $extras 额外参数数组,通过引用传递以修改system_message
|
||||
* @return void
|
||||
*/
|
||||
private function generateSystemPromptForAI($userid, WebSocketDialog $dialog, array &$extras)
|
||||
{
|
||||
$system_messages = [];
|
||||
switch ($dialog->type) {
|
||||
// 用户对话
|
||||
case "user":
|
||||
$aiPrompt = WebSocketDialogConfig::where([
|
||||
'dialog_id' => $dialog->id,
|
||||
'userid' => $userid,
|
||||
'type' => 'ai_prompt',
|
||||
])->value('value');
|
||||
if ($aiPrompt) {
|
||||
$extras['system_message'] = $aiPrompt;
|
||||
}
|
||||
break;
|
||||
// 群组对话
|
||||
case "group":
|
||||
switch ($dialog->group_type) {
|
||||
// 用户群
|
||||
case 'user':
|
||||
break;
|
||||
// 项目群
|
||||
case 'project':
|
||||
$projectInfo = Project::whereDialogId($dialog->id)->first();
|
||||
if ($projectInfo) {
|
||||
$projectDesc = $projectInfo->desc ?: "-";
|
||||
$projectStatus = $projectInfo->archived_at ? '已归档' : '正在进行中';
|
||||
$system_messages[] = <<<EOF
|
||||
当前我在项目【{$projectInfo->name}】中
|
||||
项目描述:{$projectDesc}
|
||||
项目状态:{$projectStatus}
|
||||
|
||||
如果你判断我想要或需要添加任务,请按照以下格式回复:
|
||||
|
||||
::: create-task-list
|
||||
title: 任务标题1
|
||||
desc: 任务描述1
|
||||
|
||||
title: 任务标题2
|
||||
desc: 任务描述2
|
||||
:::
|
||||
EOF;
|
||||
}
|
||||
break;
|
||||
// 任务群
|
||||
case 'task':
|
||||
$taskInfo = ProjectTask::with(['content'])->whereDialogId($dialog->id)->first();
|
||||
if ($taskInfo) {
|
||||
$taskContext = implode("\n", $taskInfo->AIContext());
|
||||
$system_messages[] = <<<EOF
|
||||
当前我在任务【{$taskInfo->name}】中
|
||||
当前时间:{$taskInfo->updated_at}
|
||||
任务ID:{$taskInfo->id}
|
||||
{$taskContext}
|
||||
|
||||
如果你判断我想要或需要添加子任务,请按照以下格式回复:
|
||||
|
||||
::: create-subtask-list
|
||||
title: 子任务标题1
|
||||
title: 子任务标题2
|
||||
:::
|
||||
EOF;
|
||||
}
|
||||
break;
|
||||
// 部门群
|
||||
case 'department':
|
||||
$userDepartment = UserDepartment::whereDialogId($dialog->id)->first();
|
||||
if ($userDepartment) {
|
||||
$system_messages[] = "当前我在【{$userDepartment->name}】的部门群聊中";
|
||||
}
|
||||
break;
|
||||
// 全体成员群
|
||||
case 'all':
|
||||
$system_messages[] = "当前我在【全体成员】的群聊中";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($extras['system_message']) {
|
||||
array_unshift($system_messages, $extras['system_message']);
|
||||
}
|
||||
if ($system_messages) {
|
||||
$extras['system_message'] = implode("\n\n----------------\n\n", Base::newTrim($system_messages));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器人信息
|
||||
* 根据机器人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);
|
||||
@@ -535,150 +906,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,7 @@ namespace App\Tasks;
|
||||
use App\Models\File;
|
||||
use App\Models\TaskWorker;
|
||||
use App\Models\Tmp;
|
||||
use App\Models\UserDevice;
|
||||
use App\Models\WebSocketTmpMsg;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
@@ -33,89 +34,75 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
|
||||
6
bin/version.js
vendored
6
bin/version.js
vendored
File diff suppressed because one or more lines are too long
@@ -11,6 +11,8 @@
|
||||
"php": "^8.0",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-ffi": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-imagick": "*",
|
||||
"ext-json": "*",
|
||||
@@ -32,10 +34,14 @@
|
||||
"league/html-to-markdown": "^5.1",
|
||||
"maatwebsite/excel": "^3.1.31",
|
||||
"madnest/madzipper": "^v1.1.0",
|
||||
"matomo/device-detector": "^6.4",
|
||||
"mews/captcha": "^3.2.6",
|
||||
"orangehill/iseed": "^3.0.1",
|
||||
"overtrue/pinyin": "^4.0",
|
||||
"phpoffice/phppresentation": "^1.1",
|
||||
"phpoffice/phpword": "^1.3",
|
||||
"predis/predis": "^1.1.7",
|
||||
"smalot/pdfparser": "^2.11",
|
||||
"symfony/mailer": "^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -84,7 +90,10 @@
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
|
||||
1152
composer.lock
generated
1152
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ class AddProjectsPersonal extends Migration
|
||||
});
|
||||
if ($isAdd) {
|
||||
// 更新数据
|
||||
\App\Models\Project::whereName('个人项目')->chunkById(100, function ($lists) {
|
||||
\App\Models\Project::where('name','like', '%个人项目%')->chunkById(100, function ($lists) {
|
||||
/** @var \App\Models\Project $item */
|
||||
foreach ($lists as $item) {
|
||||
if ($item->desc == '注册时系统自动创建项目,你可以自由删除。') {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Module\Base;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class UpdateAiModelsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$row = Setting::whereName('aibotSetting')->first();
|
||||
if (empty($row)) {
|
||||
return;
|
||||
}
|
||||
$value = Base::json2array($row->getRawOriginal('setting'));
|
||||
foreach ($value as $key => $item) {
|
||||
if (str_ends_with($key, '_models')) {
|
||||
$value[$key] = preg_replace('/\s*:\s*/', ' | ', $item);
|
||||
}
|
||||
}
|
||||
$row->setting = Base::array2json($value);
|
||||
$row->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
$row = Setting::whereName('aibotSetting')->first();
|
||||
if (empty($row)) {
|
||||
return;
|
||||
}
|
||||
$value = Base::json2array($row->getRawOriginal('setting'));
|
||||
foreach ($value as $key => $item) {
|
||||
if (str_ends_with($key, '_models')) {
|
||||
$value[$key] = preg_replace('/\s*\|\s*/', ': ', $item);
|
||||
}
|
||||
}
|
||||
$row->setting = Base::array2json($value);
|
||||
$row->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogSession;
|
||||
use App\Module\Base;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class UpdateWebSocketDialogMsgsSessionId extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.email'])
|
||||
->join('web_socket_dialog_users as du', 'web_socket_dialogs.id', '=', 'du.dialog_id')
|
||||
->join('users as u', 'du.userid', '=', 'u.userid')
|
||||
->where('u.email', 'like', 'ai-%@bot.system')
|
||||
->where('web_socket_dialogs.type', 'user')
|
||||
->get();
|
||||
foreach ($list as $item) {
|
||||
$msg = WebSocketDialogMsg::whereDialogId($item->id)->whereSessionId(0)->orderBy('id')->first();
|
||||
if ($msg || empty($item->session_id)) {
|
||||
$title = $msg?->key;
|
||||
$session = WebSocketDialogSession::createInstance([
|
||||
'dialog_id' => $item->id,
|
||||
'title' => $title ? Base::cutStr($title, 100) : 'Unknown',
|
||||
'created_at' => $item->created_at,
|
||||
]);
|
||||
$session->save();
|
||||
if (empty($item->session_id)) {
|
||||
$item->session_id = $session->id;
|
||||
$item->save();
|
||||
}
|
||||
if ($msg) {
|
||||
WebSocketDialogMsg::whereDialogId($item->id)->whereSessionId(0)->update(['session_id' => $session->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateReportLinksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('report_links', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('rid')->nullable()->default(0)->index()->comment('报告ID');
|
||||
$table->integer('num')->nullable()->default(0)->comment('累计访问');
|
||||
$table->string('code')->nullable()->default('')->comment('链接码');
|
||||
$table->bigInteger('userid')->nullable()->default(0)->index()->comment('会员ID');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('report_links');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateUserDevicesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (Schema::hasTable('user_devices'))
|
||||
return;
|
||||
|
||||
Schema::create('user_devices', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('userid')->index()->nullable()->default(0)->comment('会员ID');
|
||||
$table->string('hash')->index()->nullable()->default('')->comment('TOKEN MD5');
|
||||
$table->longText('detail')->nullable()->comment('详细信息');
|
||||
$table->timestamp('expired_at')->nullable()->comment('过期时间');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('user_devices');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddDeviceHashToUmengAliasTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('umeng_alias', 'device_hash')) {
|
||||
$table->string('device_hash')->index()->nullable()->after('device')->comment('设备哈希值,用于关联UserDevice表');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
$table->dropColumn('device_hash');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Module\Base;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateAiSettingsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$setting = Base::setting('aibotSetting');
|
||||
Base::setting('aiSetting', [
|
||||
'ai_provider' => 'openai',
|
||||
'ai_api_key' => $setting['openai_key'],
|
||||
'ai_api_url' => $setting['openai_base_url'],
|
||||
'ai_proxy' => $setting['openai_agency'],
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// This migration does not need to be reversible
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddColorToProjectFlowItemsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('project_flow_items', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('project_flow_items', 'color')) {
|
||||
$table->string('color', 20)->nullable()->default('')->after('status')->comment('自定义颜色');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('project_flow_items', function (Blueprint $table) {
|
||||
$table->dropColumn('color');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class UpdateFilesNameLengthTo200 extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
$table->string('name', 255)->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
services:
|
||||
php:
|
||||
container_name: "dootask-php-${APP_ID}"
|
||||
image: "kuaifan/php:swoole-8.0.rc18"
|
||||
shm_size: "2gb"
|
||||
image: "kuaifan/php:swoole-8.0.rc21"
|
||||
shm_size: 2G
|
||||
ulimits:
|
||||
core:
|
||||
soft: 0
|
||||
hard: 0
|
||||
volumes:
|
||||
- shared_data:/usr/share/dootask
|
||||
- ./docker/crontab/crontab.conf:/etc/supervisor/conf.d/crontab.conf
|
||||
- ./docker/php/php.conf:/etc/supervisor/conf.d/php.conf
|
||||
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
|
||||
- ./docker/log/supervisor:/var/log/supervisor
|
||||
- ./docker/logs/supervisor:/var/log/supervisor
|
||||
- ./:/var/www
|
||||
environment:
|
||||
LANG: "C.UTF-8"
|
||||
@@ -21,12 +22,18 @@ services:
|
||||
MYSQL_DB_NAME: "${DB_DATABASE}"
|
||||
MYSQL_USERNAME: "${DB_USERNAME}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:${LARAVELS_LISTEN_PORT}/health"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.2"
|
||||
- extnetwork
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
@@ -34,30 +41,34 @@ services:
|
||||
image: "nginx:alpine"
|
||||
ports:
|
||||
- "${APP_PORT}:80"
|
||||
- "${APP_SSL_PORT:-}:443"
|
||||
- "${APP_SSL_PORT:-0}:443"
|
||||
volumes:
|
||||
- ./docker/nginx:/etc/nginx/conf.d
|
||||
- ./public:/var/www/public
|
||||
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./:/var/www
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
depends_on:
|
||||
php:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.3"
|
||||
links:
|
||||
- php
|
||||
- office
|
||||
- fileview
|
||||
- drawio-webapp
|
||||
- drawio-export
|
||||
- minder
|
||||
- okr
|
||||
- ai
|
||||
- extnetwork
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
container_name: "dootask-redis-${APP_ID}"
|
||||
image: "redis:alpine"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.4"
|
||||
- extnetwork
|
||||
restart: unless-stopped
|
||||
|
||||
mariadb:
|
||||
@@ -73,159 +84,40 @@ services:
|
||||
MYSQL_DATABASE: "${DB_DATABASE}"
|
||||
MYSQL_USER: "${DB_USERNAME}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${DB_USERNAME}", "-p${DB_PASSWORD}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.5"
|
||||
- extnetwork
|
||||
restart: unless-stopped
|
||||
|
||||
office:
|
||||
container_name: "dootask-office-${APP_ID}"
|
||||
image: "onlyoffice/documentserver:8.2.2.1"
|
||||
appstore:
|
||||
container_name: "dootask-appstore-${APP_ID}"
|
||||
privileged: true
|
||||
image: "dootask/appstore:0.2.5"
|
||||
volumes:
|
||||
- ./docker/office/logs:/var/log/onlyoffice
|
||||
- ./docker/office/data:/var/www/onlyoffice/Data
|
||||
- ./docker/office/resources/require.js:/var/www/onlyoffice/documentserver/web-apps/vendor/requirejs/require.js
|
||||
- ./docker/office/resources/common/main/resources/img/header:/var/www/onlyoffice/documentserver/web-apps/apps/common/main/resources/img/header
|
||||
- ./docker/office/resources/documenteditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/main/resources/css/app.css
|
||||
- ./docker/office/resources/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css
|
||||
- ./docker/office/resources/presentationeditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/main/resources/css/app.css
|
||||
- ./docker/office/resources/presentationeditor/mobile/css/923.f9cf19de1a25c2e7bf8b.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/mobile/css/923.f9cf19de1a25c2e7bf8b.css
|
||||
- ./docker/office/resources/spreadsheeteditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/spreadsheeteditor/main/resources/css/app.css
|
||||
- ./docker/office/resources/spreadsheeteditor/mobile/css/611.1bef49f175e18fc085db.css:/var/www/onlyoffice/documentserver/web-apps/apps/spreadsheeteditor/mobile/css/611.1bef49f175e18fc085db.css
|
||||
- shared_data:/usr/share/dootask
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./:/var/www
|
||||
environment:
|
||||
JWT_SECRET: ${APP_KEY}
|
||||
HOST_PWD: "${PWD}"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.6"
|
||||
restart: unless-stopped
|
||||
|
||||
fileview:
|
||||
container_name: "dootask-fileview-${APP_ID}"
|
||||
image: "kuaifan/fileview:4.2.0-SNAPSHOT-RC25"
|
||||
environment:
|
||||
KK_CONTEXT_PATH: "/fileview"
|
||||
KK_OFFICE_PREVIEW_SWITCH_DISABLED: true
|
||||
KK_FILE_UPLOAD_ENABLED: true
|
||||
KK_MEDIA: "mp3,wav,mp4,mov,avi,wmv"
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.7"
|
||||
restart: unless-stopped
|
||||
|
||||
drawio-webapp:
|
||||
container_name: "dootask-drawio-webapp-${APP_ID}"
|
||||
image: "jgraph/drawio:24.7.17"
|
||||
volumes:
|
||||
- ./docker/drawio/webapp/index.html:/usr/local/tomcat/webapps/draw/index.html
|
||||
- ./docker/drawio/webapp/stencils:/usr/local/tomcat/webapps/draw/stencils
|
||||
- ./docker/drawio/webapp/js/app.min.js:/usr/local/tomcat/webapps/draw/js/app.min.js
|
||||
- ./docker/drawio/webapp/js/croppie/croppie.min.css:/usr/local/tomcat/webapps/draw/js/croppie/croppie.min.css
|
||||
- ./docker/drawio/webapp/js/diagramly/ElectronApp.js:/usr/local/tomcat/webapps/draw/js/diagramly/ElectronApp.js
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.8"
|
||||
depends_on:
|
||||
- drawio-export
|
||||
restart: unless-stopped
|
||||
|
||||
drawio-export:
|
||||
container_name: "dootask-drawio-export-${APP_ID}"
|
||||
image: "kuaifan/export-server:0.0.1"
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.9"
|
||||
volumes:
|
||||
- ./docker/drawio/export/fonts:/usr/share/fonts/drawio
|
||||
restart: unless-stopped
|
||||
|
||||
minder:
|
||||
container_name: "dootask-minder-${APP_ID}"
|
||||
image: "kuaifan/minder:0.1.3"
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.10"
|
||||
restart: unless-stopped
|
||||
|
||||
approve:
|
||||
container_name: "dootask-approve-${APP_ID}"
|
||||
image: "kuaifan/dooapprove:0.1.5"
|
||||
environment:
|
||||
TZ: "${TIMEZONE:-PRC}"
|
||||
MYSQL_HOST: "${DB_HOST}"
|
||||
MYSQL_PORT: "${DB_PORT}"
|
||||
MYSQL_DBNAME: "${DB_DATABASE}"
|
||||
MYSQL_USERNAME: "${DB_USERNAME}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
MYSQL_Prefix: "${DB_PREFIX}approve_"
|
||||
DEMO_DATA: true
|
||||
KEY: ${APP_KEY}
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.11"
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: unless-stopped
|
||||
|
||||
ai:
|
||||
container_name: "dootask-ai-${APP_ID}"
|
||||
image: "kuaifan/dootask-ai:0.3.1"
|
||||
environment:
|
||||
REDIS_HOST: "${REDIS_HOST}"
|
||||
REDIS_PORT: "${REDIS_PORT}"
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.12"
|
||||
depends_on:
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
|
||||
okr:
|
||||
container_name: "dootask-okr-${APP_ID}"
|
||||
image: "kuaifan/doookr:0.4.5"
|
||||
environment:
|
||||
TZ: "${TIMEZONE:-PRC}"
|
||||
DOO_TASK_URL: "http://${APP_IPPR}.3"
|
||||
MYSQL_HOST: "${DB_HOST}"
|
||||
MYSQL_PORT: "${DB_PORT}"
|
||||
MYSQL_DBNAME: "${DB_DATABASE}"
|
||||
MYSQL_USERNAME: "${DB_USERNAME}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
MYSQL_PREFIX: "${DB_PREFIX}"
|
||||
DEMO_DATA: true
|
||||
KEY: ${APP_KEY}
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.13"
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: unless-stopped
|
||||
|
||||
face:
|
||||
container_name: "dootask-face-${APP_ID}"
|
||||
image: "hitosea2020/dooface:0.0.1"
|
||||
ports:
|
||||
- "7788:7788"
|
||||
environment:
|
||||
TZ: "${TIMEZONE:-PRC}"
|
||||
STORAGE: mysql
|
||||
MYSQL_HOST: "${DB_HOST}"
|
||||
MYSQL_PORT: "${DB_PORT}"
|
||||
MYSQL_USERNAME: "${DB_USERNAME}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
MYSQL_DB_NAME: "${DB_DATABASE}"
|
||||
DB_PREFIX: "${DB_PREFIX}"
|
||||
REPORT_API: "http://${APP_IPPR}.3:80/api/public/checkin/report"
|
||||
depends_on:
|
||||
- mariadb
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.14"
|
||||
- extnetwork
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
extnetwork:
|
||||
name: "dootask-networks-${APP_ID}"
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "${APP_IPPR}.0/24"
|
||||
gateway: "${APP_IPPR}.1"
|
||||
|
||||
volumes:
|
||||
shared_data:
|
||||
name: "dootask-shared-data-${APP_ID}"
|
||||
redis_data:
|
||||
name: "dootask-redis-data-${APP_ID}"
|
||||
|
||||
5
docker/appstore/.gitignore
vendored
Normal file
5
docker/appstore/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
apps/*
|
||||
config/*
|
||||
log/*
|
||||
temp/*
|
||||
!.gitkeep
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user