Compare commits
3142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfa749f4f3 | ||
|
|
eeaff08673 | ||
|
|
0475e88dc2 | ||
|
|
e1f73a4639 | ||
|
|
e2296a6f64 | ||
|
|
1a6abf4e1b | ||
|
|
315851eb5f | ||
|
|
0b99b4a9a0 | ||
|
|
e8235dd0a2 | ||
|
|
123c74de46 | ||
|
|
9419ddd174 | ||
|
|
0666a8f5c2 | ||
|
|
81c019105c | ||
|
|
6584259454 | ||
|
|
03d0f56095 | ||
|
|
406f64a7c5 | ||
|
|
1353a2c4c9 | ||
|
|
fb88f3bd96 | ||
|
|
22b3598704 | ||
|
|
b62c580d5e | ||
|
|
6a63ceaecc | ||
|
|
591f9e61fb | ||
|
|
7011c81bcd | ||
|
|
3cf7055122 | ||
|
|
aba31eda83 | ||
|
|
1b30582dd9 | ||
|
|
0fb66358cc | ||
|
|
e226f444f7 | ||
|
|
95bf70f568 | ||
|
|
a6597b44c3 | ||
|
|
51c01c5445 | ||
|
|
161bf75a1d | ||
|
|
2f16e2c608 | ||
|
|
aea2e79b37 | ||
|
|
f433d13a2f | ||
|
|
e9abf6ed05 | ||
|
|
0c32b25ddf | ||
|
|
a03dec91c5 | ||
|
|
7c5a966944 | ||
|
|
652dc0953b | ||
|
|
03860a6dce | ||
|
|
c6bee25264 | ||
|
|
068de0fa9f | ||
|
|
4b45d5ca26 | ||
|
|
a268391e68 | ||
|
|
89bdd86f14 | ||
|
|
e533bd7e35 | ||
|
|
09ed978e80 | ||
|
|
4b106e1f41 | ||
|
|
feeeb26d94 | ||
|
|
bef0d2d992 | ||
|
|
6e6bd8a6be | ||
|
|
631fa0db4e | ||
|
|
65d30b7a30 | ||
|
|
5ba5f27ca7 | ||
|
|
acc437bf2d | ||
|
|
5fd2505a33 | ||
|
|
7f6abc331b | ||
|
|
c190aab8b9 | ||
|
|
0f71abdac3 | ||
|
|
8ddc507bd5 | ||
|
|
1c4bae2d91 | ||
|
|
73ca4b1ea5 | ||
|
|
18a922b5cd | ||
|
|
11b98978c1 | ||
|
|
379d3811a8 | ||
|
|
0401b8a6e6 | ||
|
|
6148b996d8 | ||
|
|
39781c9cd7 | ||
|
|
18758a1614 | ||
|
|
b044d8d90e | ||
|
|
02e56f87bc | ||
|
|
d9b9ee221b | ||
|
|
21ec9188ca | ||
|
|
4d768becf5 | ||
|
|
a27049386b | ||
|
|
b23e3d7359 | ||
|
|
7660164583 | ||
|
|
5e1f3c5564 | ||
|
|
197fa9c01c | ||
|
|
554e3d0c2f | ||
|
|
b800cde34d | ||
|
|
775fdd2be0 | ||
|
|
7908ae4258 | ||
|
|
bfbd8229a1 | ||
|
|
afbf8dedbf | ||
|
|
569912abef | ||
|
|
7c94f6bc9a | ||
|
|
b825b5b063 | ||
|
|
50098b5e70 | ||
|
|
e237b4db1c | ||
|
|
2a25cf3bbd | ||
|
|
02275bb417 | ||
|
|
788cae3efe | ||
|
|
0dec70c53a | ||
|
|
f534f012d2 | ||
|
|
bb83875c99 | ||
|
|
d048aa33f7 | ||
|
|
8f3e250073 | ||
|
|
63a792d169 | ||
|
|
eb3524a22d | ||
|
|
f657a24a1a | ||
|
|
a5228448d7 | ||
|
|
1ec4796f72 | ||
|
|
6964158cf6 | ||
|
|
4fc4dd1b16 | ||
|
|
3e851f0c3c | ||
|
|
b8befaa973 | ||
|
|
b05046af29 | ||
|
|
eecc6c9e53 | ||
|
|
d4e754d601 | ||
|
|
a8a54593e2 | ||
|
|
5bbffc4f5c | ||
|
|
0833018399 | ||
|
|
f6850fc795 | ||
|
|
c0b4674568 | ||
|
|
5a8996d90a | ||
|
|
548b30e5b3 | ||
|
|
80f9329004 | ||
|
|
f672280236 | ||
|
|
90a4a01de7 | ||
|
|
09cebb90fe | ||
|
|
70389aab3d | ||
|
|
d9132a722f | ||
|
|
ea7a4e46e0 | ||
|
|
07b91058af | ||
|
|
c27ace6a6a | ||
|
|
1c0a5b17ca | ||
|
|
9b12a829d2 | ||
|
|
0f41172468 | ||
|
|
8597705a77 | ||
|
|
3f733ce857 | ||
|
|
40f8ec77b8 | ||
|
|
0af967d6c9 | ||
|
|
f6d43c9f39 | ||
|
|
70b0538dd5 | ||
|
|
439262b930 | ||
|
|
968b2587ae | ||
|
|
15f471a032 | ||
|
|
5175157ba6 | ||
|
|
e51e8f7196 | ||
|
|
00b34fda42 | ||
|
|
b34fabab54 | ||
|
|
487c7e2824 | ||
|
|
46c79a8772 | ||
|
|
bfb4144e57 | ||
|
|
dc1bb72070 | ||
|
|
8e084d2362 | ||
|
|
d5a75f887d | ||
|
|
710609e98b | ||
|
|
b73ab76bfb | ||
|
|
27b64df870 | ||
|
|
eabb897f96 | ||
|
|
68c5e47bad | ||
|
|
2ae5af7019 | ||
|
|
860d1ca9b3 | ||
|
|
66a9d1f25e | ||
|
|
bbfeedcdb3 | ||
|
|
079e273edb | ||
|
|
393aab4c4b | ||
|
|
4f2bf7549c | ||
|
|
acdf23571c | ||
|
|
62ec634db3 | ||
|
|
c53e978106 | ||
|
|
a7fa757d0d | ||
|
|
5fb1bd4175 | ||
|
|
e792ab7b4d | ||
|
|
02544d29fd | ||
|
|
20acbd0331 | ||
|
|
115b4aacb8 | ||
|
|
8746caab06 | ||
|
|
625648c908 | ||
|
|
734b5f9534 | ||
|
|
a0579318bd | ||
|
|
a437e3cbd3 | ||
|
|
1b242dc04e | ||
|
|
a1a51914a2 | ||
|
|
f6cab9b5a9 | ||
|
|
a3649c04e2 | ||
|
|
a562bfdb08 | ||
|
|
8ffe64ad8e | ||
|
|
a116d06d61 | ||
|
|
c26f73a5a8 | ||
|
|
f5847a57c1 | ||
|
|
fe9d23a0ff | ||
|
|
cdc27004bf | ||
|
|
b914164a77 | ||
|
|
35e58f90bc | ||
|
|
16d360c582 | ||
|
|
4c075b4d11 | ||
|
|
8c9c1c5afa | ||
|
|
d093163cd4 | ||
|
|
9bd6fcefd3 | ||
|
|
5139947643 | ||
|
|
01ff10385a | ||
|
|
9969c3a7ac | ||
|
|
f7ed2ec3e3 | ||
|
|
fedeeb3076 | ||
|
|
8157c27529 | ||
|
|
0eba0c6a4b | ||
|
|
13fb9db52b | ||
|
|
f6818ba880 | ||
|
|
dbf3b3cc79 | ||
|
|
24534069da | ||
|
|
4cec0a7350 | ||
|
|
0b86fa7bee | ||
|
|
b406e22695 | ||
|
|
3fca783dd8 | ||
|
|
6de4865052 | ||
|
|
facc2fab24 | ||
|
|
ddc0931e90 | ||
|
|
d5d32038f5 | ||
|
|
a20edd9bec | ||
|
|
3da90337ef | ||
|
|
9633f7644e | ||
|
|
a19cf0e1c3 | ||
|
|
1a841c4b5d | ||
|
|
937e7ba154 | ||
|
|
4cc0c85a6c | ||
|
|
943941e0f6 | ||
|
|
b160021e67 | ||
|
|
1bcc035979 | ||
|
|
ef67dc144f | ||
|
|
cc96fcd6a0 | ||
|
|
d1f00b2d48 | ||
|
|
2fe28d2335 | ||
|
|
f9276f4d83 | ||
|
|
099004a080 | ||
|
|
cc1df8d7d0 | ||
|
|
686a2e4fff | ||
|
|
e98fe3eec5 | ||
|
|
fc34ff38d3 | ||
|
|
b6b44b3782 | ||
|
|
c906636776 | ||
|
|
db282d1a04 | ||
|
|
898656963d | ||
|
|
6426e0238a | ||
|
|
08e8faf3ff | ||
|
|
d21adf6004 | ||
|
|
c3ac7dd1ab | ||
|
|
f1cfba3ad8 | ||
|
|
1ceed3461c | ||
|
|
d25c26156d | ||
|
|
d75c22114c | ||
|
|
05a754f446 | ||
|
|
b5e6eff65d | ||
|
|
eaa8ae66db | ||
|
|
5e4a08538b | ||
|
|
b01a54437a | ||
|
|
5f0fc78f30 | ||
|
|
325dc5e2fe | ||
|
|
a15b29122e | ||
|
|
074ccc8aab | ||
|
|
3809046fbc | ||
|
|
83ceb3264f | ||
|
|
9055858d55 | ||
|
|
2a465b5f1d | ||
|
|
b4101f856a | ||
|
|
44baa743c0 | ||
|
|
46dd449b2f | ||
|
|
f21d45e697 | ||
|
|
1e0a19ea7a | ||
|
|
dcfa47291e | ||
|
|
bd32c9555e | ||
|
|
f8f612544e | ||
|
|
1c0271f55e | ||
|
|
a10bc74de1 | ||
|
|
958ef80602 | ||
|
|
124b63f325 | ||
|
|
40f5ba5004 | ||
|
|
32f30826b9 | ||
|
|
b4aa8b37ea | ||
|
|
8368bbec47 | ||
|
|
618e482507 | ||
|
|
43711a1a59 | ||
|
|
bbe071545d | ||
|
|
4710479b46 | ||
|
|
c28a375b5d | ||
|
|
750d3429e0 | ||
|
|
4c34fe9b85 | ||
|
|
34c56980d4 | ||
|
|
a5b8609df1 | ||
|
|
1fd7f0314a | ||
|
|
25e82d690e | ||
|
|
31879cb60b | ||
|
|
489e5b551c | ||
|
|
e29bd01f68 | ||
|
|
e5d9140aa0 | ||
|
|
09f5cca948 | ||
|
|
405e09fdf4 | ||
|
|
43624e9b7b | ||
|
|
29ed0ad05a | ||
|
|
0aafe79c65 | ||
|
|
802afd592c | ||
|
|
ac1644e32d | ||
|
|
5a1f130bec | ||
|
|
678868153a | ||
|
|
991f050dbb | ||
|
|
be04355685 | ||
|
|
762b6b3f3b | ||
|
|
71d9cbdce6 | ||
|
|
d995ef19b5 | ||
|
|
bf80e4b02b | ||
|
|
64a047cd7c | ||
|
|
566421003c | ||
|
|
198da7608d | ||
|
|
0a7edb219e | ||
|
|
65c20f2211 | ||
|
|
144767152a | ||
|
|
82be52be52 | ||
|
|
46c8caa627 | ||
|
|
0027e838a0 | ||
|
|
7e846e2a58 | ||
|
|
de7f55bb97 | ||
|
|
2ac9a59469 | ||
|
|
bdb4014b94 | ||
|
|
9509bd1510 | ||
|
|
3f11770baa | ||
|
|
259a040b7e | ||
|
|
ccb14630f7 | ||
|
|
4aa865a60f | ||
|
|
c7ea7b057c | ||
|
|
91dbbd46c0 | ||
|
|
e5146077eb | ||
|
|
76918bf973 | ||
|
|
b7d3e69f87 | ||
|
|
d7bccfd267 | ||
|
|
2a25917e41 | ||
|
|
d73239b274 | ||
|
|
9f6fffbe6b | ||
|
|
72f7ff3df5 | ||
|
|
2af1dba8dc | ||
|
|
ca353d747b | ||
|
|
1589d4df1c | ||
|
|
b3abe8af9c | ||
|
|
c178e36f9b | ||
|
|
d93092de99 | ||
|
|
790b05880a | ||
|
|
f34766ade0 | ||
|
|
0e1d5e802c | ||
|
|
db526dfcc8 | ||
|
|
c18db60e80 | ||
|
|
b579a6ade2 | ||
|
|
9d1d642734 | ||
|
|
261c051052 | ||
|
|
e499e2d0dc | ||
|
|
b860b6f389 | ||
|
|
05d5d5a967 | ||
|
|
74ba1cc723 | ||
|
|
f2042efdc2 | ||
|
|
6b7e7fa1e4 | ||
|
|
6677e6e74f | ||
|
|
c3994ddbea | ||
|
|
a981ff2f6c | ||
|
|
3e0ba398d4 | ||
|
|
aa4f7c8536 | ||
|
|
959f9454d8 | ||
|
|
6b72a309f5 | ||
|
|
c388fe373d | ||
|
|
270ddc6487 | ||
|
|
5ccaa8f106 | ||
|
|
1d92c2668d | ||
|
|
6e03a05e6d | ||
|
|
2905059947 | ||
|
|
1df927f771 | ||
|
|
bd8b6d0319 | ||
|
|
fef39b2720 | ||
|
|
7de433c5fc | ||
|
|
0a711f2656 | ||
|
|
04b6b8aa8a | ||
|
|
58f286efe4 | ||
|
|
a01b292eb3 | ||
|
|
18c608ad7e | ||
|
|
8d144f4e12 | ||
|
|
7f895bfbec | ||
|
|
b0c356fa9b | ||
|
|
79defdc3f3 | ||
|
|
b2d9568deb | ||
|
|
a130c049bf | ||
|
|
8904039515 | ||
|
|
7d28181b16 | ||
|
|
98e4c81b9b | ||
|
|
10f5af5f09 | ||
|
|
18ffad5de5 | ||
|
|
428db42140 | ||
|
|
6e5426764e | ||
|
|
f3cfcc650c | ||
|
|
fc88573c9d | ||
|
|
5cf1c3d14f | ||
|
|
79f15cc34d | ||
|
|
775cab1080 | ||
|
|
3e20e7d0ce | ||
|
|
54407e0a60 | ||
|
|
ef696391d8 | ||
|
|
0c34df290e | ||
|
|
04d31bd814 | ||
|
|
9888d9f59e | ||
|
|
3bb1bf0967 | ||
|
|
dfbcb1f45c | ||
|
|
12ecf4de40 | ||
|
|
7be1171004 | ||
|
|
2bb646d150 | ||
|
|
e7749b2dff | ||
|
|
434d8eabc8 | ||
|
|
0a14219112 | ||
|
|
5b811df8ee | ||
|
|
bc264109f3 | ||
|
|
9c29c1ca9b | ||
|
|
fe4f62ff8d | ||
|
|
35dfb9d1ff | ||
|
|
3809aca09d | ||
|
|
bcd5bb5009 | ||
|
|
0d7cc6a386 | ||
|
|
12265699b3 | ||
|
|
783c0356c7 | ||
|
|
7ef31bc0b5 | ||
|
|
467f2368dd | ||
|
|
2cfcb081a2 | ||
|
|
1829ac851d | ||
|
|
2e715004ae | ||
|
|
45ee092593 | ||
|
|
20e543f721 | ||
|
|
b2148eb656 | ||
|
|
efab4fb41b | ||
|
|
9ac3fd3615 | ||
|
|
14bc7a0f76 | ||
|
|
26727fea17 | ||
|
|
34f8d4c2a6 | ||
|
|
02708807bd | ||
|
|
176e5de531 | ||
|
|
b9180a4426 | ||
|
|
959b30c788 | ||
|
|
df79ef59ea | ||
|
|
4dc1f01cf0 | ||
|
|
cb17110562 | ||
|
|
4424e4f9be | ||
|
|
fb641ac960 | ||
|
|
a5faa378b0 | ||
|
|
66ea277a59 | ||
|
|
006bc6ceda | ||
|
|
aef3e869dc | ||
|
|
9c46d28871 | ||
|
|
1fc141050f | ||
|
|
1e45d199e2 | ||
|
|
3018f3653c | ||
|
|
1c5b856800 | ||
|
|
f53a5ea6c1 | ||
|
|
a608734be9 | ||
|
|
e52523f903 | ||
|
|
82778014b8 | ||
|
|
cb4b9a361f | ||
|
|
3a337940d1 | ||
|
|
9abdafb905 | ||
|
|
cd494b52a4 | ||
|
|
5581d1431b | ||
|
|
6539b14ecf | ||
|
|
45b30e4a33 | ||
|
|
ff48d543e7 | ||
|
|
6562b74130 | ||
|
|
23a68370b4 | ||
|
|
cc9f346d49 | ||
|
|
8c18865138 | ||
|
|
db3a17a2c8 | ||
|
|
5a0d1ac0c0 | ||
|
|
5414accc6c | ||
|
|
c415ace453 | ||
|
|
bf34beec20 | ||
|
|
f4d459af7f | ||
|
|
508cc2bd91 | ||
|
|
35b7e3a289 | ||
|
|
fc907d23a7 | ||
|
|
45e663fcf8 | ||
|
|
b00c6a9268 | ||
|
|
ad7b0cd834 | ||
|
|
eccb3e2825 | ||
|
|
f5a343f358 | ||
|
|
f8b65a5546 | ||
|
|
0f0b9c5551 | ||
|
|
41ab11e7b4 | ||
|
|
9949e7c8d4 | ||
|
|
9e36d84f19 | ||
|
|
ada526fa63 | ||
|
|
ca65eb907d | ||
|
|
7f2a0dd3e8 | ||
|
|
6e34409225 | ||
|
|
4af354e918 | ||
|
|
207d0caf2a | ||
|
|
f7487d22d5 | ||
|
|
3c10976aff | ||
|
|
ec8e144655 | ||
|
|
ae5ccfd775 | ||
|
|
9d9e22451d | ||
|
|
e68daf870f | ||
|
|
534e95f86c | ||
|
|
077713003f | ||
|
|
35fd8e62ac | ||
|
|
b7c2ddd59d | ||
|
|
f931567f56 | ||
|
|
8545e0692c | ||
|
|
19eb05269b | ||
|
|
92d757662a | ||
|
|
1b1406a4d9 | ||
|
|
03fc19f070 | ||
|
|
ffa09f1b29 | ||
|
|
ff9a1523fd | ||
|
|
8a7e5c0830 | ||
|
|
8e90ad69b1 | ||
|
|
52c389edd8 | ||
|
|
f7206c1603 | ||
|
|
86d9baa503 | ||
|
|
92c4565590 | ||
|
|
c51870ff79 | ||
|
|
182f061354 | ||
|
|
80507cab27 | ||
|
|
f801ae9b63 | ||
|
|
977173d987 | ||
|
|
cd0fcb903f | ||
|
|
7bae1d9537 | ||
|
|
b43cbb7afe | ||
|
|
72982387cc | ||
|
|
ff0245840a | ||
|
|
c55f64e209 | ||
|
|
a4cb5d1b14 | ||
|
|
13e1415355 | ||
|
|
7b49d66a8e | ||
|
|
63c6e12aca | ||
|
|
b64d4fd96f | ||
|
|
dda603c7d8 | ||
|
|
e22de5cba1 | ||
|
|
bdabfdcb3d | ||
|
|
00a8514245 | ||
|
|
94fd3197b3 | ||
|
|
7957353c3f | ||
|
|
b3b7589db3 | ||
|
|
5aed9ce29e | ||
|
|
924f0a9f7c | ||
|
|
7a7cd72db9 | ||
|
|
e9e9bab479 | ||
|
|
f258dcfca2 | ||
|
|
fe84f812e7 | ||
|
|
9eba376976 | ||
|
|
462705c4ed | ||
|
|
a2533ce7f9 | ||
|
|
dbf42c51a4 | ||
|
|
f61e7caf2b | ||
|
|
679c2070c1 | ||
|
|
92d46e1da3 | ||
|
|
7ab94205e4 | ||
|
|
ab616c5d32 | ||
|
|
8f2f68dffc | ||
|
|
18b7e17e95 | ||
|
|
cca2298d3a | ||
|
|
f3683bcc84 | ||
|
|
fa2959515e | ||
|
|
7ab5ddc408 | ||
|
|
f273858248 | ||
|
|
ca8f7374da | ||
|
|
ff1dce833a | ||
|
|
d3d5a7bade | ||
|
|
f5d6702472 | ||
|
|
3db687ad40 | ||
|
|
a5cb958398 | ||
|
|
9e522091c6 | ||
|
|
79f256976e | ||
|
|
b560c0bafd | ||
|
|
bd157d305e | ||
|
|
923016197a | ||
|
|
dcf96e2bf5 | ||
|
|
d4697cb203 | ||
|
|
6e6a50b46e | ||
|
|
b9830bc64a | ||
|
|
7c501cec45 | ||
|
|
add23934ca | ||
|
|
a8b798b00c | ||
|
|
b522b1de05 | ||
|
|
3660cbd450 | ||
|
|
50f8bb8721 | ||
|
|
e1a2d90382 | ||
|
|
d8872f215b | ||
|
|
484bc6ea39 | ||
|
|
7d1979f067 | ||
|
|
6927c0b30b | ||
|
|
aa74c5ccaf | ||
|
|
e3d0f571d2 | ||
|
|
d03dabdfdf | ||
|
|
fc339ae55f | ||
|
|
a0aa04fd8c | ||
|
|
6dc5ae1ae4 | ||
|
|
df02a6b50f | ||
|
|
9e4f733c28 | ||
|
|
1175b330f5 | ||
|
|
3cb9fff07f | ||
|
|
bfdb72dd0a | ||
|
|
5489462f90 | ||
|
|
94ac3c3922 | ||
|
|
bf75946e14 | ||
|
|
b2a70e0cce | ||
|
|
83780f9bcd | ||
|
|
bfb9795913 | ||
|
|
208598a6df | ||
|
|
6c79753051 | ||
|
|
095a238fff | ||
|
|
ebbde8afd3 | ||
|
|
bba5bb7411 | ||
|
|
9c155c6cf5 | ||
|
|
19da7a74df | ||
|
|
f5d76fd5ff | ||
|
|
77940c9430 | ||
|
|
54a42a14b6 | ||
|
|
52faf7884b | ||
|
|
841ed4e682 | ||
|
|
bc417b9eea | ||
|
|
da7dc477c8 | ||
|
|
6c519ebd61 | ||
|
|
88e859817b | ||
|
|
f5dd36260f | ||
|
|
a5325b84ae | ||
|
|
7095c9e71e | ||
|
|
fb4373c83a | ||
|
|
dd59a1aebb | ||
|
|
6f7edd0b40 | ||
|
|
397421010e | ||
|
|
8872b0519d | ||
|
|
83d3b3ffbf | ||
|
|
d1702bd62c | ||
|
|
928235eac8 | ||
|
|
3bdfaab158 | ||
|
|
d7902b4d08 | ||
|
|
3334abfb8f | ||
|
|
3e44e584c0 | ||
|
|
195a305fc3 | ||
|
|
cedffd17b3 | ||
|
|
59b29014d9 | ||
|
|
06db036e4a | ||
|
|
617e0837c9 | ||
|
|
7a275bd802 | ||
|
|
83f58eae68 | ||
|
|
19815fe27d | ||
|
|
0f75556bed | ||
|
|
dc0f925d24 | ||
|
|
c5948c4171 | ||
|
|
d144b06c1f | ||
|
|
92dfea677b | ||
|
|
7b5867e2c0 | ||
|
|
b643fe56d5 | ||
|
|
38fa72e9da | ||
|
|
82fddefc94 | ||
|
|
0c9c9cb90a | ||
|
|
38b50a8a84 | ||
|
|
0f250dbafd | ||
|
|
168650649f | ||
|
|
52babf82ae | ||
|
|
0c8517667f | ||
|
|
77d105cb9f | ||
|
|
8af33ea66a | ||
|
|
a57740e14e | ||
|
|
82230d70a5 | ||
|
|
15f3f9c0e5 | ||
|
|
7fc328492b | ||
|
|
81cedca590 | ||
|
|
df4e00e23f | ||
|
|
ad70f23a05 | ||
|
|
c93beb27fd | ||
|
|
41da2231ed | ||
|
|
9d9213fbdb | ||
|
|
50106d19e8 | ||
|
|
62d1e676bd | ||
|
|
0b7d49785c | ||
|
|
40736c4a05 | ||
|
|
1f0ab02702 | ||
|
|
21aa4f7b2b | ||
|
|
43d0a85061 | ||
|
|
8bdd31ae67 | ||
|
|
b78f93979d | ||
|
|
8d6b4a1d2e | ||
|
|
7630c83ae0 | ||
|
|
c7c47aff5a | ||
|
|
72e475cb84 | ||
|
|
f750a6aec2 | ||
|
|
0dde37e1f1 | ||
|
|
a2066fc137 | ||
|
|
f652f35c3a | ||
|
|
562697da27 | ||
|
|
d23d77ff90 | ||
|
|
119443cc88 | ||
|
|
27ae831799 | ||
|
|
b482947207 | ||
|
|
6ebc89695a | ||
|
|
a65dfec7a8 | ||
|
|
a0cd79e587 | ||
|
|
8fe1e2fee4 | ||
|
|
3cc9f7bc40 | ||
|
|
8d3d5025ed | ||
|
|
a49c0aea47 | ||
|
|
d366cf9885 | ||
|
|
be53afe6b4 | ||
|
|
cdd980112d | ||
|
|
bca284969d | ||
|
|
dd899a3e13 | ||
|
|
d6ca66aa2f | ||
|
|
20ba671cd3 | ||
|
|
672795ac49 | ||
|
|
9716d7fe43 | ||
|
|
193ad8d902 | ||
|
|
a87f903c50 | ||
|
|
82f154a229 | ||
|
|
bee36801ab | ||
|
|
37f379c890 | ||
|
|
88b995ca9c | ||
|
|
919289c5ca | ||
|
|
0535b56766 | ||
|
|
6afd413b87 | ||
|
|
4818409329 | ||
|
|
919b652a06 | ||
|
|
15d3ec9d81 | ||
|
|
e0be6e429e | ||
|
|
8d24be914d | ||
|
|
8bbe9c97e9 | ||
|
|
ccbd904a3f | ||
|
|
4ed3db7e41 | ||
|
|
65ced28004 | ||
|
|
4c282962b3 | ||
|
|
c64c436b9f | ||
|
|
378e270f41 | ||
|
|
7217bd7d1a | ||
|
|
ff2461d89d | ||
|
|
0eb3430c14 | ||
|
|
da7c1e40e3 | ||
|
|
8c9e928ddc | ||
|
|
477aef7db6 | ||
|
|
cc97d9f1ea | ||
|
|
ee6cf05a92 | ||
|
|
575db58476 | ||
|
|
986a2f8cbb | ||
|
|
0b1da914cd | ||
|
|
04acd7c56d | ||
|
|
4430d85242 | ||
|
|
55ade32589 | ||
|
|
0ffbaaaeaa | ||
|
|
62b40ddb84 | ||
|
|
2d5ce87605 | ||
|
|
7ca0bc5960 | ||
|
|
021c09e426 | ||
|
|
75db81f2f9 | ||
|
|
f162617765 | ||
|
|
b7d10a4c58 | ||
|
|
79ca1aea02 | ||
|
|
957201804c | ||
|
|
cf5e126eaa | ||
|
|
69fc0a118b | ||
|
|
4dacc26567 | ||
|
|
7de1ed7d45 | ||
|
|
ab47f01625 | ||
|
|
13c4fa4f1f | ||
|
|
173631f115 | ||
|
|
8462e9c097 | ||
|
|
3c9447e1b6 | ||
|
|
38eaf2eb02 | ||
|
|
c8364ed17b | ||
|
|
ba52738904 | ||
|
|
4061ae4275 | ||
|
|
82afb5b150 | ||
|
|
e1203f0c8d | ||
|
|
e6f6b3fee2 | ||
|
|
e5efcd3d26 | ||
|
|
bf45587c80 | ||
|
|
29a0d22938 | ||
|
|
635cc04c50 | ||
|
|
bc5343652b | ||
|
|
03f140fe3b | ||
|
|
3e4a119f61 | ||
|
|
3b7bcbc14a | ||
|
|
3c49e96e02 | ||
|
|
5be209ab59 | ||
|
|
56ea048ab3 | ||
|
|
9fc0bd0439 | ||
|
|
1c2798cbf4 | ||
|
|
9d8af2eaab | ||
|
|
bba1e0d12f | ||
|
|
c060e60e4a | ||
|
|
1c504bd899 | ||
|
|
b617648bd8 | ||
|
|
e849c7a34f | ||
|
|
f6dd1ce98e | ||
|
|
9c78db8d45 | ||
|
|
5154348cf9 | ||
|
|
4521cea3b4 | ||
|
|
0ff1ac7743 | ||
|
|
277a751ed4 | ||
|
|
96be2a86ca | ||
|
|
f28bff569a | ||
|
|
e34aa77a54 | ||
|
|
e53b65496f | ||
|
|
f6ee630615 | ||
|
|
ec2e1e3152 | ||
|
|
6cffe9baed | ||
|
|
b63df27409 | ||
|
|
617c466ac0 | ||
|
|
ed8e443f3a | ||
|
|
58cb49b125 | ||
|
|
7dd5baa9ec | ||
|
|
bbf9107560 | ||
|
|
be527355ee | ||
|
|
c866500120 | ||
|
|
3e2a40aaa0 | ||
|
|
eef9fa56c6 | ||
|
|
945d84dbc4 | ||
|
|
d353d33107 | ||
|
|
f54bad5d79 | ||
|
|
b605c70e91 | ||
|
|
1752e88c42 | ||
|
|
e2718a39a0 | ||
|
|
25298ac69e | ||
|
|
cf9f389f75 | ||
|
|
567c75830a | ||
|
|
7b1d352c95 | ||
|
|
4fa54381a6 | ||
|
|
9c91f7cf83 | ||
|
|
edd5cd1ca1 | ||
|
|
f2ec6ad05e | ||
|
|
a04ef4ac38 | ||
|
|
43b3d1d379 | ||
|
|
b65fdeacc2 | ||
|
|
622fe1e5d9 | ||
|
|
a6c7c0c7ad | ||
|
|
e5c8748b75 | ||
|
|
f096d71cc1 | ||
|
|
d73a152a36 | ||
|
|
f4e6fd060e | ||
|
|
c78ca1de5d | ||
|
|
2b219c7256 | ||
|
|
6ffa651742 | ||
|
|
cb3b22a4bf | ||
|
|
145bfdb0e9 | ||
|
|
8c7b0c502d | ||
|
|
684bf12a5c | ||
|
|
aaa75aff14 | ||
|
|
f03600bd65 | ||
|
|
1c4c4fe3fb | ||
|
|
5e46b2cd1a | ||
|
|
027db7c0ec | ||
|
|
5bb17ddc6b | ||
|
|
e8edd74bc3 | ||
|
|
ed064a825a | ||
|
|
32c232a0b5 | ||
|
|
c2fd747c45 | ||
|
|
9148853f2c | ||
|
|
23d0f50a3d | ||
|
|
36cdf87bfe | ||
|
|
cfd2e1fd7b | ||
|
|
3cafac99ff | ||
|
|
1dd4e8da71 | ||
|
|
543015a36e | ||
|
|
2efdfc4b1f | ||
|
|
7234d9307e | ||
|
|
769ce1ce7c | ||
|
|
62c1d5783e | ||
|
|
a6bd4a2ffe | ||
|
|
f1a9077b7e | ||
|
|
2c3e80bd8f | ||
|
|
e52d066fb0 | ||
|
|
5279d57018 | ||
|
|
25e5eb4427 | ||
|
|
b01d5ce8c4 | ||
|
|
ff41f5c041 | ||
|
|
dd0770a93f | ||
|
|
9a3e76fff3 | ||
|
|
7c867578ee | ||
|
|
d543c27000 | ||
|
|
a8be330baa | ||
|
|
c128c58110 | ||
|
|
e32a3887cd | ||
|
|
94932c7486 | ||
|
|
a1920745fb | ||
|
|
51e8f9555e | ||
|
|
213ab8418b | ||
|
|
707f1dd6cb | ||
|
|
125ce036cd | ||
|
|
172c562a71 | ||
|
|
80bbe6711c | ||
|
|
3f56c64086 | ||
|
|
e6167119e0 | ||
|
|
368fae5f32 | ||
|
|
6ae46cf7bb | ||
|
|
e97806c85b | ||
|
|
f31e88bed1 | ||
|
|
6bd20038f9 | ||
|
|
30cfb1200d | ||
|
|
154e0039d1 | ||
|
|
a8f3b02ee7 | ||
|
|
b3e83e13bc | ||
|
|
d0a0e77c44 | ||
|
|
a14896307f | ||
|
|
976b300277 | ||
|
|
ccbd873204 | ||
|
|
9c1482f9e9 | ||
|
|
5a7f4efa91 | ||
|
|
f78c4a1fb0 | ||
|
|
db6500369f | ||
|
|
9e4beaa317 | ||
|
|
afd021737a | ||
|
|
3982ed56f7 | ||
|
|
df4a01a7f9 | ||
|
|
a6fac96ec1 | ||
|
|
8ed9186ff4 | ||
|
|
821df75d4b | ||
|
|
0c09a2445c | ||
|
|
e6983e858d | ||
|
|
f8b69df955 | ||
|
|
15370a93c7 | ||
|
|
bc18aeeadc | ||
|
|
a1f143b0aa | ||
|
|
c13fe9d590 | ||
|
|
50203fbcb3 | ||
|
|
ffe7ebf711 | ||
|
|
f0b5e0c3b9 | ||
|
|
501235ef12 | ||
|
|
da0fa31181 | ||
|
|
0272933f70 | ||
|
|
30d88761b4 | ||
|
|
fb286cea3c | ||
|
|
6bcc7b6c49 | ||
|
|
6338a44cc1 | ||
|
|
ae4680f20c | ||
|
|
2841874417 | ||
|
|
b6a4e6b4de | ||
|
|
34cfd1e344 | ||
|
|
b467dc55e5 | ||
|
|
9fd8d44a6e | ||
|
|
64262134c4 | ||
|
|
0019c9ef41 | ||
|
|
2676ebd047 | ||
|
|
97cdd56110 | ||
|
|
d973451bdc | ||
|
|
80313f613e | ||
|
|
5c564524a3 | ||
|
|
e081fbd92b | ||
|
|
0ecc20472a | ||
|
|
b51052f0c6 | ||
|
|
cb106e42ee | ||
|
|
52f9495ff8 | ||
|
|
440b633bad | ||
|
|
a07913181a | ||
|
|
34ffd96c86 | ||
|
|
46a623b430 | ||
|
|
c16e37023c | ||
|
|
1cb0cdf540 | ||
|
|
073d03a882 | ||
|
|
30b9276ab4 | ||
|
|
76c8b4a4c6 | ||
|
|
9ea4781d93 | ||
|
|
07d583f73f | ||
|
|
12c74aef7a | ||
|
|
64b10e3060 | ||
|
|
ab2b29f267 | ||
|
|
be9a968ad9 | ||
|
|
5f87067a75 | ||
|
|
ef273bd9dd | ||
|
|
0737a9fae7 | ||
|
|
727d7e1d81 | ||
|
|
87e8589aea | ||
|
|
b13758d3e9 | ||
|
|
14775e2861 | ||
|
|
94af3822d8 | ||
|
|
07254c9f27 | ||
|
|
a99c2f6944 | ||
|
|
f9540b08cd | ||
|
|
34af77eb6d | ||
|
|
cf3f22776c | ||
|
|
5bebc8b5ee | ||
|
|
8a4b0c57f9 | ||
|
|
1acfd7ee34 | ||
|
|
a29661c54d | ||
|
|
90558d5ece | ||
|
|
e6c7007be5 | ||
|
|
16d0d1687f | ||
|
|
95ab44d118 | ||
|
|
e541757b76 | ||
|
|
f422aea330 | ||
|
|
d5eb3716aa | ||
|
|
7fb854fb48 | ||
|
|
60b5ecdcd7 | ||
|
|
6cce7d31ff | ||
|
|
46f5dd99a6 | ||
|
|
9753dec996 | ||
|
|
53f2e07178 | ||
|
|
3aa2c604d8 | ||
|
|
d8fbf36e00 | ||
|
|
008653e3d9 | ||
|
|
23188777fe | ||
|
|
8eb0a49ee6 | ||
|
|
207f09a4af | ||
|
|
69120c5045 | ||
|
|
b8143d1a9b | ||
|
|
f7eab5893a | ||
|
|
5fc598a220 | ||
|
|
783c21ad18 | ||
|
|
a1ce6e6928 | ||
|
|
8cbae629a5 | ||
|
|
da7e832f21 | ||
|
|
a572ba0523 | ||
|
|
85a20168dc | ||
|
|
25be9c0fef | ||
|
|
a8c890ba51 | ||
|
|
11628b98ca | ||
|
|
4ae6ca945b | ||
|
|
49aa1434aa | ||
|
|
9e92c61fbf | ||
|
|
c84111b6b9 | ||
|
|
3a2fcdd18a | ||
|
|
84a800f69b | ||
|
|
77e08aa048 | ||
|
|
0d6fd903f1 | ||
|
|
bcc74dd927 | ||
|
|
dd0720afa7 | ||
|
|
a06a4095b6 | ||
|
|
29bc009c07 | ||
|
|
520d2a0e20 | ||
|
|
dbeb9dd561 | ||
|
|
5b02d8008f | ||
|
|
a032c6114f | ||
|
|
69ec4966d5 | ||
|
|
87fab80ea3 | ||
|
|
bd2dabe851 | ||
|
|
4a45d69e5b | ||
|
|
e15bea9342 | ||
|
|
7132413837 | ||
|
|
c51116acaa | ||
|
|
002776f15e | ||
|
|
c7f5c62e71 | ||
|
|
3c57cf8d81 | ||
|
|
f29bf3640a | ||
|
|
07663dea6c | ||
|
|
0ddb696e90 | ||
|
|
cc0a6d4706 | ||
|
|
4c0ecc8f07 | ||
|
|
d50c8ce691 | ||
|
|
8aa66661ac | ||
|
|
00a9b3b57b | ||
|
|
3896d08207 | ||
|
|
9b736c99f8 | ||
|
|
129d7e5850 | ||
|
|
c2b26ffe6e | ||
|
|
9b01e076f5 | ||
|
|
88553872fc | ||
|
|
2b8de4c028 | ||
|
|
24c5200a90 | ||
|
|
bca0410a08 | ||
|
|
42234be5cf | ||
|
|
8e108e2d38 | ||
|
|
248b0ce196 | ||
|
|
d25ee3c234 | ||
|
|
8ea1234596 | ||
|
|
32530e5dc9 | ||
|
|
952d060e2f | ||
|
|
712f9e07b7 | ||
|
|
03cd6e79bb | ||
|
|
cbd9e8a33c | ||
|
|
13222fbe9a | ||
|
|
4b89eb88bd | ||
|
|
646a5e3b28 | ||
|
|
08153cd99b | ||
|
|
61ebbac333 | ||
|
|
d63c1f156f | ||
|
|
a4548e2cba | ||
|
|
77a3f2027e | ||
|
|
ecf0c78993 | ||
|
|
1a0c1e3306 | ||
|
|
506207d3ba | ||
|
|
76bf46c152 | ||
|
|
96c64fbb91 | ||
|
|
7fedb7d275 | ||
|
|
c16f316200 | ||
|
|
4c5c071b21 | ||
|
|
df917001d3 | ||
|
|
65e75f974d | ||
|
|
8afc1db72f | ||
|
|
71f13a0b50 | ||
|
|
4f57b195a8 | ||
|
|
aa1ea41c5d | ||
|
|
b45058de72 | ||
|
|
576ab9a268 | ||
|
|
e3312c97a7 | ||
|
|
6bafa0a6dd | ||
|
|
153d26ffcd | ||
|
|
74fecdd941 | ||
|
|
902844e008 | ||
|
|
e78d850138 | ||
|
|
94cefe52dd | ||
|
|
a011f82912 | ||
|
|
a160b2a471 | ||
|
|
396144f3fb | ||
|
|
ff0fadc0c1 | ||
|
|
65ec3a10bf | ||
|
|
01c721c7e0 | ||
|
|
d9aadb4f30 | ||
|
|
964611eba4 | ||
|
|
98d2627036 | ||
|
|
ba64540743 | ||
|
|
62c50bb4e6 | ||
|
|
0d4b005f4e | ||
|
|
61b1206091 | ||
|
|
2d37faea1d | ||
|
|
c0a0f34ff4 | ||
|
|
e983677e57 | ||
|
|
a813809fc6 | ||
|
|
f28b99b516 | ||
|
|
bab37530e4 | ||
|
|
fb24c63e7f | ||
|
|
65b8e2270e | ||
|
|
bfb2db8a3f | ||
|
|
fe8deb98a2 | ||
|
|
cee2458370 | ||
|
|
764bf6dd55 | ||
|
|
88aee1e3bf | ||
|
|
3f5c85b434 | ||
|
|
d34bff28c5 | ||
|
|
bcc7d6d35c | ||
|
|
f3f0dec87b | ||
|
|
bf4c4df939 | ||
|
|
51efb07c17 | ||
|
|
20e13ee9eb | ||
|
|
b1b4ef926f | ||
|
|
43e6b4dc2f | ||
|
|
906d87f43f | ||
|
|
8f622dd6a5 | ||
|
|
ec8d48292e | ||
|
|
1882a7baba | ||
|
|
d4cccbeb09 | ||
|
|
1f187ba8fb | ||
|
|
9c4ff466a4 | ||
|
|
e5c3cf6adb | ||
|
|
02fd214b33 | ||
|
|
7fbd3bc760 | ||
|
|
1ddb88a3a6 | ||
|
|
6edd4451c5 | ||
|
|
1e83c7442a | ||
|
|
91533c5cac | ||
|
|
a2ee1135dd | ||
|
|
0ec255ed60 | ||
|
|
a19d11061f | ||
|
|
a730c95492 | ||
|
|
89a50fd389 | ||
|
|
82a340d576 | ||
|
|
c952da669c | ||
|
|
24cec7016a | ||
|
|
2c407bea78 | ||
|
|
fba98db7cb | ||
|
|
00eb8f7b01 | ||
|
|
1055daa0e3 | ||
|
|
928145214d | ||
|
|
56e52a7dfd | ||
|
|
479d3e3f39 | ||
|
|
ad3e773f27 | ||
|
|
42c77db1d4 | ||
|
|
11ea2d3697 | ||
|
|
d0b54ab27c | ||
|
|
5b9b6ed966 | ||
|
|
fc192891b7 | ||
|
|
14f54e9df4 | ||
|
|
07a290dbf9 | ||
|
|
694f9a37a5 | ||
|
|
13e58c63f4 | ||
|
|
67c79bf565 | ||
|
|
428b72ef3d | ||
|
|
b78d92b387 | ||
|
|
a09f2038ee | ||
|
|
fbb74e09e8 | ||
|
|
ca1028921a | ||
|
|
0fd37e4c05 | ||
|
|
f3d9e3376e | ||
|
|
9296008ecc | ||
|
|
ee7a1bd99c | ||
|
|
21eab03684 | ||
|
|
da066e40ce | ||
|
|
a219b7b6ee | ||
|
|
85c4ed6399 | ||
|
|
fa42194d15 | ||
|
|
e574e728d4 | ||
|
|
2ca35e4458 | ||
|
|
99027858d9 | ||
|
|
e7fcb47e81 | ||
|
|
02d6dcd592 | ||
|
|
6e0a575da9 | ||
|
|
93387c289e | ||
|
|
1227a05e2d | ||
|
|
9f00047fdd | ||
|
|
9bc3e56c79 | ||
|
|
508aaef303 | ||
|
|
efd44a5da1 | ||
|
|
0c70613865 | ||
|
|
6fda0bd548 | ||
|
|
77224c3726 | ||
|
|
f25d72e4f5 | ||
|
|
34603ff96e | ||
|
|
812232b945 | ||
|
|
bd7228a378 | ||
|
|
ab61715973 | ||
|
|
095f461cfd | ||
|
|
047771e6f8 | ||
|
|
e2cec420fa | ||
|
|
35e55b8677 | ||
|
|
1b0ec71d93 | ||
|
|
c6c735bbe8 | ||
|
|
d5bc7d4051 | ||
|
|
74405f1a2a | ||
|
|
016bc41180 | ||
|
|
e5df3e6746 | ||
|
|
13fb884387 | ||
|
|
3b9c9872ca | ||
|
|
2fc329a403 | ||
|
|
8ca1ef3b50 | ||
|
|
f7dd9f852f | ||
|
|
4a9ed730c6 | ||
|
|
a023c0b8bf | ||
|
|
ff38be3187 | ||
|
|
9ffb2de2c8 | ||
|
|
dcd87f86f1 | ||
|
|
d149c16713 | ||
|
|
1d99022ca3 | ||
|
|
bc85da49e3 | ||
|
|
18e1240775 | ||
|
|
e149e276d5 | ||
|
|
02654c8327 | ||
|
|
dace1dd1f3 | ||
|
|
c46fd080df | ||
|
|
ef2230a331 | ||
|
|
2ecd0584aa | ||
|
|
65c398880b | ||
|
|
5962a593da | ||
|
|
67baddf7a8 | ||
|
|
ceb4fc8292 | ||
|
|
c51135a4cc | ||
|
|
b2b4f593ce | ||
|
|
a95504bbf1 | ||
|
|
6ed0e14fe0 | ||
|
|
257e69268b | ||
|
|
7e951196bf | ||
|
|
501872e8d2 | ||
|
|
87e46ec5a5 | ||
|
|
ebe953cf63 | ||
|
|
cbfcdbf836 | ||
|
|
bd15915648 | ||
|
|
312acdab51 | ||
|
|
4ba9cc88dd | ||
|
|
239013a2cb | ||
|
|
85412ea4b7 | ||
|
|
cfda858d87 | ||
|
|
df8fdd56ba | ||
|
|
698d03f77e | ||
|
|
3e15a3341c | ||
|
|
d8a25e75d7 | ||
|
|
42f69124aa | ||
|
|
621726ab3b | ||
|
|
cce7523f45 | ||
|
|
5e6a62376a | ||
|
|
b03fb9f1de | ||
|
|
1a7591314f | ||
|
|
b8852f821c | ||
|
|
6ebca3befa | ||
|
|
8db34c6ee6 | ||
|
|
d799c06017 | ||
|
|
50a7950ccd | ||
|
|
a393dec0a0 | ||
|
|
423aad4179 | ||
|
|
80d10051cf | ||
|
|
b1776c82ad | ||
|
|
36f313380e | ||
|
|
7df9c37850 | ||
|
|
9001c51bea | ||
|
|
99757fc947 | ||
|
|
8e9ff1116a | ||
|
|
f1df4e07d2 | ||
|
|
3e3799074a | ||
|
|
ae0ee590e4 | ||
|
|
988a9b0606 | ||
|
|
7a457e4364 | ||
|
|
2edbe4fb3f | ||
|
|
8eaff830ad | ||
|
|
7fdc7a47e3 | ||
|
|
0e821d1c84 | ||
|
|
c23de08cf5 | ||
|
|
a6acb7ea0d | ||
|
|
0c64cf0546 | ||
|
|
a4a9ab8d2d | ||
|
|
19a1ae9bec | ||
|
|
36cb8290f4 | ||
|
|
244991e8e8 | ||
|
|
d6a7c19cbf | ||
|
|
64906a827d | ||
|
|
da53306a2c | ||
|
|
48515d7caf | ||
|
|
1f6ef62499 | ||
|
|
6b4b88aba7 | ||
|
|
fadff146b4 | ||
|
|
01feacfe54 | ||
|
|
d6ddc5ff88 | ||
|
|
287b6b396d | ||
|
|
b976f294f9 | ||
|
|
dce48bd0cb | ||
|
|
ab84235890 | ||
|
|
7445ac3a39 | ||
|
|
f9ceb3e2d8 | ||
|
|
8bb7b60055 | ||
|
|
190211a467 | ||
|
|
8a6868e811 | ||
|
|
6aa868c8d8 | ||
|
|
4dfa1c8efc | ||
|
|
e2e7bc8d72 | ||
|
|
a97d78bbf4 | ||
|
|
22dbd288df | ||
|
|
4685cdcd3c | ||
|
|
f792b3d983 | ||
|
|
adc94cef90 | ||
|
|
e639cfbc2f | ||
|
|
e520cd9020 | ||
|
|
daf8d15f45 | ||
|
|
0e473ceacc | ||
|
|
873bd0ed88 | ||
|
|
58b7853d63 | ||
|
|
2284788366 | ||
|
|
d1766e52b6 | ||
|
|
fdd5e36d19 | ||
|
|
4fe4dc8c6e | ||
|
|
a3202cbead | ||
|
|
e8b03ae565 | ||
|
|
829e3982d2 | ||
|
|
07c5f586b0 | ||
|
|
2ebaeb3453 | ||
|
|
5660be12f6 | ||
|
|
3cd00e1343 | ||
|
|
f983146501 | ||
|
|
6cf64ce538 | ||
|
|
47a7876505 | ||
|
|
3f5ac55753 | ||
|
|
a33d95f2c1 | ||
|
|
1128db184e | ||
|
|
153fd6c569 | ||
|
|
c9d002c1cd | ||
|
|
e0a108eb2e | ||
|
|
ae587950b9 | ||
|
|
e956a03098 | ||
|
|
1702aab538 | ||
|
|
3c67b49d08 | ||
|
|
d58246b255 | ||
|
|
814a488801 | ||
|
|
e029b39eb9 | ||
|
|
a8361299c7 | ||
|
|
e3f5fb323a | ||
|
|
be262c3a69 | ||
|
|
a4525d4519 | ||
|
|
4f6034457f | ||
|
|
5413457b6b | ||
|
|
977cf61b50 | ||
|
|
8c8c5b04d5 | ||
|
|
620465d62a | ||
|
|
a80e0d4c45 | ||
|
|
0ab6e6ca8d | ||
|
|
dcd41b4be2 | ||
|
|
33cd9358c0 | ||
|
|
51a3ad25d1 | ||
|
|
f586938fe9 | ||
|
|
912d229bdd | ||
|
|
a93345afbd | ||
|
|
a7bd0e0dac | ||
|
|
e2fd37fe24 | ||
|
|
6e6397fc91 | ||
|
|
45c20dbed9 | ||
|
|
594c19da03 | ||
|
|
9251ccbb12 | ||
|
|
34305a1285 | ||
|
|
ccc60dfd77 | ||
|
|
b7da689955 | ||
|
|
0598a36b19 | ||
|
|
947e106f19 | ||
|
|
81957c9396 | ||
|
|
d54c86cec9 | ||
|
|
c17eca28fa | ||
|
|
9a69f3b019 | ||
|
|
c39fc80c02 | ||
|
|
b0eead121a | ||
|
|
511b98ab5b | ||
|
|
a69b01ecf5 | ||
|
|
a967493d77 | ||
|
|
050c9702d8 | ||
|
|
0d23b973de | ||
|
|
fc3170369b | ||
|
|
647f7fdc7d | ||
|
|
8c3cd379a2 | ||
|
|
cf9051412a | ||
|
|
6db0ff5647 | ||
|
|
9ce127df86 | ||
|
|
20eec62fde | ||
|
|
effc8ce43f | ||
|
|
ced25e0cd2 | ||
|
|
72c70fe494 | ||
|
|
dc062a44e1 | ||
|
|
dff22272b5 | ||
|
|
a0a1e03b53 | ||
|
|
3915c065fe | ||
|
|
dc71a779e0 | ||
|
|
b56ba93634 | ||
|
|
2926472f7d | ||
|
|
a253d42f10 | ||
|
|
700d566255 | ||
|
|
fbbace90aa | ||
|
|
6b5f7e780c | ||
|
|
79d4932bee | ||
|
|
e8af0f2ea6 | ||
|
|
f1ecf33ce7 | ||
|
|
18eaf56ff9 | ||
|
|
75f15ccc96 | ||
|
|
3f17e91f72 | ||
|
|
ee6eddf308 | ||
|
|
da84f15e9f | ||
|
|
62f4d43bd9 | ||
|
|
376120b6d0 | ||
|
|
ff872c7dce | ||
|
|
a834481d32 | ||
|
|
4c5d3bd43e | ||
|
|
b737b841f5 | ||
|
|
0c5500edd4 | ||
|
|
990a40e4e4 | ||
|
|
5eb2124b06 | ||
|
|
20d8180347 | ||
|
|
49203c15a7 | ||
|
|
008040d96c | ||
|
|
1a36044de2 | ||
|
|
4886edc684 | ||
|
|
617fe902a4 | ||
|
|
78ad3468ae | ||
|
|
b58de926b2 | ||
|
|
7c4c7eea9c | ||
|
|
eb7d93af87 | ||
|
|
956b68a545 | ||
|
|
79065a7675 | ||
|
|
d3514a0334 | ||
|
|
bb163605af | ||
|
|
afcbd6af92 | ||
|
|
13edea3449 | ||
|
|
6cdcd4e0dc | ||
|
|
efce884494 | ||
|
|
dcffeded9a | ||
|
|
e24b6806da | ||
|
|
bc7874a3a0 | ||
|
|
d348871b0c | ||
|
|
f9ee740a8c | ||
|
|
c0a90ae89d | ||
|
|
58ca285edf | ||
|
|
325f8c0f7e | ||
|
|
829fe7e4ba | ||
|
|
a6a18a0ee4 | ||
|
|
eda9eb08d5 | ||
|
|
4625ae7548 | ||
|
|
186d3b0d79 | ||
|
|
7bf5805714 | ||
|
|
19604c46f0 | ||
|
|
a77a32d64e | ||
|
|
53145f0ca2 | ||
|
|
6880baa6a4 | ||
|
|
2bda6bf668 | ||
|
|
80fe978454 | ||
|
|
94a30ea940 | ||
|
|
f9d1aa93c4 | ||
|
|
d3bda0d869 | ||
|
|
426fa63288 | ||
|
|
bf46a00937 | ||
|
|
0fce0c2386 | ||
|
|
77843ccdee | ||
|
|
b86edcfa96 | ||
|
|
5fb242024a | ||
|
|
abbfbb85e6 | ||
|
|
37407cdbac | ||
|
|
f1f96bda4e | ||
|
|
6f38c4efdd | ||
|
|
e325698899 | ||
|
|
ed36d622ec | ||
|
|
dabe1376c3 | ||
|
|
199fd4462e | ||
|
|
85a7776159 | ||
|
|
d7d8ee481e | ||
|
|
875da9fbe5 | ||
|
|
2bd8199d88 | ||
|
|
ca490f3e96 | ||
|
|
b81f2f0675 | ||
|
|
aef23dda13 | ||
|
|
693fa46688 | ||
|
|
30676fb761 | ||
|
|
ac6bdc07ec | ||
|
|
f6afdd6604 | ||
|
|
856037c3c9 | ||
|
|
3203da411d | ||
|
|
a6708a26a6 | ||
|
|
053daa621b | ||
|
|
a16f5fca07 | ||
|
|
cfdb6e2a93 | ||
|
|
73261da19b | ||
|
|
71f48a4f7c | ||
|
|
dbdb805269 | ||
|
|
bd61b8c948 | ||
|
|
5e6a21ddc5 | ||
|
|
ccc8170ec7 | ||
|
|
d4bfbb81d8 | ||
|
|
a46ffa1089 | ||
|
|
182e5a6974 | ||
|
|
8ca021df6a | ||
|
|
106c011f6b | ||
|
|
76664c61c4 | ||
|
|
24839f960f | ||
|
|
ce7d3f8475 | ||
|
|
5e8a6af74c | ||
|
|
23a363aeea | ||
|
|
6cbf2bbada | ||
|
|
ee9cf0a6b6 | ||
|
|
8d39b4aa0d | ||
|
|
3c93ad18b2 | ||
|
|
cc125cc292 | ||
|
|
6823d87198 | ||
|
|
288e857321 | ||
|
|
73d1950d97 | ||
|
|
ee2b047e5d | ||
|
|
ae83fce524 | ||
|
|
985c5ff54b | ||
|
|
fe5f56e98b | ||
|
|
40ef700e5a | ||
|
|
8661c28d10 | ||
|
|
9edddc461d | ||
|
|
2fbb640bc8 | ||
|
|
a03050bc7b | ||
|
|
654a90626e | ||
|
|
9acafed459 | ||
|
|
b7dcb543f6 | ||
|
|
e2768f7f20 | ||
|
|
cda2d0da27 | ||
|
|
c61815db3a | ||
|
|
9390965a0c | ||
|
|
0688feefb1 | ||
|
|
93c8d86caf | ||
|
|
540bff89cf | ||
|
|
41c09b3838 | ||
|
|
0a26361724 | ||
|
|
ee9ad65e18 | ||
|
|
db6b571cfb | ||
|
|
bfe359c440 | ||
|
|
ee8f67793a | ||
|
|
629fe79c61 | ||
|
|
9ae278d622 | ||
|
|
3417d68609 | ||
|
|
f757749282 | ||
|
|
ea40e95cae | ||
|
|
eb066f52fe | ||
|
|
b7007135cb | ||
|
|
a7bd403b2c | ||
|
|
59c7b148dd | ||
|
|
c67f52e960 | ||
|
|
f311625060 | ||
|
|
d3c08f8d90 | ||
|
|
2bc655d7ef | ||
|
|
d2b8d0372e | ||
|
|
40b637b16e | ||
|
|
6e68f399b4 | ||
|
|
0be6c70e92 | ||
|
|
6c2d8fc163 | ||
|
|
a8193b8feb | ||
|
|
34159caf22 | ||
|
|
c75f406459 | ||
|
|
99dca06d44 | ||
|
|
d12c0c4207 | ||
|
|
915a5ed7d5 | ||
|
|
7bfc43c85f | ||
|
|
77ea022ddf | ||
|
|
93578f93f4 | ||
|
|
f129615ebe | ||
|
|
0e5b44baad | ||
|
|
3596475790 | ||
|
|
6218521dea | ||
|
|
65db8b5703 | ||
|
|
f5ff9a3648 | ||
|
|
cbbd50a2e3 | ||
|
|
b04647e65a | ||
|
|
d34d94faa6 | ||
|
|
4038d9560f | ||
|
|
006fc43498 | ||
|
|
47c9b2e1b0 | ||
|
|
dc3e5f0a59 | ||
|
|
01bda83fcd | ||
|
|
9ecb9c68fb | ||
|
|
4612d5180a | ||
|
|
cfb653796c | ||
|
|
d00cd5cb26 | ||
|
|
285a62c87e | ||
|
|
bcb0c6bc77 | ||
|
|
d1ab2d98eb | ||
|
|
c3d5328154 | ||
|
|
fc30588014 | ||
|
|
65b02001b2 | ||
|
|
cd011a172f | ||
|
|
bf913d9eff | ||
|
|
c2dd15fca1 | ||
|
|
b267863b58 | ||
|
|
d189fb100a | ||
|
|
dc6c5bef26 | ||
|
|
7208d51644 | ||
|
|
16359a968d | ||
|
|
d553f77533 | ||
|
|
bc25f5dfdf | ||
|
|
d40028340c | ||
|
|
4194d1cddd | ||
|
|
1fdd532133 | ||
|
|
71c62a3772 | ||
|
|
9be6cd5148 | ||
|
|
c6568969c7 | ||
|
|
f5b1a6ab05 | ||
|
|
5efe659cf5 | ||
|
|
b254fd5ce2 | ||
|
|
631a0ffff4 | ||
|
|
8b11e9a19e | ||
|
|
f6b006b000 | ||
|
|
3a26f420b8 | ||
|
|
0919e415ec | ||
|
|
030a07698d | ||
|
|
a7f2582df7 | ||
|
|
5f0a0e0371 | ||
|
|
28717fd0c7 | ||
|
|
7014ea176a | ||
|
|
b4f2da66be | ||
|
|
b53462cf6e | ||
|
|
8b40364722 | ||
|
|
6ee1824410 | ||
|
|
f63c2da37a | ||
|
|
9be0642ba5 | ||
|
|
55a922c7b3 | ||
|
|
50893929d6 | ||
|
|
03c94e791a | ||
|
|
96bb554813 | ||
|
|
1bc77de144 | ||
|
|
aa07c78fc8 | ||
|
|
52dda88d40 | ||
|
|
c555b309bd | ||
|
|
0a51225762 | ||
|
|
fab49b1dda | ||
|
|
57e422f2d3 | ||
|
|
50a1a3147e | ||
|
|
277115a30f | ||
|
|
7464de3adc | ||
|
|
3d725ddeef | ||
|
|
38d8f289e4 | ||
|
|
edfd6e6de2 | ||
|
|
a68ab6512e | ||
|
|
8383b88a44 | ||
|
|
27ff24f44e | ||
|
|
b111ecb227 | ||
|
|
ac17952cd3 | ||
|
|
e24978fdd7 | ||
|
|
a4d7579e3f | ||
|
|
52171b794a | ||
|
|
3c33f02e9d | ||
|
|
0a8823c40b | ||
|
|
a3f7e71638 | ||
|
|
7ebf4fb9ce | ||
|
|
c96bad3cdf | ||
|
|
0968c43f61 | ||
|
|
ae147c76ff | ||
|
|
0e916a2804 | ||
|
|
494565e131 | ||
|
|
c4430e1a6c | ||
|
|
26adfa11bf | ||
|
|
69ec57669e | ||
|
|
3556133585 | ||
|
|
a65181757d | ||
|
|
42d39a830e | ||
|
|
2e70c9617c | ||
|
|
6230bf94c5 | ||
|
|
47832ececb | ||
|
|
60e6003485 | ||
|
|
9133f289b4 | ||
|
|
76570e2f1b | ||
|
|
c9234a4b49 | ||
|
|
c1361fadda | ||
|
|
ec7af94f71 | ||
|
|
81690d6ce9 | ||
|
|
236b57864b | ||
|
|
22259ec34d | ||
|
|
5a4700753a | ||
|
|
cc862741dc | ||
|
|
779b32e8ad | ||
|
|
e3ce3bcfbe | ||
|
|
673053f181 | ||
|
|
b6eb77ae63 | ||
|
|
0e63255a7f | ||
|
|
f42408a363 | ||
|
|
897fc51ce3 | ||
|
|
6848b126c5 | ||
|
|
0ed9afd1bd | ||
|
|
26cca8298f | ||
|
|
58407af2ba | ||
|
|
3a0473a74f | ||
|
|
6e5124fe22 | ||
|
|
02bd022c62 | ||
|
|
f2538884ea | ||
|
|
8d121d4056 | ||
|
|
96438604ee | ||
|
|
63ccd675d0 | ||
|
|
2c08145c40 | ||
|
|
12effb5738 | ||
|
|
91bfb989be | ||
|
|
192de79fea | ||
|
|
82063f1b21 | ||
|
|
02b263439b | ||
|
|
7efaf3bb32 | ||
|
|
7dd5b082cf | ||
|
|
6320eaa3ac | ||
|
|
85b88b6b61 | ||
|
|
f285665f90 | ||
|
|
8f4399dc2f | ||
|
|
c8b8cc578d | ||
|
|
a142f52113 | ||
|
|
aa666a9662 | ||
|
|
0a4ac6abb7 | ||
|
|
96b0cb8aa0 | ||
|
|
b3a30720fa | ||
|
|
b711605bdc | ||
|
|
31efee2e97 | ||
|
|
569af135bd | ||
|
|
2975a0eaf9 | ||
|
|
d4ee87f324 | ||
|
|
c676a3037c | ||
|
|
e4790062c8 | ||
|
|
bb8a6982d0 | ||
|
|
80af98111b | ||
|
|
9a69d20949 | ||
|
|
5e52996a9e | ||
|
|
33d22d4970 | ||
|
|
170473fb2d | ||
|
|
67ccaea41e | ||
|
|
67d7e81ffa | ||
|
|
1788b40431 | ||
|
|
7f432cefb9 | ||
|
|
57e8c9c7cd | ||
|
|
c1b63af5f5 | ||
|
|
cf7f245a49 | ||
|
|
4824f30950 | ||
|
|
88fb1d8e62 | ||
|
|
e67ce9a438 | ||
|
|
976b9690d2 | ||
|
|
36735ace50 | ||
|
|
3aeea13526 | ||
|
|
6f33c3f5d6 | ||
|
|
53aab1ed0f | ||
|
|
b209040978 | ||
|
|
e74aeb9393 | ||
|
|
e53242613b | ||
|
|
bea7ba00f0 | ||
|
|
24d90b93e2 | ||
|
|
f380b0433d | ||
|
|
f7df6408ed | ||
|
|
10a77ee2a9 | ||
|
|
d5db894891 | ||
|
|
5a44076859 | ||
|
|
e78513cb80 | ||
|
|
2860c4cbe6 | ||
|
|
ebce9fa596 | ||
|
|
8080d0bb4e | ||
|
|
221e42d02b | ||
|
|
e06fd21a4b | ||
|
|
f42036c104 | ||
|
|
937bc4ead3 | ||
|
|
322a855ba2 | ||
|
|
7c4d537d67 | ||
|
|
b78e4240cb | ||
|
|
4f663dd761 | ||
|
|
b3bd5aded5 | ||
|
|
7714c53085 | ||
|
|
3a74cdc98b | ||
|
|
3631f511d4 | ||
|
|
5f7d528d9d | ||
|
|
85ceb8b938 | ||
|
|
b4b268a4d7 | ||
|
|
4b39f13fa9 | ||
|
|
4abcec08f4 | ||
|
|
4144f92631 | ||
|
|
fb8d759103 | ||
|
|
e215cda700 | ||
|
|
846fdcf145 | ||
|
|
ecdabc668d | ||
|
|
e8839974d4 | ||
|
|
2a864b6617 | ||
|
|
ada88a1c02 | ||
|
|
8fe16416f9 | ||
|
|
0daf06c06d | ||
|
|
3b697e7400 | ||
|
|
a543f8716b | ||
|
|
63703a029f | ||
|
|
22415e6c61 | ||
|
|
1a69e76fe7 | ||
|
|
7f916c4770 | ||
|
|
f76d36a74b | ||
|
|
ab0539a263 | ||
|
|
4104dea68e | ||
|
|
5aded9daa3 | ||
|
|
91d5bd80ff | ||
|
|
40d56a0155 | ||
|
|
54117fe51a | ||
|
|
fbd662e400 | ||
|
|
ccb31a81f8 | ||
|
|
dbb9366de6 | ||
|
|
6d7a4edae3 | ||
|
|
632068a74c | ||
|
|
4e78920f99 | ||
|
|
fdc85bbcbf | ||
|
|
67dafae9d6 | ||
|
|
989e5a5f9d | ||
|
|
a7e5bd0b80 | ||
|
|
da131746be | ||
|
|
8a7e80fe86 | ||
|
|
865dc61cd1 | ||
|
|
c8b96a8bce | ||
|
|
5546dbaa0e | ||
|
|
fd6312408b | ||
|
|
4f4c6de8a2 | ||
|
|
4506ba8cd3 | ||
|
|
9300e9fd9a | ||
|
|
a4eb8317da | ||
|
|
0e819de1bc | ||
|
|
9800f9e3da | ||
|
|
a0f6a17005 | ||
|
|
6087c7fed0 | ||
|
|
3fa0b472d2 | ||
|
|
1ce96ddae6 | ||
|
|
d4ef140c8e | ||
|
|
7de575e236 | ||
|
|
f0f0883a88 | ||
|
|
c1695a78d6 | ||
|
|
15e37eded3 | ||
|
|
57cd91e6d4 | ||
|
|
a178334d8e | ||
|
|
dd8ba7e8da | ||
|
|
d26df91960 | ||
|
|
f249763d41 | ||
|
|
1bada9ab30 | ||
|
|
a185ab2973 | ||
|
|
ce83bef0ed | ||
|
|
66135d8222 | ||
|
|
e99e069e55 | ||
|
|
327cdbc873 | ||
|
|
6eabba9679 | ||
|
|
c99f6cfcf2 | ||
|
|
0579a73c1c | ||
|
|
12b3c14299 | ||
|
|
c21da4292b | ||
|
|
3f9cdfd887 | ||
|
|
8dac2bc444 | ||
|
|
13ec6ec323 | ||
|
|
59aa854470 | ||
|
|
e0c3ea4456 | ||
|
|
d6a3727713 | ||
|
|
48cd32742c | ||
|
|
852ceba828 | ||
|
|
905c8be6eb | ||
|
|
fad98dcc9d | ||
|
|
8b5409de5a | ||
|
|
bcf1ad0870 | ||
|
|
617e88e0b5 | ||
|
|
c9e0840173 | ||
|
|
5e4f99da6c | ||
|
|
28bc303fcf | ||
|
|
91c63f281b | ||
|
|
7b3769b1db | ||
|
|
211f9f0c15 | ||
|
|
37ccf4dacb | ||
|
|
971167cad3 | ||
|
|
332bed3136 | ||
|
|
e2a9906de0 | ||
|
|
c5879e4376 | ||
|
|
22324f4c16 | ||
|
|
fa9c3b4f2f | ||
|
|
f411f17386 | ||
|
|
ab3a82300c | ||
|
|
dbb9162267 | ||
|
|
84d3e4f617 | ||
|
|
6209b53321 | ||
|
|
62a2bcf71d | ||
|
|
cdefe9d4a7 | ||
|
|
2bebad1112 | ||
|
|
9f186f1e9c | ||
|
|
a6873302f3 | ||
|
|
615f40d458 | ||
|
|
c4b49b34b8 | ||
|
|
d12fb47902 | ||
|
|
21132f475a | ||
|
|
a55e0a457d | ||
|
|
fa7b049316 | ||
|
|
f8f5bc476b | ||
|
|
e5c622cb89 | ||
|
|
aa70c41041 | ||
|
|
3cce9b67d4 | ||
|
|
c0342ea6d1 | ||
|
|
2813f4c062 | ||
|
|
9ca9de0d7e | ||
|
|
603db9de7f | ||
|
|
c4e72507e0 | ||
|
|
d06d1c177c | ||
|
|
4ff1cf68fc | ||
|
|
8d92933e43 | ||
|
|
9497fb1bb6 | ||
|
|
fc65d56977 | ||
|
|
fe4cba61e2 | ||
|
|
8144bea613 | ||
|
|
7a431d86d2 | ||
|
|
7ecfd86ffa | ||
|
|
66b9e7e9b3 | ||
|
|
6bed109f97 | ||
|
|
fbc5eed5c5 | ||
|
|
43b665652e | ||
|
|
5760d3ef0f | ||
|
|
e712b99287 | ||
|
|
85d88b6800 | ||
|
|
1a62a47935 | ||
|
|
689d842d58 | ||
|
|
8215e73a95 | ||
|
|
d3fc274f08 | ||
|
|
e4bcb8b518 | ||
|
|
9a942c483d | ||
|
|
e9fd223808 | ||
|
|
5dfc66fc21 | ||
|
|
bab82dc290 | ||
|
|
4c1125b9e1 | ||
|
|
85ef2d9687 | ||
|
|
b7fc815d58 | ||
|
|
ad1cc964c9 | ||
|
|
96a2b250a3 | ||
|
|
d72ab58f98 | ||
|
|
abd453f2f6 | ||
|
|
4b7283dbe8 | ||
|
|
f5a068fffc | ||
|
|
faf5dec08a | ||
|
|
e4070e249d | ||
|
|
fe5ec9677a | ||
|
|
5fdd5adef8 | ||
|
|
bc250ad4b8 | ||
|
|
22050b7488 | ||
|
|
6df906aa24 | ||
|
|
d2f20128bb | ||
|
|
cef19488d2 | ||
|
|
0ceb2de79d | ||
|
|
79feaaf801 | ||
|
|
34c005001d | ||
|
|
3508d7a472 | ||
|
|
1e58587b1c | ||
|
|
e99f952c28 | ||
|
|
b0742021b6 | ||
|
|
5e784f64a6 | ||
|
|
32aae08ef2 | ||
|
|
af46fc501b | ||
|
|
07c3a958fa | ||
|
|
ecdbf8765f | ||
|
|
5682943c24 | ||
|
|
5fbc5d3164 | ||
|
|
90af11a842 | ||
|
|
b644a65f22 | ||
|
|
6db82c8176 | ||
|
|
fa149fcaa9 | ||
|
|
9772e4b48a | ||
|
|
08cc4a4815 | ||
|
|
aa554627fb | ||
|
|
420e1a9d63 | ||
|
|
b8ed8566ee | ||
|
|
bbb3cee927 | ||
|
|
2737fa4697 | ||
|
|
14913ae312 | ||
|
|
d2fe6217a6 | ||
|
|
aef46d4c76 | ||
|
|
ad494b86e3 | ||
|
|
c0a594655d | ||
|
|
ed4afa63f0 | ||
|
|
08fccf4adc | ||
|
|
a52e9e152d | ||
|
|
38befc94ca | ||
|
|
df4c8cc352 | ||
|
|
2305d30d35 | ||
|
|
cdc7e671ce | ||
|
|
d541397594 | ||
|
|
4205b68b8c | ||
|
|
6ffb458ffc | ||
|
|
e43dd53e4f | ||
|
|
5ef4516e3d | ||
|
|
51b1d78d8a | ||
|
|
3fdcbf92b6 | ||
|
|
742875d5eb | ||
|
|
4a79eeb9df | ||
|
|
43cee0eb4a | ||
|
|
92beb20455 | ||
|
|
930f55b080 | ||
|
|
e7239b3c5c | ||
|
|
1e92ca5518 | ||
|
|
c9e3e88443 | ||
|
|
05f20eb761 | ||
|
|
a9033c610b | ||
|
|
e9e1cd9028 | ||
|
|
4e8239377f | ||
|
|
83a501b69f | ||
|
|
efe80ce0eb | ||
|
|
e0fc1c5ef7 | ||
|
|
a38ddf83f2 | ||
|
|
e7fbe8bb49 | ||
|
|
523166da48 | ||
|
|
a24990e06a | ||
|
|
3f11451f3c | ||
|
|
0b888b9597 | ||
|
|
acddde8faa | ||
|
|
c587a50505 | ||
|
|
7befd4277e | ||
|
|
bbc29d5d5f | ||
|
|
9dc7c08b61 | ||
|
|
0f690a99df | ||
|
|
d8c8cca6fc | ||
|
|
9bccc99228 | ||
|
|
4f1016a939 | ||
|
|
71fb50b4ce | ||
|
|
7359b04001 | ||
|
|
5700b97e98 | ||
|
|
19e50be276 | ||
|
|
14595d3a60 | ||
|
|
eea9d1f3ce | ||
|
|
157147ded7 | ||
|
|
b8d51c6462 | ||
|
|
7fe3436819 | ||
|
|
a2cd57641b | ||
|
|
80bfc2c86f | ||
|
|
e3bd895908 | ||
|
|
018ff205f5 | ||
|
|
9fee5c61a6 | ||
|
|
ba8e7b3fbb | ||
|
|
dbe53481f1 | ||
|
|
8b9cfbcf29 | ||
|
|
db67d662d1 | ||
|
|
dcf947c498 | ||
|
|
c94025013b | ||
|
|
304b22b1c1 | ||
|
|
f23ece5e9e | ||
|
|
651194f12e | ||
|
|
ae00f49b30 | ||
|
|
60f5341a9a | ||
|
|
3b17ebcb09 | ||
|
|
c861f49e29 | ||
|
|
1d2f50c896 | ||
|
|
4f676c0ccb | ||
|
|
5801d0fc14 | ||
|
|
525ac41258 | ||
|
|
39a0d001ef | ||
|
|
0e9c1c19ce | ||
|
|
0011e04823 | ||
|
|
e919170ee8 | ||
|
|
8024edc1b0 | ||
|
|
984c68dc09 | ||
|
|
c35a177ac1 | ||
|
|
ea82e2dbfe | ||
|
|
034bd7dcb8 | ||
|
|
343905362c | ||
|
|
79d5b70364 | ||
|
|
11a2fcf1f4 | ||
|
|
7c16f9f134 | ||
|
|
fb0ef19158 | ||
|
|
3c7b7e021f | ||
|
|
a38fa4625f | ||
|
|
46376121d0 | ||
|
|
7b2b026bad | ||
|
|
9796e104a5 | ||
|
|
0bf3020db7 | ||
|
|
12e8f81a58 | ||
|
|
f073e61965 | ||
|
|
85e6066292 | ||
|
|
f57d3cf02c | ||
|
|
aa03c00dd5 | ||
|
|
1a3c4640ec | ||
|
|
29d8396910 | ||
|
|
d1e38910ef | ||
|
|
0c4abb5db3 | ||
|
|
6f7b44cb08 | ||
|
|
3a2c40a43e | ||
|
|
288e265aaa | ||
|
|
42dec0464e | ||
|
|
5b09a111cd | ||
|
|
769e2b0223 | ||
|
|
1fed482025 | ||
|
|
46705dd55f | ||
|
|
c11f946979 | ||
|
|
e928ff1fce | ||
|
|
333d4517b5 | ||
|
|
d64d06a70a | ||
|
|
39834f507c | ||
|
|
5199609d54 | ||
|
|
53a5a33fa1 | ||
|
|
a397908bd4 | ||
|
|
bd9e0bba9c | ||
|
|
7179efd3bd | ||
|
|
de0bca2076 | ||
|
|
5f3f350e1b | ||
|
|
7eeacc59db | ||
|
|
d6d96d2d2b | ||
|
|
bb39b7db89 | ||
|
|
706fd0d588 | ||
|
|
97fc99c5e4 | ||
|
|
666767539d | ||
|
|
28234f64ec | ||
|
|
80bede367d | ||
|
|
6722051e82 | ||
|
|
c9fc9916b2 | ||
|
|
9ff7160870 | ||
|
|
9eec44249c | ||
|
|
146fb3321a | ||
|
|
05fcb5cb0c | ||
|
|
09cf62cadd | ||
|
|
632e113d63 | ||
|
|
c4743d3b26 | ||
|
|
10960eec59 | ||
|
|
b3c79be4e7 | ||
|
|
efe6c99199 | ||
|
|
521a0dbec6 | ||
|
|
916997d92a | ||
|
|
0af07058df | ||
|
|
84c98dd5c1 | ||
|
|
a229c12baf | ||
|
|
e0ecd0ad0a | ||
|
|
96ca57509f | ||
|
|
e9e090fdf6 | ||
|
|
6af0922542 | ||
|
|
1bb400b7dc | ||
|
|
bbb4550f10 | ||
|
|
96b2af9cc0 | ||
|
|
e47f601a51 | ||
|
|
af33f473b5 | ||
|
|
c73f1c0061 | ||
|
|
b5d63dfd12 | ||
|
|
952836e1f1 | ||
|
|
4912f97461 | ||
|
|
e2a0b7a033 | ||
|
|
0e12a08076 | ||
|
|
3ae3acf705 | ||
|
|
b8770433d7 | ||
|
|
7030148a76 | ||
|
|
4ed09dddcf | ||
|
|
aba74681ef | ||
|
|
c768395094 | ||
|
|
d7620bf4ff | ||
|
|
3a9adfa089 | ||
|
|
c8bc67e7bf | ||
|
|
7c72003b91 | ||
|
|
6a4392266d | ||
|
|
5d1b805b93 | ||
|
|
dffd860d5c | ||
|
|
8d8777ba95 | ||
|
|
00e255a4a8 | ||
|
|
bba40830fb | ||
|
|
a3974195c2 | ||
|
|
7c4c26e86e | ||
|
|
1be70fd8f2 | ||
|
|
84e3b22357 | ||
|
|
a3c509da83 | ||
|
|
9944dc4693 | ||
|
|
5b1fc8af84 | ||
|
|
ee708d1d1b | ||
|
|
d0df50705a | ||
|
|
cb2b762a6f | ||
|
|
3971c63dda | ||
|
|
3fe9c60480 | ||
|
|
00050c9c6b | ||
|
|
afc0fd78c8 | ||
|
|
ac46e1ca22 | ||
|
|
9efdeb6b97 | ||
|
|
30ac03e0cc | ||
|
|
89001bec0a | ||
|
|
c6b9dfff22 | ||
|
|
b8aa32dcaa | ||
|
|
c589d5e0a5 | ||
|
|
1b090c1c6e | ||
|
|
40474319e3 | ||
|
|
b79fdbd7e0 | ||
|
|
f3f65fa99e | ||
|
|
6a9408a8bc | ||
|
|
2b88764c7e | ||
|
|
4a75844c98 | ||
|
|
9d9a3ebe54 | ||
|
|
79b9d412c1 | ||
|
|
579cf4ee3b | ||
|
|
554c847ea4 | ||
|
|
b7d9ac7436 | ||
|
|
a951a719d1 | ||
|
|
31b03beb2c | ||
|
|
2b3d5ff223 | ||
|
|
7e5bbb4bb7 | ||
|
|
fcecccc9b8 | ||
|
|
29ef080399 | ||
|
|
459bce93c1 | ||
|
|
0ba161f4c5 | ||
|
|
cfb8736a1f | ||
|
|
9686df4898 | ||
|
|
a613507835 | ||
|
|
bc5046c04d | ||
|
|
77d3ba0c3b | ||
|
|
68bd66089a | ||
|
|
7a1c2c7f2d | ||
|
|
4f7259747c | ||
|
|
e3079ce6e0 | ||
|
|
e26d75c894 | ||
|
|
e176600a5a | ||
|
|
7147db0ef2 | ||
|
|
f0d4aa324e | ||
|
|
6870a29ec9 | ||
|
|
50f044455a | ||
|
|
6cc20ce23f | ||
|
|
defd0d2361 | ||
|
|
f49f73409c | ||
|
|
3d783c59c2 | ||
|
|
7582727753 | ||
|
|
b16b214099 | ||
|
|
a101feff21 | ||
|
|
7eef3e7989 | ||
|
|
d3182f278f | ||
|
|
cf3abdf643 | ||
|
|
b8edd410b5 | ||
|
|
d0122328b7 | ||
|
|
3b2460c5d1 | ||
|
|
41742cd6df | ||
|
|
a8a2badc99 | ||
|
|
423594e20c | ||
|
|
bb251c8aee | ||
|
|
d728e2d7c0 | ||
|
|
6e2e915d95 | ||
|
|
9d7d2b3fda | ||
|
|
600944fc22 | ||
|
|
142de587e4 | ||
|
|
cda1c88434 | ||
|
|
b4bf8304ec | ||
|
|
9ea1fa6df7 | ||
|
|
829ed0a575 | ||
|
|
bbdf4932e3 | ||
|
|
cc76e91e12 | ||
|
|
b59aca0b5c | ||
|
|
dd4d8d8880 | ||
|
|
479e9f83a9 | ||
|
|
a8481e0ad5 | ||
|
|
827dde4d1b | ||
|
|
4a027cec20 | ||
|
|
00fa06a274 | ||
|
|
23cb7ffd6f | ||
|
|
2aab517e0c | ||
|
|
25c2a0d90e | ||
|
|
a603211f1a | ||
|
|
34745fb3e5 | ||
|
|
05680ab152 | ||
|
|
ffb05d5aab | ||
|
|
fe4ab6e9d5 | ||
|
|
1ea764c860 | ||
|
|
be94c816ca | ||
|
|
201c0e086b | ||
|
|
569145196e | ||
|
|
7bae000a28 | ||
|
|
0d57b8a163 | ||
|
|
34820cc395 | ||
|
|
efdc4c5229 | ||
|
|
29c9ba56ff | ||
|
|
e6be45516d | ||
|
|
2b5b522f3e | ||
|
|
4ff310ea8b | ||
|
|
ee4f894933 | ||
|
|
482b9d43dd | ||
|
|
ff64a1a510 | ||
|
|
f328a5a439 | ||
|
|
d418a9e086 | ||
|
|
e87e69fa1d | ||
|
|
5d52cb823f | ||
|
|
5a18eccad5 | ||
|
|
df627b0ad5 | ||
|
|
395ccaad22 | ||
|
|
cc96bbf17e | ||
|
|
a19ee35ca4 | ||
|
|
39211297e2 | ||
|
|
46967f1e00 | ||
|
|
40b52d8f3b | ||
|
|
c5f7073cc1 | ||
|
|
a1a8e8a962 | ||
|
|
3d30f4e6c2 | ||
|
|
c5114203b2 | ||
|
|
1be7e67655 | ||
|
|
b8eab81ad2 | ||
|
|
75242f8844 | ||
|
|
d423c2bd05 | ||
|
|
89260dc751 | ||
|
|
dd7e24850d | ||
|
|
42f805d7cc | ||
|
|
94f1e764bc | ||
|
|
69fd97485d | ||
|
|
68e8c4bb59 | ||
|
|
7c7965a3f7 | ||
|
|
189986dd88 | ||
|
|
b8c2b3a97a | ||
|
|
5b079018e8 | ||
|
|
3a834ae90d | ||
|
|
c9d9afc72b | ||
|
|
dac34c8988 | ||
|
|
40f18764b9 | ||
|
|
5f95c23029 | ||
|
|
026dca2d84 | ||
|
|
11b152cfbb | ||
|
|
af5db70c07 | ||
|
|
b4d718126a | ||
|
|
f73ee2c13b | ||
|
|
aebcca76e4 | ||
|
|
e55fd2cede | ||
|
|
e272c06028 | ||
|
|
ada0de8dbc | ||
|
|
874bf4b051 | ||
|
|
f8e70bd7f7 | ||
|
|
04cb03e0b2 | ||
|
|
df287e7122 | ||
|
|
bef48be571 | ||
|
|
7e77ac1ecb | ||
|
|
f33faf66a6 | ||
|
|
77ecb89533 | ||
|
|
02bd1deb2c | ||
|
|
8476ed2e51 | ||
|
|
a3e32f88b0 | ||
|
|
92f9240da7 | ||
|
|
62cd82a9ec | ||
|
|
97ce98177f | ||
|
|
a614789baf | ||
|
|
a6d9617e7f | ||
|
|
4b62aa04aa | ||
|
|
1ca7e257ee | ||
|
|
24d7300f23 | ||
|
|
04038253cf | ||
|
|
f5fc2301bc | ||
|
|
ce82cc8dd9 | ||
|
|
d7e823dfdd | ||
|
|
bbb69b3ec2 | ||
|
|
8831aa1ce1 | ||
|
|
db2e4edb27 | ||
|
|
fa7a33cb9b | ||
|
|
fd5088836b | ||
|
|
45746684d5 | ||
|
|
d647a9d6a0 | ||
|
|
cbb5ab2ecc | ||
|
|
fc01f00915 | ||
|
|
986e2cf0b4 | ||
|
|
ddc7aecd53 | ||
|
|
bf845208db | ||
|
|
dd582fef65 | ||
|
|
f6fd9a5edf | ||
|
|
fead758660 | ||
|
|
abfcc1020f | ||
|
|
c2215452d6 | ||
|
|
10d68790e7 | ||
|
|
7a47cb9031 | ||
|
|
56da2a5725 | ||
|
|
6185082311 | ||
|
|
7e5f350de5 | ||
|
|
925667d840 | ||
|
|
87669010e3 | ||
|
|
4703b03b82 | ||
|
|
e27ea9e117 | ||
|
|
1df0a1dfb4 | ||
|
|
7b02bc006b | ||
|
|
ccea4b5b1b | ||
|
|
3096a6be79 | ||
|
|
ab23311851 | ||
|
|
035fba3fe5 | ||
|
|
7d388cb10a | ||
|
|
5152cee99e | ||
|
|
1a011fd971 | ||
|
|
220ce21a4a | ||
|
|
01b50c442e | ||
|
|
371c87f5a4 | ||
|
|
155f42dd37 | ||
|
|
9289591ba0 | ||
|
|
99d8559c56 | ||
|
|
0adbdbdf1b | ||
|
|
13560616c8 | ||
|
|
621706b1ff | ||
|
|
a5ed59bd64 | ||
|
|
a6ce767532 | ||
|
|
19fd36b195 | ||
|
|
829612e720 | ||
|
|
9ceb00bf54 | ||
|
|
560562755b | ||
|
|
aebd47e535 | ||
|
|
31f051a39e | ||
|
|
49f27ee4cd | ||
|
|
eabbe82b39 | ||
|
|
01fd5f78e8 | ||
|
|
5869c8b6bc | ||
|
|
5142497c7a | ||
|
|
9adb825a88 | ||
|
|
cde9c819d1 | ||
|
|
14195a2647 | ||
|
|
a35f1505e3 | ||
|
|
c444e2d3fa | ||
|
|
287e3e378d | ||
|
|
41c2dabe26 | ||
|
|
5c00a28920 | ||
|
|
854ed2147d | ||
|
|
dc057b5ca7 | ||
|
|
a9585ed9cc | ||
|
|
36499a36b1 | ||
|
|
ff2f311d43 | ||
|
|
99403fad7d | ||
|
|
29a651b261 | ||
|
|
2e92682df2 | ||
|
|
b667184c35 | ||
|
|
5d57666cd4 | ||
|
|
ee3c3a0497 | ||
|
|
6c84319fca | ||
|
|
a17991b25e | ||
|
|
cfb8818f7b | ||
|
|
d167efb52b | ||
|
|
ad0ce38bc7 | ||
|
|
09dbdf1df7 | ||
|
|
68aef19c2c | ||
|
|
99b6198153 | ||
|
|
76866e6292 | ||
|
|
7393fdf6cf | ||
|
|
0870ccf4d8 | ||
|
|
e8675c9c14 | ||
|
|
1114a74f94 | ||
|
|
3ea7d61bb7 | ||
|
|
f077b41ffc | ||
|
|
ad5cb79d40 | ||
|
|
1fdaecf7b4 | ||
|
|
dd2ff9359b | ||
|
|
c8e3a5ee4c | ||
|
|
5dddf25e5e | ||
|
|
7ad8abce6b | ||
|
|
175864403e | ||
|
|
2082fbc4dd | ||
|
|
9247860b50 | ||
|
|
a4e41ffb24 | ||
|
|
19815415d0 | ||
|
|
c940698933 | ||
|
|
6426c76bce | ||
|
|
04632182b4 | ||
|
|
29bbbd804a | ||
|
|
fd33eb3d1c | ||
|
|
4845e2e6a6 | ||
|
|
414b423311 | ||
|
|
e507c148ca | ||
|
|
ae4be9e08e | ||
|
|
296ae69cf9 | ||
|
|
f613a8cfc6 | ||
|
|
b0369a3af1 | ||
|
|
27d69e90fa | ||
|
|
19a4f63ffa | ||
|
|
a32b15e89c | ||
|
|
644d8e22a1 | ||
|
|
722b3b4788 | ||
|
|
3b97c6ecd9 | ||
|
|
20aa89dd6a | ||
|
|
0d2754ab95 | ||
|
|
010e9c2fbe | ||
|
|
8d4511e2b1 | ||
|
|
77f1869e3c | ||
|
|
ec032b91a3 | ||
|
|
68d9d3a659 | ||
|
|
c4cc7ea18c | ||
|
|
e75fbadd53 | ||
|
|
4957f32d06 | ||
|
|
c2715f9b5e | ||
|
|
d3a3c34287 | ||
|
|
95ec53381b | ||
|
|
8c6f1120e4 | ||
|
|
ea1c2a34e2 | ||
|
|
a233c492e6 | ||
|
|
aea2124d61 | ||
|
|
317970f010 | ||
|
|
b0fda87923 | ||
|
|
73cb7e9397 | ||
|
|
95d3c7dfce | ||
|
|
40d5c1f11e | ||
|
|
847d7f98fc | ||
|
|
3a13b8bf30 | ||
|
|
11e30d5860 | ||
|
|
6f1a0d90b0 | ||
|
|
098269adf6 | ||
|
|
0d63447987 | ||
|
|
edfb32647b | ||
|
|
20496b2edb | ||
|
|
4739539722 | ||
|
|
dba2bdd9e7 | ||
|
|
905662e963 | ||
|
|
ac2dfcfce3 | ||
|
|
fd6fef4292 | ||
|
|
eee7b8f0f1 | ||
|
|
ef88924f36 | ||
|
|
c9378b0eab | ||
|
|
cde96cb8b4 | ||
|
|
ffbd9b1f26 | ||
|
|
59524c78e6 | ||
|
|
1ad0c3a3ec | ||
|
|
e85d4c24d3 | ||
|
|
18280b4ff9 | ||
|
|
e504fb845f | ||
|
|
003186edd9 | ||
|
|
61e958d757 | ||
|
|
2ee349605e | ||
|
|
7d200d9a73 | ||
|
|
0468178c43 | ||
|
|
6f4a94c8d1 | ||
|
|
e83a65ec40 | ||
|
|
53ea6b00ab | ||
|
|
45865c0c18 | ||
|
|
979d8057a4 | ||
|
|
b81f75b794 | ||
|
|
8f7578ab06 | ||
|
|
dd39a8b63f | ||
|
|
7595309dc8 | ||
|
|
93abe72041 | ||
|
|
60c513ea3b | ||
|
|
385abae741 | ||
|
|
a680538f80 | ||
|
|
11571c2045 | ||
|
|
f1d267e2b2 | ||
|
|
003b52fbc9 | ||
|
|
6e2df6fcfc | ||
|
|
cd151667c1 | ||
|
|
1a3ecbb4e9 | ||
|
|
0b1f433fe4 | ||
|
|
7aa44b8623 | ||
|
|
6e68100f86 | ||
|
|
c869e88c40 | ||
|
|
9771e1e741 | ||
|
|
9034a70b8f | ||
|
|
e58ebebb80 | ||
|
|
edab43504e | ||
|
|
900b11280c | ||
|
|
0f449f151a | ||
|
|
8186649a9f | ||
|
|
364ef34248 | ||
|
|
c66714d1f5 | ||
|
|
d42fcca81f | ||
|
|
16d1591675 | ||
|
|
d7b2f93797 | ||
|
|
9203046d97 | ||
|
|
b860a749f3 | ||
|
|
6c689fe55f | ||
|
|
c18fd1ef20 | ||
|
|
7e56389c34 | ||
|
|
28771c38e7 | ||
|
|
de7e74ff19 | ||
|
|
2cf661b54f | ||
|
|
16ea92c970 | ||
|
|
3ad55cf6d5 | ||
|
|
d0951041eb | ||
|
|
150d9f18b4 | ||
|
|
09f48c32c7 | ||
|
|
596594d8b3 | ||
|
|
59d17aa950 | ||
|
|
836d4f8209 | ||
|
|
c13aa8d7a4 | ||
|
|
5fb07b7aa7 | ||
|
|
ee12cc21c1 | ||
|
|
6abfc7a2e2 | ||
|
|
18758421fd | ||
|
|
644a986747 | ||
|
|
ad164f3539 | ||
|
|
d5a7ae5fa3 | ||
|
|
a845b7b892 | ||
|
|
a29414a477 | ||
|
|
1c3ce47dcc | ||
|
|
8f30693473 | ||
|
|
0f5f7c515a | ||
|
|
8e76493cc6 | ||
|
|
8617c72aab | ||
|
|
7f0a150b74 | ||
|
|
a53aaff620 | ||
|
|
cab81c5c04 | ||
|
|
5923b6169a | ||
|
|
0f5eb829c6 | ||
|
|
d87c06de9a | ||
|
|
76e8870738 | ||
|
|
247b9ee0c1 | ||
|
|
80dd76b078 | ||
|
|
9163cee90e | ||
|
|
fe46d63031 | ||
|
|
d14d5a5942 | ||
|
|
7181652f01 | ||
|
|
4024cfb7c7 | ||
|
|
1cf5a24c26 | ||
|
|
06795ca32c | ||
|
|
c4fd5ee60a | ||
|
|
e30b66e5c1 | ||
|
|
9ee8d9b828 | ||
|
|
6107bde666 | ||
|
|
e69a91feb3 | ||
|
|
174df30978 | ||
|
|
8ffac1376a | ||
|
|
433a46bba9 | ||
|
|
ba3d5299a0 | ||
|
|
0aa6911159 | ||
|
|
1cef313689 | ||
|
|
4a198ccd8f | ||
|
|
11e08fea73 | ||
|
|
a5be461021 | ||
|
|
d2e04843a4 | ||
|
|
d0276e63c6 | ||
|
|
2d4fa5735b | ||
|
|
1c367eb687 | ||
|
|
56cf534340 | ||
|
|
01fbc8b39d | ||
|
|
91f3ea7c46 | ||
|
|
d11a89d02c | ||
|
|
b580a9f7ad | ||
|
|
c2db186620 | ||
|
|
679a0002a7 | ||
|
|
81fdcce40f | ||
|
|
8af540f9b2 | ||
|
|
79898d87c0 | ||
|
|
3c400af559 | ||
|
|
c8375b846a | ||
|
|
765f74b619 | ||
|
|
2fcb7fb59b | ||
|
|
d828ed83a5 | ||
|
|
22993283f0 | ||
|
|
69f0d3b2bc | ||
|
|
ab0a5898cb | ||
|
|
60a41cae41 | ||
|
|
6c7f7f9543 | ||
|
|
d571d6e121 | ||
|
|
b7b933c89d | ||
|
|
7e98a78333 | ||
|
|
342b9d6fc6 | ||
|
|
ef7ed58a22 | ||
|
|
160c736b38 | ||
|
|
01bb72523e | ||
|
|
ec62d7be5a | ||
|
|
f8cbd31f61 | ||
|
|
8c04a432a8 | ||
|
|
3a9001e091 | ||
|
|
a172909ddf | ||
|
|
54d695f851 | ||
|
|
0147e7f1e1 | ||
|
|
86886ded16 | ||
|
|
6c44abded9 | ||
|
|
f3fb777924 | ||
|
|
1323bba420 | ||
|
|
e55a1d8713 | ||
|
|
d951a6057d | ||
|
|
9755c59687 | ||
|
|
2918c55fa9 | ||
|
|
f01f5d7837 | ||
|
|
01313b16e1 | ||
|
|
601d037201 | ||
|
|
9bc56f5d17 | ||
|
|
8237cd21ed | ||
|
|
bb3f1f2c10 | ||
|
|
f8463823d9 | ||
|
|
08c46fd66e | ||
|
|
1939d42d09 | ||
|
|
9a0a04ed76 | ||
|
|
813a49bf67 | ||
|
|
77924ff248 | ||
|
|
9dad51fa0b | ||
|
|
8c39a81644 | ||
|
|
7749fac683 | ||
|
|
91f4b2fd9d | ||
|
|
e0a3259765 | ||
|
|
38c1a768fc | ||
|
|
3c71af064c | ||
|
|
7841f54db8 | ||
|
|
936dee9da5 | ||
|
|
a10d5ee43b | ||
|
|
a0b8529606 | ||
|
|
6bf1eb5bde | ||
|
|
fb1b5969f5 | ||
|
|
8eca06fd5f | ||
|
|
3c8642f30f | ||
|
|
3796a3dbde | ||
|
|
637104643c | ||
|
|
9364cfe1cf | ||
|
|
4ae66ee3aa | ||
|
|
a6534518f8 | ||
|
|
e327311477 | ||
|
|
78c766c52e | ||
|
|
3c89ecb2c7 | ||
|
|
948972184e | ||
|
|
8874a3fec7 | ||
|
|
dde28136e1 | ||
|
|
ce03296078 | ||
|
|
457efa1c79 | ||
|
|
cfd1fc275b | ||
|
|
9d56dd122f | ||
|
|
b1e35e6824 | ||
|
|
775fdec0b8 | ||
|
|
f403014ef6 | ||
|
|
5982424864 | ||
|
|
2ad5ccabec | ||
|
|
9ffc3520e7 | ||
|
|
7ba28a9770 | ||
|
|
bec72dfd5f | ||
|
|
cf6b4f5432 | ||
|
|
fe82c44687 | ||
|
|
49e4f15bd1 | ||
|
|
7dd85b2ba6 | ||
|
|
7b64793449 | ||
|
|
5485f2013e | ||
|
|
6ed24ec310 | ||
|
|
e39335864d | ||
|
|
27ec2c276a | ||
|
|
b2f8da500b | ||
|
|
80a21dbf4a | ||
|
|
33bd39859d | ||
|
|
7367a769b1 | ||
|
|
1ca4c67f7e | ||
|
|
120a67159d | ||
|
|
aa808587df | ||
|
|
57df991dd3 | ||
|
|
665184bfa2 | ||
|
|
ceee696443 | ||
|
|
7fda376f19 | ||
|
|
bd60cb3a18 | ||
|
|
534ed6ae6c | ||
|
|
acac93bbd1 | ||
|
|
070e4b8527 | ||
|
|
a4e8761add | ||
|
|
590b76a884 | ||
|
|
98729cfa7b | ||
|
|
2bd077136f | ||
|
|
6fba94594b | ||
|
|
24e9ff4c86 | ||
|
|
379357ee8e | ||
|
|
ef7a64644b | ||
|
|
c163c20c3d | ||
|
|
3ddbeac419 | ||
|
|
ef7a2ec123 | ||
|
|
8dd4cfa6b2 | ||
|
|
7491a6faac | ||
|
|
eaf1a59e89 | ||
|
|
4cbc5040a2 | ||
|
|
b90b129d98 | ||
|
|
64c0ae2734 | ||
|
|
1c2a7a219a | ||
|
|
868b8e7206 | ||
|
|
6115828dfe | ||
|
|
af9b5ab014 | ||
|
|
5497bd97b2 | ||
|
|
c53db63cc7 | ||
|
|
1263603c7b | ||
|
|
d1036c3be7 | ||
|
|
5adbd6e8f1 | ||
|
|
b83ce02849 | ||
|
|
735a9c8a5d | ||
|
|
bf46881e3b | ||
|
|
f68226f4b4 | ||
|
|
07d864e48b | ||
|
|
0326864510 | ||
|
|
2a97060b96 | ||
|
|
6cffd81540 | ||
|
|
eb2b11ed3e | ||
|
|
7cb52bebf5 | ||
|
|
3731f5d32c | ||
|
|
7161b14631 | ||
|
|
2e7ff88be6 | ||
|
|
6368ee5c12 | ||
|
|
42106e907d | ||
|
|
3f71162e9d | ||
|
|
008d8de24e | ||
|
|
e3000dcc7c | ||
|
|
f98cf4cbfc | ||
|
|
807f885f92 | ||
|
|
15ecd8d592 | ||
|
|
588a6f1bf4 | ||
|
|
f633bc81cf | ||
|
|
3006dc6584 | ||
|
|
a7c63e22e5 | ||
|
|
4304158088 | ||
|
|
0f446a1f77 | ||
|
|
71639a91b9 | ||
|
|
c1fb67f143 | ||
|
|
b0395a6ed2 | ||
|
|
7d0e6ac50b | ||
|
|
c6ea524821 | ||
|
|
7c82769448 | ||
|
|
a4b69c3911 | ||
|
|
e301abe9e3 | ||
|
|
d38ccba618 | ||
|
|
849ac56296 | ||
|
|
67bb821a0e | ||
|
|
43c51d48d9 | ||
|
|
48b7f4924e | ||
|
|
30c149be31 | ||
|
|
42d9271ea0 | ||
|
|
00e666e423 | ||
|
|
6b29b686c7 | ||
|
|
db99a21514 | ||
|
|
6fb1c474d2 | ||
|
|
f1c7c35e48 | ||
|
|
923549bb5e | ||
|
|
0aa18ded72 | ||
|
|
ee08d8d740 | ||
|
|
0c23fa7c9d | ||
|
|
fe91765ab0 | ||
|
|
8541e180cc | ||
|
|
a3d950e2a3 | ||
|
|
e1c80636ba | ||
|
|
4fb971a935 | ||
|
|
117d0fbcef | ||
|
|
389cafc240 | ||
|
|
4591b59465 | ||
|
|
d4a082382d | ||
|
|
885437de8d | ||
|
|
39cd9f4a44 | ||
|
|
01a2244fed | ||
|
|
e0e3dd128e | ||
|
|
0f6408d7f6 | ||
|
|
b610dc4969 | ||
|
|
bc6fa7fd27 | ||
|
|
0e34cc49df | ||
|
|
629881d16b | ||
|
|
92569f5b3a | ||
|
|
4ff32511b9 | ||
|
|
5afed4b85e | ||
|
|
2aa687a40b | ||
|
|
57567ebb1b | ||
|
|
6448169caa | ||
|
|
e5901f28e3 | ||
|
|
2d3f0dd95a | ||
|
|
2e4e827887 | ||
|
|
22704b32d6 | ||
|
|
2839e595cc | ||
|
|
a9f28dfb47 | ||
|
|
c1bdd079cf | ||
|
|
467d05a358 | ||
|
|
05c8a6e664 | ||
|
|
59b2ecebda | ||
|
|
fe8cf38a7f | ||
|
|
e754ceace8 | ||
|
|
41ceb9bd82 | ||
|
|
b0b7c63561 | ||
|
|
c13f65611f | ||
|
|
b9feeabfb3 | ||
|
|
8e9ba7a2d2 | ||
|
|
b3e9b1c2be | ||
|
|
e969b5b7e4 | ||
|
|
8e0a684a9a | ||
|
|
152aa0f92b | ||
|
|
97fb0ba95a | ||
|
|
01355b9a28 | ||
|
|
772922cefb | ||
|
|
95a3aa7290 | ||
|
|
6f8a4af5eb | ||
|
|
b332c4e23d | ||
|
|
47c0c3fa3f | ||
|
|
71cb8612d8 | ||
|
|
af12aecd36 | ||
|
|
fec116f131 | ||
|
|
d1fdec0970 | ||
|
|
77b6c53a42 | ||
|
|
f2eaac50a6 | ||
|
|
ec8b0ada14 | ||
|
|
1e28be9671 | ||
|
|
8a0e813734 | ||
|
|
2975d550eb | ||
|
|
248a9c4070 | ||
|
|
327d0d20a9 | ||
|
|
1ffd2812dc | ||
|
|
29da4d209d | ||
|
|
35a369a953 | ||
|
|
4785cae8f0 | ||
|
|
9eaa575d1a | ||
|
|
996ed78a0e | ||
|
|
1759572b2e | ||
|
|
0a9f9eea90 | ||
|
|
223fb540b1 | ||
|
|
c5f1c95f7b | ||
|
|
60086ead7c | ||
|
|
3bbcbca926 | ||
|
|
63c1deb630 | ||
|
|
424e2428fe | ||
|
|
2fdef45156 | ||
|
|
63ac99e906 | ||
|
|
4cd4550a36 | ||
|
|
16af625aef | ||
|
|
8e72794c07 | ||
|
|
d9cf6d7e1b | ||
|
|
57c4f935e2 | ||
|
|
f4e4252227 | ||
|
|
84bbcf7753 | ||
|
|
df129f0d20 | ||
|
|
a76da167eb | ||
|
|
1547b3f53b | ||
|
|
68f5b30f7d | ||
|
|
d9b9c0f42c | ||
|
|
aa0597c27e | ||
|
|
4d59cd1521 | ||
|
|
6ba4170f08 | ||
|
|
827bc97e8e | ||
|
|
7107409b1b | ||
|
|
6c086fab6f | ||
|
|
54d215b8d1 | ||
|
|
4cd47a5c77 | ||
|
|
d027d67e08 | ||
|
|
a7d9e635eb | ||
|
|
e7196efaea | ||
|
|
0a2a903c74 | ||
|
|
8104c26b19 | ||
|
|
420245c630 | ||
|
|
20cf83907d | ||
|
|
0a04d2ee83 | ||
|
|
bc05781edd | ||
|
|
ce9d91e990 | ||
|
|
973d13b705 | ||
|
|
51e4427108 | ||
|
|
d09ab541ab | ||
|
|
a9e1dc3cd5 | ||
|
|
1551e6c640 | ||
|
|
03a1bdfc7f | ||
|
|
24545aa9cb | ||
|
|
c11426419c | ||
|
|
cf7e7c1a06 | ||
|
|
ab388f1ef5 | ||
|
|
101d5c7eb0 | ||
|
|
cad253b85f | ||
|
|
f17009a988 | ||
|
|
1f76278d2b | ||
|
|
e032d29c91 | ||
|
|
31d1b0c994 | ||
|
|
611c6d415c | ||
|
|
e3b7ac00fd | ||
|
|
7c952822db | ||
|
|
b9e435c0e2 | ||
|
|
356d40e640 | ||
|
|
123ffd4e66 | ||
|
|
c952659620 | ||
|
|
ff59f31e84 | ||
|
|
011fc8cc9e | ||
|
|
7428022b2f | ||
|
|
3815eb1983 | ||
|
|
3778f10edb | ||
|
|
00a4b867ea | ||
|
|
3a1024e848 | ||
|
|
ea8e1e9c57 | ||
|
|
478d63893b | ||
|
|
3066631301 | ||
|
|
b0998d5f95 | ||
|
|
a074cb9664 | ||
|
|
43a3d1e5ac | ||
|
|
24c8a4a9d1 | ||
|
|
b5ccba552f | ||
|
|
8d2ee364ba | ||
|
|
14006068c8 | ||
|
|
b4358ffc66 | ||
|
|
33135b1df1 | ||
|
|
9a66c38e01 | ||
|
|
81132ecab0 | ||
|
|
313fc09c8d | ||
|
|
5610486a89 | ||
|
|
9f0231405a | ||
|
|
e650dd0503 | ||
|
|
6fc6cb75e4 | ||
|
|
184efc6f27 | ||
|
|
1117b57101 | ||
|
|
0928ba71f2 | ||
|
|
7170eded68 | ||
|
|
32e891d6ba | ||
|
|
5a273676fb | ||
|
|
c3f6edf7c5 | ||
|
|
e0c0cc8613 | ||
|
|
fbeb3dd81e | ||
|
|
9d89af37be | ||
|
|
fbc8a36232 | ||
|
|
ea028ea1a1 | ||
|
|
35b1c12bb5 | ||
|
|
fc89d96635 | ||
|
|
27129652f2 | ||
|
|
23ef992a7f | ||
|
|
04533e17ec | ||
|
|
7f2fcba542 | ||
|
|
c115e2f985 | ||
|
|
88642c2003 | ||
|
|
2c678b5363 | ||
|
|
bc3b72fafe | ||
|
|
df3b8cf09c | ||
|
|
65393b7809 | ||
|
|
9782c849ad | ||
|
|
e7efaed08a | ||
|
|
337b3e5b5d | ||
|
|
7b1b7d1372 | ||
|
|
e5b838a2b3 | ||
|
|
f9cc2ceb11 | ||
|
|
4f107c5618 | ||
|
|
b1c5aaff43 | ||
|
|
d0a4473e2b | ||
|
|
1c79361094 | ||
|
|
f72114c223 | ||
|
|
dbd59cd958 | ||
|
|
d65f8a3c82 | ||
|
|
96587a4e45 | ||
|
|
76ab47c82e | ||
|
|
af8344c555 | ||
|
|
10ff02b8a0 | ||
|
|
cb6cf1e34b | ||
|
|
953e924aa2 | ||
|
|
2e0c262d32 | ||
|
|
cbf2e6a140 | ||
|
|
49c5a9f621 | ||
|
|
fb8f63f305 | ||
|
|
49ff61ad65 | ||
|
|
695fb60aa4 | ||
|
|
da20fafa39 | ||
|
|
d6a9ecd912 | ||
|
|
f95d721c9c | ||
|
|
69d6417985 | ||
|
|
ab2bbc28c8 | ||
|
|
b0b39429ed | ||
|
|
ff14cbc752 | ||
|
|
dd013aaaa3 | ||
|
|
119f61ef67 | ||
|
|
a99588c766 | ||
|
|
6f35fe9936 | ||
|
|
3112425e43 | ||
|
|
1ab3aefaa5 | ||
|
|
be08732e6b | ||
|
|
97b58b5f9a | ||
|
|
e4855875cf | ||
|
|
7a267cc07b | ||
|
|
f4f351cf9d | ||
|
|
970b811ab9 | ||
|
|
99604dbe35 | ||
|
|
09b1d89718 | ||
|
|
9d1a9f3134 | ||
|
|
ccb889233c | ||
|
|
939f2cbf97 | ||
|
|
580e0cb36a | ||
|
|
4bd8835f23 | ||
|
|
22f32da0c5 | ||
|
|
aa8a094383 | ||
|
|
4a72b7f089 | ||
|
|
a33e4905cf | ||
|
|
40bd2f0742 | ||
|
|
1fd73fe79e | ||
|
|
fd7d3e06f4 | ||
|
|
7a4d27da69 | ||
|
|
afbadf7d81 | ||
|
|
7b65c64431 | ||
|
|
4f2c0e94d9 | ||
|
|
7593d7a3e9 | ||
|
|
35ddc4a472 | ||
|
|
3400d1e803 | ||
|
|
f672428fde | ||
|
|
4171d993d0 | ||
|
|
d22310ea31 | ||
|
|
1521d1e883 | ||
|
|
0412615f6e | ||
|
|
2bd666efe7 | ||
|
|
541e1f760b | ||
|
|
f176bed436 | ||
|
|
403545cd9b | ||
|
|
54e4ed27ae | ||
|
|
e5ddf5616a | ||
|
|
967c4f04d9 | ||
|
|
36d4d445a6 | ||
|
|
5ed0ae2fa9 | ||
|
|
503a719609 | ||
|
|
2dcbba63cb | ||
|
|
5e86bdfda7 | ||
|
|
4f74a0440c | ||
|
|
858709f610 | ||
|
|
6689d48fad | ||
|
|
8581a7c308 | ||
|
|
9878efb198 | ||
|
|
9855c50367 | ||
|
|
04c59041e0 | ||
|
|
7fb1ecc9b0 | ||
|
|
ecd2cdd28e | ||
|
|
4b0ad22f8d | ||
|
|
789558da6c | ||
|
|
3f96117a57 | ||
|
|
282d6b5746 | ||
|
|
86d2bb9e2a | ||
|
|
ab4fbf0437 | ||
|
|
ae527a78ef | ||
|
|
8218868681 | ||
|
|
251f4ca4ac | ||
|
|
5d5c8ced24 | ||
|
|
f0ec9b7826 | ||
|
|
90b8e785a4 | ||
|
|
cb11a71ff5 | ||
|
|
54dd90fc4a | ||
|
|
4bb080bd58 | ||
|
|
5115a048a3 | ||
|
|
259351cdd2 | ||
|
|
972b8f83bc | ||
|
|
9f6b1c1e25 | ||
|
|
6229a103aa | ||
|
|
84116e531b | ||
|
|
2c1b944b7c | ||
|
|
0e66ca148d | ||
|
|
ce70c1ca3a | ||
|
|
56b91a59a2 | ||
|
|
8f1fbcb19e | ||
|
|
a4799a1bc0 | ||
|
|
cc24ff22e9 | ||
|
|
72ca335c4c | ||
|
|
0ef6476e58 | ||
|
|
6ba63d1466 | ||
|
|
f9d28e1b6b | ||
|
|
63534d3eb5 | ||
|
|
2ab2cf01db | ||
|
|
2fc039dd70 | ||
|
|
230d1a1f86 | ||
|
|
3d6df3cc09 | ||
|
|
469f30044a | ||
|
|
0993d87799 | ||
|
|
09fa33236f | ||
|
|
4e9abd6512 | ||
|
|
2996c0b38e | ||
|
|
36e366abe0 | ||
|
|
f9c6c6c127 | ||
|
|
34772ef2bf | ||
|
|
305af935a7 | ||
|
|
d0438390cc | ||
|
|
d22266a947 | ||
|
|
e34fb25759 | ||
|
|
675955b2e6 | ||
|
|
4516bce0ee | ||
|
|
4853fbcec3 | ||
|
|
72e5f9a83e | ||
|
|
1a1ddc34a2 | ||
|
|
40ebfb676c | ||
|
|
19dd16fcf0 | ||
|
|
f596749645 | ||
|
|
b5e3cc2503 | ||
|
|
2e11fe2b58 | ||
|
|
70f1258bab | ||
|
|
d91fa33330 | ||
|
|
320f183b49 | ||
|
|
c8f11578d6 | ||
|
|
f33be18be3 | ||
|
|
513c7dbd85 | ||
|
|
02a9623310 | ||
|
|
58c1d52f52 | ||
|
|
b71c881b43 | ||
|
|
ffc2c7dea3 | ||
|
|
d7652a7a32 |
12
.env.docker
12
.env.docker
@@ -1,19 +1,23 @@
|
||||
APP_NAME=Dootask
|
||||
TIMEZONE=PRC
|
||||
|
||||
APP_NAME=DooTask
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_DEBUG=false
|
||||
APP_SCHEME=auto
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_ID=
|
||||
APP_IPPR=
|
||||
APP_PORT=2222
|
||||
APP_SSL_PORT=
|
||||
APP_DEV_PORT=
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST="${APP_IPPR}.5"
|
||||
DB_HOST=mariadb
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=dootask
|
||||
DB_USERNAME=dootask
|
||||
@@ -30,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
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
TIMEZONE=PRC
|
||||
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
|
||||
61
.github/workflows/electron.yml
vendored
61
.github/workflows/electron.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Create changelog text
|
||||
id: changelog
|
||||
uses: loopwerk/tag-changelog@v1
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
exclude_types: other,chore
|
||||
|
||||
- name: Create release
|
||||
uses: actions/create-release@latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
body: ${{ steps.changelog.outputs.changes }}
|
||||
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
environment: build
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-11]
|
||||
platform: [
|
||||
build-mac,
|
||||
build-mac-arm,
|
||||
build-win
|
||||
]
|
||||
|
||||
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||
EP_PRE_RELEASE: true
|
||||
run: ./cmd electron ${{ matrix.platform }}
|
||||
|
||||
304
.github/workflows/publish.yml
vendored
Normal file
304
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,304 @@
|
||||
name: "Publish"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "pro"
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_release: ${{ steps.check-tag.outputs.should_release }}
|
||||
version: ${{ steps.get-version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get version from package.json
|
||||
id: get-version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check if tag exists
|
||||
id: check-tag
|
||||
run: |
|
||||
VERSION=${{ steps.get-version.outputs.version }}
|
||||
if git ls-remote --tags origin | grep -q "refs/tags/v${VERSION}$"; then
|
||||
echo "This version v${VERSION} has been released"
|
||||
echo "should_release=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Version v${VERSION} has not been released, continue building"
|
||||
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
create-release:
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.result }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Create Release
|
||||
id: create-release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// 获取最新的 tag
|
||||
const { data: tags } = await github.rest.repos.listTags({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 1
|
||||
});
|
||||
|
||||
// 获取提交日志
|
||||
let changelog = '';
|
||||
if (tags.length > 0) {
|
||||
const { data: commits } = await github.rest.repos.compareCommits({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
base: tags[0].name,
|
||||
head: 'HEAD'
|
||||
});
|
||||
|
||||
// 按类型分组提交
|
||||
const groups = {
|
||||
'feat:': { title: '## Features', commits: new Set() },
|
||||
'fix:': { title: '## Bug Fixes', commits: new Set() },
|
||||
'perf:': { title: '## Performance Improvements', commits: new Set() }
|
||||
};
|
||||
|
||||
// 分类收集提交,使用 Set 去重
|
||||
commits.commits.forEach(commit => {
|
||||
const message = commit.commit.message.split('\n')[0].trim();
|
||||
for (const [prefix, group] of Object.entries(groups)) {
|
||||
if (message.startsWith(prefix)) {
|
||||
// 移除前缀后添加到对应分组
|
||||
const cleanMessage = message.slice(prefix.length).trim();
|
||||
group.commits.add(cleanMessage); // 使用 Set.add 自动去重
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 生成更新日志
|
||||
const sections = [];
|
||||
for (const group of Object.values(groups)) {
|
||||
if (group.commits.size > 0) {
|
||||
sections.push(`${group.title}\n\n${Array.from(group.commits).map(msg => `- ${msg}`).join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (sections.length > 0) {
|
||||
changelog = '# Changelog\n\n' + sections.join('\n\n');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 release
|
||||
const { data } = await github.rest.repos.createRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: `v${{ needs.check-version.outputs.version }}`,
|
||||
name: `${{ needs.check-version.outputs.version }}`,
|
||||
body: changelog || 'No significant changes in this release.',
|
||||
draft: true,
|
||||
prerelease: false
|
||||
})
|
||||
return data.id
|
||||
|
||||
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:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: "macos-latest"
|
||||
build_type: "mac"
|
||||
- platform: "ubuntu-latest"
|
||||
build_type: "android"
|
||||
- platform: "windows-latest"
|
||||
build_type: "windows"
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
environment: build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
# Android 构建步骤
|
||||
- name: (Android) Build Js
|
||||
if: matrix.build_type == 'android'
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: |
|
||||
git submodule init
|
||||
git submodule update --remote "resources/mobile"
|
||||
./cmd appbuild publish
|
||||
|
||||
- name: (Android) Setup JDK 11
|
||||
if: matrix.build_type == 'android'
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "11"
|
||||
|
||||
- name: (Android) Build App
|
||||
if: matrix.build_type == 'android'
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 20
|
||||
max_attempts: 5
|
||||
command: |
|
||||
cd resources/mobile/platforms/android/eeuiApp
|
||||
chmod +x ./gradlew
|
||||
./gradlew assembleRelease --quiet
|
||||
|
||||
- name: (Android) Upload File
|
||||
if: matrix.build_type == 'android'
|
||||
env:
|
||||
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
|
||||
run: |
|
||||
node ./electron/build.js android-upload
|
||||
|
||||
- name: (Android) Upload Release
|
||||
if: matrix.build_type == 'android'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const globby = require('globby');
|
||||
|
||||
// 查找 APK 文件
|
||||
const files = await globby('resources/mobile/platforms/android/eeuiApp/app/build/outputs/apk/release/*.apk');
|
||||
|
||||
for (const file of files) {
|
||||
const data = await fs.promises.readFile(file);
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.RELEASE_ID,
|
||||
name: path.basename(file),
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
# Mac 构建步骤
|
||||
- name: (Mac) Build Client
|
||||
if: matrix.build_type == 'mac'
|
||||
env:
|
||||
APPLEID: ${{ secrets.APPLEID }}
|
||||
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
./cmd electron mac
|
||||
|
||||
# Windows 构建步骤
|
||||
- name: (Windows) Build Client
|
||||
if: matrix.build_type == 'windows'
|
||||
env:
|
||||
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
shell: bash
|
||||
run: |
|
||||
./cmd electron win
|
||||
|
||||
publish-release:
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Publish Release
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
with:
|
||||
script: |
|
||||
github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.RELEASE_ID,
|
||||
draft: false,
|
||||
prerelease: false
|
||||
})
|
||||
|
||||
- name: Publish Official
|
||||
env:
|
||||
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
|
||||
run: |
|
||||
pushd electron || exit
|
||||
npm install
|
||||
popd || exit
|
||||
node ./electron/build.js published
|
||||
51
.gitignore
vendored
51
.gitignore
vendored
@@ -1,28 +1,61 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
/vendor
|
||||
|
||||
# Build and temporary files
|
||||
/build
|
||||
/public/hot
|
||||
/public/tmp
|
||||
/tmp
|
||||
|
||||
# Uploads and user-generated content
|
||||
/public/summary
|
||||
/public/uploads/*
|
||||
/public/.well-known
|
||||
/public/.user.ini
|
||||
|
||||
# Storage and configuration
|
||||
/config/LICENSE
|
||||
/storage/*.key
|
||||
/vendor
|
||||
/build
|
||||
/tmp
|
||||
._*
|
||||
|
||||
# Environment and configuration
|
||||
.env
|
||||
vars.yaml
|
||||
|
||||
# IDE and editor files
|
||||
.idea
|
||||
.vscode
|
||||
.windsurfrules
|
||||
|
||||
# Development tools
|
||||
.vagrant
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
|
||||
# Development file
|
||||
/index.html
|
||||
|
||||
# Testing
|
||||
.phpunit.result.cache
|
||||
test.*
|
||||
|
||||
# Logs and debug files
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
test.*
|
||||
composer.lock
|
||||
|
||||
# Lock files
|
||||
dootask.lock
|
||||
package-lock.json
|
||||
|
||||
# Laravel/Swoole specific
|
||||
laravels-timer-process.pid
|
||||
.DS_Store
|
||||
vars.yaml
|
||||
laravels.conf
|
||||
laravels.pid
|
||||
|
||||
# System files
|
||||
._*
|
||||
.DS_Store
|
||||
|
||||
# Documentation
|
||||
AGENTS.md
|
||||
README_LOCAL.md
|
||||
|
||||
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
[submodule "resources/drawio"]
|
||||
path = resources/drawio
|
||||
url = https://github.com/jgraph/drawio.git
|
||||
[submodule "resources/mobile"]
|
||||
path = resources/mobile
|
||||
url = https://github.com/kuaifan/dootask-app.git
|
||||
170
.prefetch
Normal file
170
.prefetch
Normal file
@@ -0,0 +1,170 @@
|
||||
office/web-apps/apps/api/documents/api.js?hash={version}
|
||||
|
||||
office/{path}/fonts/000
|
||||
office/{path}/fonts/001
|
||||
office/{path}/fonts/002
|
||||
office/{path}/fonts/020
|
||||
office/{path}/fonts/022
|
||||
office/{path}/fonts/023
|
||||
office/{path}/fonts/024
|
||||
office/{path}/fonts/027
|
||||
office/{path}/fonts/028
|
||||
office/{path}/fonts/029
|
||||
office/{path}/fonts/030
|
||||
office/{path}/fonts/036
|
||||
office/{path}/fonts/037
|
||||
office/{path}/fonts/038
|
||||
office/{path}/fonts/039
|
||||
office/{path}/fonts/050
|
||||
office/{path}/fonts/051
|
||||
office/{path}/fonts/052
|
||||
office/{path}/fonts/053
|
||||
office/{path}/fonts/058
|
||||
office/{path}/fonts/059
|
||||
office/{path}/fonts/060
|
||||
office/{path}/fonts/061
|
||||
office/{path}/fonts/062
|
||||
office/{path}/fonts/063
|
||||
office/{path}/fonts/064
|
||||
office/{path}/fonts/065
|
||||
office/{path}/fonts/066
|
||||
office/{path}/fonts/067
|
||||
office/{path}/fonts/068
|
||||
office/{path}/fonts/069
|
||||
office/{path}/fonts/070
|
||||
office/{path}/fonts/071
|
||||
office/{path}/fonts/072
|
||||
office/{path}/fonts/073
|
||||
office/{path}/fonts/074
|
||||
office/{path}/fonts/075
|
||||
office/{path}/fonts/076
|
||||
office/{path}/fonts/077
|
||||
office/{path}/fonts/078
|
||||
office/{path}/fonts/079
|
||||
office/{path}/fonts/080
|
||||
office/{path}/fonts/081
|
||||
office/{path}/fonts/086
|
||||
office/{path}/fonts/091
|
||||
office/{path}/fonts/092
|
||||
office/{path}/fonts/093
|
||||
office/{path}/fonts/094
|
||||
office/{path}/fonts/095
|
||||
office/{path}/fonts/096
|
||||
office/{path}/fonts/097
|
||||
office/{path}/fonts/098
|
||||
office/{path}/fonts/099
|
||||
office/{path}/fonts/100
|
||||
office/{path}/fonts/101
|
||||
office/{path}/fonts/102
|
||||
office/{path}/fonts/103
|
||||
office/{path}/fonts/131
|
||||
office/{path}/fonts/132
|
||||
office/{path}/fonts/133
|
||||
office/{path}/fonts/134
|
||||
office/{path}/fonts/135
|
||||
office/{path}/fonts/136
|
||||
office/{path}/fonts/137
|
||||
office/{path}/fonts/138
|
||||
office/{path}/fonts/139
|
||||
office/{path}/fonts/140
|
||||
office/{path}/fonts/141
|
||||
office/{path}/fonts/142
|
||||
office/{path}/fonts/143
|
||||
office/{path}/fonts/145
|
||||
office/{path}/fonts/147
|
||||
office/{path}/fonts/152
|
||||
office/{path}/fonts/154
|
||||
office/{path}/fonts/177
|
||||
office/{path}/fonts/178
|
||||
office/{path}/fonts/179
|
||||
office/{path}/fonts/180
|
||||
office/{path}/fonts/181
|
||||
office/{path}/fonts/182
|
||||
office/{path}/fonts/183
|
||||
office/{path}/fonts/184
|
||||
office/{path}/fonts/185
|
||||
office/{path}/fonts/186
|
||||
office/{path}/fonts/187
|
||||
office/{path}/fonts/188
|
||||
office/{path}/fonts/189
|
||||
office/{path}/fonts/190
|
||||
office/{path}/fonts/191
|
||||
office/{path}/fonts/192
|
||||
office/{path}/fonts/193
|
||||
office/{path}/fonts/198
|
||||
office/{path}/fonts/199
|
||||
office/{path}/fonts/200
|
||||
office/{path}/fonts/201
|
||||
office/{path}/fonts/202
|
||||
office/{path}/fonts/203
|
||||
office/{path}/fonts/204
|
||||
office/{path}/fonts/205
|
||||
office/{path}/fonts/206
|
||||
office/{path}/fonts/207
|
||||
office/{path}/fonts/208
|
||||
office/{path}/fonts/209
|
||||
office/{path}/fonts/210
|
||||
office/{path}/fonts/211
|
||||
office/{path}/fonts/212
|
||||
office/{path}/fonts/214
|
||||
office/{path}/fonts/215
|
||||
office/{path}/fonts/216
|
||||
office/{path}/fonts/217
|
||||
office/{path}/sdkjs/cell/sdk-all-min.js
|
||||
office/{path}/sdkjs/cell/sdk-all.js
|
||||
office/{path}/sdkjs/common/AllFonts.js
|
||||
office/{path}/sdkjs/common/AllFonts.js
|
||||
office/{path}/sdkjs/common/AllFonts.js
|
||||
office/{path}/sdkjs/common/Charts/ChartStyles.js
|
||||
office/{path}/sdkjs/common/Charts/ChartStyles.js
|
||||
office/{path}/sdkjs/common/Charts/ChartStyles.js
|
||||
office/{path}/sdkjs/common/Images/fonts_thumbnail_ea@2x.png.bin
|
||||
office/{path}/sdkjs/common/Images/fonts_thumbnail_ea@2x.png.bin
|
||||
office/{path}/sdkjs/common/Images/fonts_thumbnail_ea@2x.png.bin
|
||||
office/{path}/sdkjs/common/libfont/engine/fonts.js
|
||||
office/{path}/sdkjs/common/libfont/engine/fonts.js
|
||||
office/{path}/sdkjs/common/libfont/engine/fonts.js
|
||||
office/{path}/sdkjs/common/libfont/engine/fonts.wasm
|
||||
office/{path}/sdkjs/common/libfont/engine/fonts.wasm
|
||||
office/{path}/sdkjs/common/libfont/engine/fonts.wasm
|
||||
office/{path}/sdkjs/slide/sdk-all-min.js
|
||||
office/{path}/sdkjs/slide/sdk-all.js
|
||||
office/{path}/sdkjs/word/sdk-all-min.js
|
||||
office/{path}/sdkjs/word/sdk-all.js
|
||||
office/{path}/web-apps/apps/documenteditor/main/app.js
|
||||
office/{path}/web-apps/apps/documenteditor/main/code.js
|
||||
office/{path}/web-apps/apps/documenteditor/main/locale/zh.json
|
||||
office/{path}/web-apps/apps/documenteditor/main/resources/css/app.css
|
||||
office/{path}/web-apps/apps/documenteditor/main/resources/img/iconssmall@2.5x.svg
|
||||
office/{path}/web-apps/apps/presentationeditor/main/app.js
|
||||
office/{path}/web-apps/apps/presentationeditor/main/code.js
|
||||
office/{path}/web-apps/apps/presentationeditor/main/locale/zh.json
|
||||
office/{path}/web-apps/apps/presentationeditor/main/resources/css/app.css
|
||||
office/{path}/web-apps/apps/presentationeditor/main/resources/img/iconsbig@2.5x.svg
|
||||
office/{path}/web-apps/apps/presentationeditor/main/resources/img/iconsbig@2x.png
|
||||
office/{path}/web-apps/apps/presentationeditor/main/resources/img/iconssmall@2.5x.svg
|
||||
office/{path}/web-apps/apps/spreadsheeteditor/main/app.js
|
||||
office/{path}/web-apps/apps/spreadsheeteditor/main/code.js
|
||||
office/{path}/web-apps/apps/spreadsheeteditor/main/locale/zh.json
|
||||
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/css/app.css
|
||||
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/formula-lang/zh_desc.json
|
||||
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/img/iconssmall@2.5x.svg
|
||||
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/img/iconssmall@2x.png
|
||||
office/{path}/web-apps/vendor/xregexp/xregexp-all-min.js
|
||||
office/{path}/web-apps/vendor/xregexp/xregexp-all-min.js
|
||||
office/{path}/web-apps/vendor/xregexp/xregexp-all-min.js
|
||||
|
||||
drawio/webapp/js/app.min.js
|
||||
drawio/webapp/js/extensions.min.js
|
||||
drawio/webapp/js/shapes-14-6-5.min.js
|
||||
drawio/webapp/js/stencils.min.js
|
||||
drawio/webapp/math/es5/core.js
|
||||
drawio/webapp/math/es5/input/asciimath.js
|
||||
drawio/webapp/math/es5/input/tex.js
|
||||
drawio/webapp/math/es5/output/svg.js
|
||||
drawio/webapp/math/es5/output/svg/fonts/tex.js
|
||||
drawio/webapp/styles/grapheditor.css
|
||||
|
||||
minder/css/chunk-vendors.fe9c56c6.css
|
||||
minder/js/app.aa385de3.js
|
||||
minder/js/chunk-vendors.cc7455b8.js
|
||||
4175
CHANGELOG.md
Normal file
4175
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
146
README.md
146
README.md
@@ -1,115 +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` & `Docker Compose v2.0+` must be installed
|
||||
- System: `Centos/Debian/Ubuntu/macOS`
|
||||
- Hardware suggestion: 2 cores and above 2G memory
|
||||
- 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 project
|
||||
## 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 https://github.com/kuaifan/dootask.git
|
||||
# Or you can use gitee
|
||||
git clone 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: ./cmd install --port 2222)
|
||||
# 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
|
||||
./cmd port 2222
|
||||
# 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
|
||||
### Start Service
|
||||
|
||||
```bash
|
||||
# Development mode, Mac OS only
|
||||
./cmd up
|
||||
```
|
||||
|
||||
### Development & Build
|
||||
|
||||
Please ensure you have installed `NodeJs 20+`
|
||||
|
||||
```bash
|
||||
# Development mode
|
||||
./cmd dev
|
||||
|
||||
# Production projects, macOS only
|
||||
# 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 test "your command" # To run a phpunit command
|
||||
./cmd mysql "your command" # To run a mysql command (backup: Backup database, recovery: Restore database)
|
||||
```
|
||||
|
||||
### NGINX PROXY SSL
|
||||
#### Method 1: Automatic Configuration
|
||||
|
||||
```bash
|
||||
# 1、Nginx config add
|
||||
# Run command and follow the prompts
|
||||
./cmd https
|
||||
```
|
||||
|
||||
#### Method 2: Nginx Proxy Configuration
|
||||
|
||||
```bash
|
||||
# 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、Enter directory and run command
|
||||
./cmd https
|
||||
# 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: enter directory and run command
|
||||
./cmd update
|
||||
|
||||
# Or method 2: use this method if method 1 fails
|
||||
git pull
|
||||
./cmd mysql backup
|
||||
./cmd uninstall
|
||||
./cmd install
|
||||
./cmd mysql recovery
|
||||
./cmd artisan migrate
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
* Please retry if upgrade fails across major versions.
|
||||
* If you encounter 502 errors after upgrade, run `./cmd reup` to restart services.
|
||||
|
||||
## Project Migration
|
||||
|
||||
After installing the new project, follow these steps to complete migration:
|
||||
|
||||
1、Backup original database
|
||||
|
||||
```bash
|
||||
# Run command in the old project
|
||||
./cmd mysql backup
|
||||
```
|
||||
|
||||
2、Copy the following files and directories from old project to the same paths in new project
|
||||
|
||||
- `Database backup file`
|
||||
- `docker/appstore`
|
||||
- `public/uploads`
|
||||
|
||||
3、Restore database to new project
|
||||
```bash
|
||||
# Run command in the new project
|
||||
./cmd mysql recovery
|
||||
```
|
||||
|
||||
## Uninstall Project
|
||||
|
||||
```bash
|
||||
# Enter directory and run command
|
||||
./cmd uninstall
|
||||
```
|
||||
|
||||
### More Commands
|
||||
|
||||
```bash
|
||||
./cmd help
|
||||
```
|
||||
|
||||
101
README_CN.md
101
README_CN.md
@@ -1,19 +1,28 @@
|
||||
# Install (Docker)
|
||||
# DooTask - 开源任务管理系统
|
||||
|
||||
**[English](./README.md)** | 中文文档
|
||||
|
||||
- [截图预览](README_PREVIEW.md)
|
||||
- [截图预览](./README_PREVIEW.md)
|
||||
- [演示站点](http://www.dootask.com/)
|
||||
|
||||
**QQ交流群**
|
||||
|
||||
- QQ群号: `546574618`
|
||||
|
||||
## 📍 0.x 迁移到 1.x
|
||||
|
||||
- 升级时请务必备份好数据!
|
||||
- 如果升级失败请尝试执行 `./cmd update` 重试几次。
|
||||
- 如果升级中出现 `没有找到 xxx 容器` 的提示,请运行 `./cmd reup` 后再执行 `./cmd update`。
|
||||
- 如果升级后出现502错误请运行 `./cmd reup` 重启服务即可。
|
||||
- 如果升级后出现 `应用「xxx」未安装` 的提示,请使用管理员账号进入应用商店安装相关应用。
|
||||
|
||||
## 安装程序
|
||||
|
||||
- 必须安装:`Docker` 和 `Docker Compose v2.0+`
|
||||
- 支持环境:`Centos/Debian/Ubuntu/macOS`
|
||||
- 硬件建议:2核2G以上
|
||||
- 必须安装:`Docker v20.10+` 和 `Docker Compose v2.0+`
|
||||
- 支持环境:`Centos/Debian/Ubuntu/macOS` 等 linux/unix 系统
|
||||
- 硬件建议:2核4G以上
|
||||
- 特别说明:Windows 可以使用 WSL2 安装 Linux 环境后再安装 DooTask。
|
||||
|
||||
### 部署项目
|
||||
|
||||
@@ -21,14 +30,14 @@
|
||||
# 1、克隆项目到您的本地或服务器
|
||||
|
||||
# 通过github克隆项目
|
||||
git clone https://github.com/kuaifan/dootask.git
|
||||
git clone --depth=1 https://github.com/kuaifan/dootask.git
|
||||
# 或者你也可以使用gitee
|
||||
git clone https://gitee.com/aipaw/dootask.git
|
||||
git clone --depth=1 https://gitee.com/aipaw/dootask.git
|
||||
|
||||
# 2、进入目录
|
||||
cd dootask
|
||||
|
||||
# 3、一键安装项目(自定义端口安装 ./cmd install --port 2222)
|
||||
# 3、一键安装项目(自定义端口安装,如:./cmd install --port 80)
|
||||
./cmd install
|
||||
```
|
||||
|
||||
@@ -42,44 +51,44 @@ cd dootask
|
||||
### 更换端口
|
||||
|
||||
```bash
|
||||
./cmd port 2222
|
||||
# 此方法仅更换http端口,更换https端口请阅读下面SSL配置
|
||||
./cmd port 80
|
||||
```
|
||||
|
||||
### 停止服务
|
||||
|
||||
```bash
|
||||
./cmd stop
|
||||
./cmd down
|
||||
```
|
||||
|
||||
# 一旦应用程序被设置,无论何时你想要启动服务器(如果它被停止)运行以下命令
|
||||
./cmd start
|
||||
### 启动服务
|
||||
|
||||
```bash
|
||||
./cmd up
|
||||
```
|
||||
|
||||
### 开发编译
|
||||
|
||||
请确保你已经安装了 `NodeJs 20+`
|
||||
|
||||
```bash
|
||||
# 开发模式,仅限macOS
|
||||
# 开发模式
|
||||
./cmd dev
|
||||
|
||||
# 编译项目,仅限macOS
|
||||
# 编译项目(这是网页端的,客户端请参考“.github/workflows/publish.yml”文件)
|
||||
./cmd prod
|
||||
```
|
||||
|
||||
### SSL 配置
|
||||
|
||||
### 运行命令的快捷方式
|
||||
#### 方法1:自动配置
|
||||
|
||||
```bash
|
||||
# 你可以使用以下命令来执行
|
||||
./cmd artisan "your command" # 运行 artisan 命令
|
||||
./cmd php "your command" # 运行 php 命令
|
||||
./cmd nginx "your command" # 运行 nginx 命令
|
||||
./cmd redis "your command" # 运行 redis 命令
|
||||
./cmd composer "your command" # 运行 composer 命令
|
||||
./cmd supervisorctl "your command" # 运行 supervisorctl 命令
|
||||
./cmd test "your command" # 运行 phpunit 命令
|
||||
./cmd mysql "your command" # 运行 mysql 命令 (backup: 备份数据库,recovery: 还原数据库)
|
||||
```bash
|
||||
# 执行指令,根据提示执行即可
|
||||
./cmd https
|
||||
```
|
||||
|
||||
### NGINX 代理 SSL
|
||||
#### 方法2:Nginx 代理配置
|
||||
|
||||
```bash
|
||||
# 1、Nginx 代理配置添加
|
||||
@@ -87,8 +96,8 @@ proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# 2、进入项目所在目录,运行以下命令
|
||||
./cmd https
|
||||
# 2、执行指令(如果取消 Nginx 代理配置请运行:./cmd https close)
|
||||
./cmd https agent
|
||||
```
|
||||
|
||||
## 升级更新
|
||||
@@ -96,21 +105,43 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
**注意:在升级之前请备份好你的数据!**
|
||||
|
||||
```bash
|
||||
# 方法1:进入项目所在目录,运行以下命令
|
||||
./cmd update
|
||||
```
|
||||
|
||||
# (或者)方法2:如果方法1失败请使用此方法
|
||||
git pull
|
||||
* 跨越大版本升级失败时请重试执行一次。
|
||||
* 如果升级后出现502请运行 `./cmd reup` 重启服务即可。
|
||||
|
||||
## 迁移项目
|
||||
|
||||
在新项目安装好之后按照以下步骤完成项目迁移:
|
||||
|
||||
1、备份原数据库
|
||||
|
||||
```bash
|
||||
# 在旧的项目下执行指令
|
||||
./cmd mysql backup
|
||||
./cmd uninstall
|
||||
./cmd install
|
||||
```
|
||||
|
||||
2、将旧项目以下文件和目录拷贝至新项目同路径位置
|
||||
|
||||
- `数据库备份文件`
|
||||
- `docker/appstore`
|
||||
- `public/uploads`
|
||||
|
||||
3、还原数据库至新项目
|
||||
```bash
|
||||
# 在新的项目下执行指令
|
||||
./cmd mysql recovery
|
||||
./cmd artisan migrate
|
||||
```
|
||||
|
||||
## 卸载项目
|
||||
|
||||
```bash
|
||||
# 进入项目所在目录,运行以下命令
|
||||
./cmd uninstall
|
||||
```
|
||||
|
||||
### 更多指令
|
||||
|
||||
```bash
|
||||
./cmd help
|
||||
```
|
||||
|
||||
31
README_PUBLISH.md
Normal file
31
README_PUBLISH.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 发布
|
||||
|
||||
## 准备工作
|
||||
|
||||
1. 添加环境变量 `APPLEID`、`APPLEIDPASS` 用于公证
|
||||
2. 添加环境变量 `CSC_LINK`、`CSC_KEY_PASSWORD` 用于签名
|
||||
3. 添加环境变量 `GITHUB_TOKEN`、`GITHUB_REPOSITORY` 用于发布到GitHub(GitHub Actions 发布不需要)
|
||||
4. 添加环境变量 `PUBLISH_KEY` 用于发布到私有服务器
|
||||
|
||||
## 发布版本
|
||||
|
||||
```shell
|
||||
npm run translate # 翻译(可选)
|
||||
npm run version # 生成版本
|
||||
npm run build # 编译前端
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 执行 `npm run build` 作用是生成网页端;
|
||||
- 客户端 (Windows、Mac、Android) 会通过 GitHub Actions 自动生成并发布;所以,如果要自动发布只需要提交git并推送即可;
|
||||
- 如果想手动生成客户端执行 `./cmd electron` 根据提示选择操作。
|
||||
|
||||
|
||||
## 编译 App
|
||||
|
||||
```shell
|
||||
./cmd appbuild publish # 编译生成App需要的资源
|
||||
```
|
||||
|
||||
编译完后进入 `resources/mobile` EEUI框架目录内打包 Android 或 iOS 应用(Android 以实现 GitHub Actions 自动发布)
|
||||
1780
_ide_helper.php
1780
_ide_helper.php
File diff suppressed because it is too large
Load Diff
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}");
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Hhxsv5\LaravelS\Swoole\Events\ServerStartInterface;
|
||||
use Swoole\Http\Server;
|
||||
|
||||
class ServerStartEvent implements ServerStartInterface
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
$server->startMsecTime = $this->msecTime();
|
||||
}
|
||||
|
||||
private function msecTime()
|
||||
{
|
||||
list($msec, $sec) = explode(' ', microtime());
|
||||
$time = explode(".", $sec . ($msec * 1000));
|
||||
return $time[0];
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\WebSocket;
|
||||
use Cache;
|
||||
use App\Services\RequestContext;
|
||||
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
|
||||
use Swoole\Http\Server;
|
||||
|
||||
@@ -16,9 +16,16 @@ class WorkerStartEvent implements WorkerStartInterface
|
||||
|
||||
public function handle(Server $server, $workerId)
|
||||
{
|
||||
if (isset($server->startMsecTime) && Cache::get("swooleServerStartMsecTime") != $server->startMsecTime) {
|
||||
Cache::forever("swooleServerStartMsecTime", $server->startMsecTime);
|
||||
WebSocket::query()->delete();
|
||||
// 仅在Worker进程启动时执行一次初始化代码
|
||||
$initTable = app('swoole')->initFlagTable;
|
||||
if ($initTable->incr('init_flag', 'value') === 1) {
|
||||
$this->handleFirstWorkerTasks();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Exceptions;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Image;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
@@ -51,6 +53,11 @@ class Handler extends ExceptionHandler
|
||||
*/
|
||||
public function render($request, Throwable $e)
|
||||
{
|
||||
if ($e instanceof NotFoundHttpException) {
|
||||
if ($result = $this->ImagePathHandler($request)) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
if ($e instanceof ApiException) {
|
||||
return response()->json(Base::retError($e->getMessage(), $e->getData(), $e->getCode()));
|
||||
} elseif ($e instanceof ModelNotFoundException) {
|
||||
@@ -67,9 +74,168 @@ class Handler extends ExceptionHandler
|
||||
public function report(Throwable $e)
|
||||
{
|
||||
if ($e instanceof ApiException) {
|
||||
Log::error($e->getMessage(), ['exception' => ' at ' . $e->getFile() .':' . $e->getLine()]);
|
||||
if ($e->isWriteLog()) {
|
||||
Log::error($e->getMessage(), [
|
||||
'code' => $e->getCode(),
|
||||
'data' => $e->getData(),
|
||||
'exception' => ' at ' . $e->getFile() . ':' . $e->getLine()
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
parent::report($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片路径处理
|
||||
* @param $request
|
||||
* @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse|null
|
||||
*/
|
||||
private function ImagePathHandler($request)
|
||||
{
|
||||
$path = $request->path();
|
||||
|
||||
// 处理图片
|
||||
$patternCrop = '/^(uploads\/.*\.(png|jpg|jpeg))\/crop\/([^\/]+)$/';
|
||||
$patternThumb = '/^(uploads\/.*)_thumb\.(png|jpg|jpeg)$/';
|
||||
$matchesCrop = null;
|
||||
$matchesThumb = null;
|
||||
if (preg_match($patternCrop, $path, $matchesCrop) || preg_match($patternThumb, $path, $matchesThumb)) {
|
||||
// 获取参数
|
||||
if ($matchesCrop) {
|
||||
$file = $matchesCrop[1];
|
||||
$ext = $matchesCrop[2];
|
||||
$rules = preg_replace('/\s+/', '', $matchesCrop[3]);
|
||||
$rules = str_replace(['=', '&'], [':', ','], $rules);
|
||||
$rules = explode(',', $rules);
|
||||
} elseif ($matchesThumb) {
|
||||
$file = $matchesThumb[1];
|
||||
$ext = $matchesThumb[2];
|
||||
$rules = ['percentage:320x0'];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (empty($rules)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 提取年月
|
||||
$Ym = date("Ym");
|
||||
if (preg_match('/\/(\d{6})\//', $file, $ms)) {
|
||||
$Ym = $ms[1];
|
||||
}
|
||||
|
||||
// 文件存在直接返回
|
||||
$dirName = str_replace(['/', '.'], '_', $file);
|
||||
$fileName = str_replace([':', ','], ['-', '_'], implode(',', $rules)) . '.' . $ext;
|
||||
$savePath = public_path('uploads/tmp/crop/' . $Ym . '/' . $dirName . '/' . $fileName);
|
||||
if (file_exists($savePath)) {
|
||||
// 设置头部声明图片缓存
|
||||
return response()->file($savePath, [
|
||||
'Pragma' => 'public',
|
||||
'Cache-Control' => 'max-age=1814400',
|
||||
'Expires' => gmdate('D, d M Y H:i:s', time() + 1814400) . ' GMT',
|
||||
'Last-Modified' => gmdate('D, d M Y H:i:s', filemtime($savePath)) . ' GMT',
|
||||
'ETag' => md5_file($savePath)
|
||||
]);
|
||||
}
|
||||
|
||||
// 文件不存在处理
|
||||
$sourcePath = public_path($file);
|
||||
if (!file_exists($sourcePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 判断删除多余文件
|
||||
$saveDir = dirname($savePath);
|
||||
if (is_dir($saveDir)) {
|
||||
$items = glob($saveDir . '/*');
|
||||
if (count($items) > 5) {
|
||||
usort($items, function ($a, $b) {
|
||||
return filemtime($b) - filemtime($a);
|
||||
});
|
||||
$itemsToDelete = array_slice($items, 5);
|
||||
foreach ($itemsToDelete as $item) {
|
||||
if (is_file($item)) {
|
||||
unlink($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Base::makeDir($saveDir);
|
||||
}
|
||||
|
||||
// 处理图片
|
||||
try {
|
||||
$handle = 0;
|
||||
$image = new Image($sourcePath);
|
||||
foreach ($rules as $rule) {
|
||||
if (!str_contains($rule, ':')) {
|
||||
continue;
|
||||
}
|
||||
[$type, $value] = explode(':', $rule);
|
||||
if (!in_array($type, ['ratio', 'size', 'percentage', 'cover', 'contain'])) {
|
||||
continue;
|
||||
}
|
||||
switch ($type) {
|
||||
// 按比例裁剪
|
||||
case 'ratio':
|
||||
if (is_numeric($value)) {
|
||||
$image->ratioCrop($value);
|
||||
$handle++;
|
||||
}
|
||||
break;
|
||||
|
||||
// 按尺寸缩放
|
||||
case 'size':
|
||||
$size = Base::newIntval(explode('x', $value));
|
||||
if (count($size) === 2) {
|
||||
$image->resize($size[0], $size[1]);
|
||||
$handle++;
|
||||
}
|
||||
break;
|
||||
|
||||
// 按尺寸缩放
|
||||
case 'percentage':
|
||||
case 'cover':
|
||||
case 'contain':
|
||||
$size = Base::newIntval(explode('x', $value));
|
||||
if (count($size) === 2) {
|
||||
$image->thumb($size[0], $size[1], $type);
|
||||
$handle++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($handle > 0) {
|
||||
$image->saveTo($savePath);
|
||||
Image::compressImage($savePath, 80);
|
||||
return response()->file($savePath, [
|
||||
'Pragma' => 'public',
|
||||
'Cache-Control' => 'max-age=1814400',
|
||||
'Expires' => gmdate('D, d M Y H:i:s', time() + 1814400) . ' GMT',
|
||||
'Last-Modified' => gmdate('D, d M Y H:i:s', filemtime($savePath)) . ' GMT',
|
||||
'ETag' => md5_file($savePath)
|
||||
]);
|
||||
} else {
|
||||
$image->destroy();
|
||||
}
|
||||
} catch (\ImagickException) { }
|
||||
}
|
||||
|
||||
// 容错处理
|
||||
$patternFault = '/^(images\/.*\.(png|jpg|jpeg))\/crop\/([^\/]+)$/';
|
||||
$matchesFault = null;
|
||||
if (preg_match($patternFault, $path, $matchesFault)) {
|
||||
$file = public_path($matchesFault[1]);
|
||||
if (!file_exists($file)) {
|
||||
$file = public_path('images/other/imgerr.jpg');
|
||||
}
|
||||
if (file_exists($file)) {
|
||||
return response()->file($file);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
if (!function_exists('asset_main')) {
|
||||
function asset_main($path, $secure = null)
|
||||
{
|
||||
return preg_replace("/^https*:\/\//", "//", app('url')->asset($path, $secure));
|
||||
return preg_replace("/^https?:\/\//", "//", app('url')->asset($path, $secure));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,3 +15,10 @@ if (!function_exists('seeders_at')) {
|
||||
return date("Y-m-d H:i:s", $time);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('md5s')) {
|
||||
function md5s($val, $len = 16)
|
||||
{
|
||||
return substr(md5($val), 32 - $len);
|
||||
}
|
||||
}
|
||||
|
||||
1237
app/Http/Controllers/Api/ApproveController.php
Executable file
1237
app/Http/Controllers/Api/ApproveController.php
Executable file
File diff suppressed because it is too large
Load Diff
161
app/Http/Controllers/Api/ComplaintController.php
Executable file
161
app/Http/Controllers/Api/ComplaintController.php
Executable file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Request;
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Models\Complaint;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
|
||||
/**
|
||||
* @apiDefine dialog
|
||||
*
|
||||
* 投诉
|
||||
*/
|
||||
class ComplaintController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @api {get} api/complaint/lists 01. 获取举报投诉列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName lists
|
||||
*
|
||||
* @apiParam {Number} [type] 类型
|
||||
* @apiParam {Number} [status] 状态
|
||||
*
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
* @apiParam {Number} [pagesize] 每页显示数量,默认:50,最大:100
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function lists()
|
||||
{
|
||||
$user = User::auth();
|
||||
$user->identity('admin');
|
||||
//
|
||||
$type = intval(Request::input('type'));
|
||||
$status = Request::input('status');
|
||||
//
|
||||
$complaints = Complaint::query()
|
||||
->when($type, function($q) use($type) {
|
||||
$q->where('type', $type);
|
||||
})
|
||||
->when($status != "", function($q) use($status) {
|
||||
$q->where('status', $status);
|
||||
})
|
||||
->orderByDesc('id')
|
||||
->paginate(Base::getPaginate(100, 50));
|
||||
//
|
||||
return Base::retSuccess('success', $complaints);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/complaint/submit 02. 举报投诉
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName submit
|
||||
*
|
||||
* @apiParam {Number} dialog_id 对话ID
|
||||
* @apiParam {Number} type 类型
|
||||
* @apiParam {String} reason 原因
|
||||
* @apiParam {String} imgs 图片
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function submit()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$dialog_id = intval(Request::input('dialog_id'));
|
||||
$type = intval(Request::input('type'));
|
||||
$reason = trim(Request::input('reason'));
|
||||
$imgs = Request::input('imgs');
|
||||
//
|
||||
WebSocketDialog::checkDialog($dialog_id);
|
||||
//
|
||||
if (!$type) {
|
||||
return Base::retError('请选择举报类型');
|
||||
}
|
||||
if (!$reason) {
|
||||
return Base::retError('请填写举报原因');
|
||||
}
|
||||
//
|
||||
$report_imgs = [];
|
||||
if (!empty($imgs) && is_array($imgs)) {
|
||||
foreach ($imgs as $img) {
|
||||
$report_imgs[] = Base::unFillUrl($img['path']);
|
||||
}
|
||||
}
|
||||
//
|
||||
Complaint::createInstance([
|
||||
'dialog_id' => $dialog_id,
|
||||
'userid' => $user->userid,
|
||||
'type' => $type,
|
||||
'reason' => $reason,
|
||||
'imgs' => $report_imgs,
|
||||
])->save();
|
||||
// 通知管理员
|
||||
$botUser = User::botGetOrCreate('system-msg');
|
||||
User::where("identity", "like", "%,admin,%")
|
||||
->orderByDesc('line_at')
|
||||
->take(10)
|
||||
->get()
|
||||
->each(function ($adminUser) use ($reason, $botUser) {
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $adminUser->userid);
|
||||
if ($dialog) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'title' => '收到新的举报信息',
|
||||
'content' => "收到新的举报信息:{$reason} (请前往应用查看详情)"
|
||||
], $botUser->userid);
|
||||
}
|
||||
});
|
||||
//
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/complaint/action 03. 举报投诉 - 操作
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName action
|
||||
*
|
||||
* @apiParam {Number} id ID
|
||||
* @apiParam {Number} type 类型
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function action()
|
||||
{
|
||||
$user = User::auth();
|
||||
$user->identity('admin');
|
||||
//
|
||||
$id = intval(Request::input('id'));
|
||||
$type = trim(Request::input('type'));
|
||||
//
|
||||
if ($type == 'handle') {
|
||||
Complaint::whereId($id)->update([
|
||||
"status" => 1
|
||||
]);
|
||||
}
|
||||
if ($type == 'delete') {
|
||||
Complaint::whereId($id)->delete();
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
109
app/Http/Controllers/Api/PublicController.php
Executable file
109
app/Http/Controllers/Api/PublicController.php
Executable file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\UserBot;
|
||||
use App\Module\Base;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
* @apiDefine public
|
||||
*
|
||||
* 公开
|
||||
*/
|
||||
class PublicController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* 签到 - 路由器(openwrt)功能安装脚本
|
||||
*
|
||||
* @apiParam {String} key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function checkin__install()
|
||||
{
|
||||
$key = trim(Request::input('key'));
|
||||
//
|
||||
$setting = Base::setting('checkinSetting');
|
||||
if ($setting['open'] !== 'open') {
|
||||
return <<<EOF
|
||||
#!/bin/sh
|
||||
echo "function off"
|
||||
EOF;
|
||||
}
|
||||
if ($key != $setting['key']) {
|
||||
return <<<EOF
|
||||
#!/bin/sh
|
||||
echo "key error"
|
||||
EOF;
|
||||
}
|
||||
//
|
||||
$reportUrl = Base::fillUrl("api/public/checkin/report");
|
||||
return <<<EOE
|
||||
#!/bin/sh
|
||||
echo 'installing...'
|
||||
|
||||
cat > /etc/init.d/dootask-checkin-report <<EOF
|
||||
#!/bin/sh
|
||||
mac=\\\$(awk 'NR!=1&&\\\$3=="0x2" {print \\\$4}' /proc/net/arp | tr "\\n" ",")
|
||||
tmp='{"key":"{$setting['key']}","mac":"'\\\${mac}'","time":"'\\\$(date +%s)'"}'
|
||||
curl -4 -X POST "{$reportUrl}" -H "Content-Type: application/json" -d \\\${tmp}
|
||||
EOF
|
||||
|
||||
chmod +x /etc/init.d/dootask-checkin-report
|
||||
crontab -l >/tmp/cronbak
|
||||
sed -i '/\/etc\/init.d\/dootask-checkin-report/d' /tmp/cronbak
|
||||
sed -i '/^$/d' /tmp/cronbak
|
||||
echo "* * * * * sh /etc/init.d/dootask-checkin-report" >>/tmp/cronbak
|
||||
crontab /tmp/cronbak
|
||||
rm -f /tmp/cronbak
|
||||
/etc/init.d/cron enable
|
||||
/etc/init.d/cron restart
|
||||
|
||||
echo 'installed'
|
||||
EOE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {post} 签到 - 上报
|
||||
* - 1、路由器(openwrt)签到上报
|
||||
* - 2、考勤机签到上报
|
||||
*
|
||||
* @apiParam {String} key
|
||||
* @apiParam {String} mac 使用逗号分割多个
|
||||
* @apiParam {String} time
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function checkin__report()
|
||||
{
|
||||
$key = trim(Request::input('key'));
|
||||
$mac = trim(Request::input('mac'));
|
||||
$time = intval(Request::input('time'));
|
||||
$type = trim(Request::input('type'));
|
||||
//
|
||||
$setting = Base::setting('checkinSetting');
|
||||
if ($setting['open'] !== 'open') {
|
||||
return 'function off';
|
||||
}
|
||||
$alreadyTip = false;
|
||||
if ($type === 'face') {
|
||||
if (!in_array('face', $setting['modes'])) {
|
||||
return 'mode off';
|
||||
}
|
||||
if ($key != $setting['face_key']) {
|
||||
return 'key error';
|
||||
}
|
||||
$alreadyTip = $setting['face_retip'] === 'open';
|
||||
} else {
|
||||
if (!in_array('auto', $setting['modes'])) {
|
||||
return 'mode off';
|
||||
}
|
||||
if ($key != $setting['key']) {
|
||||
return 'key error';
|
||||
}
|
||||
}
|
||||
UserBot::checkinBotCheckin($mac, $time, $alreadyTip);
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,18 @@
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
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;
|
||||
use Carbon\Carbon;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Request;
|
||||
@@ -27,12 +30,15 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/my 01. 我发送的汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName my
|
||||
*
|
||||
* @apiParam {String} [type] 汇报类型,weekly:周报,daily:日报
|
||||
* @apiParam {Array} [created_at] 汇报时间
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.key: 关键词
|
||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||
* - keys.created_at: 汇报时间
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50
|
||||
*
|
||||
@@ -43,30 +49,45 @@ class ReportController extends AbstractController
|
||||
public function my(): array
|
||||
{
|
||||
$user = User::auth();
|
||||
// 搜索当前用户
|
||||
//
|
||||
$builder = Report::with(['receivesUser'])->whereUserid($user->userid);
|
||||
$type = trim(Request::input('type'));
|
||||
$createAt = Request::input('created_at');
|
||||
in_array($type, [Report::WEEKLY, Report::DAILY]) && $builder->whereType($type);
|
||||
$whereArray = [];
|
||||
if (is_array($createAt)) {
|
||||
if ($createAt[0] > 0) $whereArray[] = ['created_at', '>=', date('Y-m-d H:i:s', Base::dayTimeF($createAt[0]))];
|
||||
if ($createAt[1] > 0) $whereArray[] = ['created_at', '<=', date('Y-m-d H:i:s', Base::dayTimeE($createAt[1]))];
|
||||
$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']);
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
$list = $builder->where($whereArray)->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
|
||||
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
|
||||
return Base::retSuccess('success', $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/receive 02. 我接收的汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName receive
|
||||
*
|
||||
* @apiParam {String} [username] 会员名
|
||||
* @apiParam {String} [type] 汇报类型,weekly:周报,daily:日报
|
||||
* @apiParam {Array} [created_at] 汇报时间
|
||||
* @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
|
||||
*
|
||||
@@ -81,24 +102,41 @@ class ReportController extends AbstractController
|
||||
$builder->whereHas("receivesUser", function ($query) use ($user) {
|
||||
$query->where("report_receives.userid", $user->userid);
|
||||
});
|
||||
$type = trim(Request::input('type'));
|
||||
$createAt = Request::input('created_at');
|
||||
$username = trim(Request::input('username', ''));
|
||||
$builder->whereHas('sendUser', function ($query) use ($username) {
|
||||
if (!empty($username)) {
|
||||
$query->where('users.email', 'LIKE', '%' . $username . '%');
|
||||
$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']}%");
|
||||
});
|
||||
} 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());
|
||||
}
|
||||
});
|
||||
in_array($type, [Report::WEEKLY, Report::DAILY]) && $builder->whereType($type);
|
||||
$whereArray = [];
|
||||
if (is_array($createAt)) {
|
||||
if ($createAt[0] > 0) $whereArray[] = ['created_at', '>=', date('Y-m-d H:i:s', Base::dayTimeF($createAt[0]))];
|
||||
if ($createAt[1] > 0) $whereArray[] = ['created_at', '<=', date('Y-m-d H:i:s', Base::dayTimeE($createAt[1]))];
|
||||
}
|
||||
$list = $builder->where($whereArray)->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
|
||||
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
|
||||
if ($list->items()) {
|
||||
foreach ($list->items() as $item) {
|
||||
$item->receive_time = ReportReceive::query()->whereRid($item["id"])->whereUserid($user->userid)->value("receive_time");
|
||||
$item->receive_at = ReportReceive::query()->whereRid($item["id"])->whereUserid($user->userid)->value("receive_at");
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('success', $list);
|
||||
@@ -107,16 +145,18 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/store 03. 保存并发送工作汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName store
|
||||
*
|
||||
* @apiParam {Number} [id] 汇报ID
|
||||
* @apiParam {String} [title] 汇报标题
|
||||
* @apiParam {Array} [type] 汇报类型,weekly:周报,daily:日报
|
||||
* @apiParam {Number} [content] 内容
|
||||
* @apiParam {Number} [receive] 汇报对象
|
||||
* @apiParam {Number} [offset] 偏移量
|
||||
* @apiParam {Number} id 汇报ID,0为新建
|
||||
* @apiParam {String} [sign] 唯一签名,通过[api/report/template]接口返回
|
||||
* @apiParam {String} title 汇报标题
|
||||
* @apiParam {Array} type 汇报类型,weekly:周报,daily:日报
|
||||
* @apiParam {Number} content 内容
|
||||
* @apiParam {Number} [receive] 汇报对象
|
||||
* @apiParam {Number} offset 时间偏移量
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -124,21 +164,23 @@ class ReportController extends AbstractController
|
||||
*/
|
||||
public function store(): array
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$input = [
|
||||
"id" => Base::getPostValue("id", 0),
|
||||
"title" => Base::getPostValue("title"),
|
||||
"type" => Base::getPostValue("type"),
|
||||
"content" => Base::getPostValue("content"),
|
||||
"receive" => Base::getPostValue("receive"),
|
||||
"id" => Request::input("id", 0),
|
||||
"sign" => Request::input("sign"),
|
||||
"title" => Request::input("title"),
|
||||
"type" => Request::input("type"),
|
||||
"content" => Request::input("content"),
|
||||
"receive" => Request::input("receive"),
|
||||
// 以当前日期为基础的周期偏移量。例如选择了上一周那么就是 -1,上一天同理。
|
||||
"offset" => Base::getPostValue("offset", 0),
|
||||
"offset" => Request::input("offset", 0),
|
||||
];
|
||||
$validator = Validator::make($input, [
|
||||
'id' => 'numeric',
|
||||
'title' => 'required',
|
||||
'type' => ['required', Rule::in([Report::WEEKLY, Report::DAILY])],
|
||||
'content' => 'required',
|
||||
'receive' => 'required',
|
||||
'offset' => ['numeric', 'max:0'],
|
||||
], [
|
||||
'id.numeric' => 'ID只能是数字',
|
||||
@@ -146,14 +188,12 @@ class ReportController extends AbstractController
|
||||
'type.required' => '请选择汇报类型',
|
||||
'type.in' => '汇报类型错误',
|
||||
'content.required' => '请填写汇报内容',
|
||||
'receive.required' => '请选择接收人',
|
||||
'offset.numeric' => '工作汇报周期格式错误,只能是数字',
|
||||
'offset.max' => '只能提交当天/本周或者之前的的工作汇报',
|
||||
]);
|
||||
if ($validator->fails())
|
||||
return Base::retError($validator->errors()->first());
|
||||
|
||||
$user = User::auth();
|
||||
// 接收人
|
||||
if (is_array($input["receive"])) {
|
||||
// 删除当前登录人
|
||||
@@ -165,7 +205,7 @@ class ReportController extends AbstractController
|
||||
|
||||
foreach ($input["receive"] as $userid) {
|
||||
$input["receive_content"][] = [
|
||||
"receive_time" => Carbon::now()->toDateTimeString(),
|
||||
"receive_at" => Carbon::now()->toDateTimeString(),
|
||||
"userid" => $userid,
|
||||
"read" => 0,
|
||||
];
|
||||
@@ -173,7 +213,7 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
// 在事务中运行
|
||||
Report::transaction(function () use ($input, $user) {
|
||||
return AbstractModel::transaction(function () use ($input, $user) {
|
||||
$id = $input["id"];
|
||||
if ($id) {
|
||||
// 编辑
|
||||
@@ -181,29 +221,41 @@ class ReportController extends AbstractController
|
||||
$report->updateInstance([
|
||||
"title" => $input["title"],
|
||||
"type" => $input["type"],
|
||||
"content" => htmlspecialchars($input["content"]),
|
||||
]);
|
||||
} else {
|
||||
// 生成唯一标识
|
||||
$sign = Report::generateSign($input["type"], $input["offset"]);
|
||||
$sign = Base::isNumber($input["sign"]) ? $input["sign"] : Report::generateSign($input["type"], $input["offset"]);
|
||||
// 检查唯一标识是否存在
|
||||
if (empty($input["id"])) {
|
||||
if (Report::query()->whereSign($sign)->whereType($input["type"])->count() > 0)
|
||||
throw new ApiException("请勿重复提交工作汇报");
|
||||
if (empty($input["id"]) && Report::query()->whereSign($sign)->whereType($input["type"])->count() > 0) {
|
||||
throw new ApiException("请勿重复提交工作汇报");
|
||||
}
|
||||
$report = Report::createInstance([
|
||||
"sign" => $sign,
|
||||
"title" => $input["title"],
|
||||
"type" => $input["type"],
|
||||
"content" => htmlspecialchars($input["content"]),
|
||||
"userid" => $user->userid,
|
||||
"sign" => $sign,
|
||||
]);
|
||||
}
|
||||
|
||||
$report->save();
|
||||
if (!empty($input["receive_content"])) {
|
||||
// 删除关联
|
||||
$report->Receives()->delete();
|
||||
|
||||
// 保存内容
|
||||
$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"]) {
|
||||
// 保存接收人
|
||||
$report->Receives()->createMany($input["receive_content"]);
|
||||
}
|
||||
@@ -224,13 +276,15 @@ class ReportController extends AbstractController
|
||||
];
|
||||
Task::deliver(new PushTask($params, false));
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('保存成功', $report);
|
||||
});
|
||||
return Base::retSuccess('保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/template 04. 生成汇报模板
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName template
|
||||
@@ -250,6 +304,7 @@ class ReportController extends AbstractController
|
||||
$offset = abs(intval(Request::input("offset", 0)));
|
||||
$id = intval(Request::input("offset", 0));
|
||||
$now_dt = trim(Request::input("date")) ? Carbon::parse(Request::input("date")) : Carbon::now();
|
||||
|
||||
// 获取开始时间
|
||||
if ($type === Report::DAILY) {
|
||||
$start_time = Carbon::today();
|
||||
@@ -271,21 +326,31 @@ class ReportController extends AbstractController
|
||||
$start_time->startOfWeek();
|
||||
$end_time = Carbon::instance($start_time)->endOfWeek();
|
||||
}
|
||||
|
||||
// 生成唯一标识
|
||||
$sign = Report::generateSign($type, 0, Carbon::instance($start_time));
|
||||
$one = Report::query()->whereSign($sign)->whereType($type)->first();
|
||||
$one = Report::whereSign($sign)->whereType($type)->first();
|
||||
|
||||
// 如果已经提交了相关汇报
|
||||
if ($one && $id > 0) {
|
||||
return Base::retSuccess('success', [
|
||||
"content" => $one->content,
|
||||
"title" => $one->title,
|
||||
"id" => $one->id,
|
||||
"sign" => $one->sign,
|
||||
"title" => $one->title,
|
||||
"content" => $one->content,
|
||||
]);
|
||||
}
|
||||
|
||||
// 表格头部
|
||||
$labels = [
|
||||
Doo::translate('项目'),
|
||||
Doo::translate('任务'),
|
||||
Doo::translate('负责人'),
|
||||
Doo::translate('备注'),
|
||||
];
|
||||
|
||||
// 已完成的任务
|
||||
$completeContent = "";
|
||||
$completeDatas = [];
|
||||
$complete_task = ProjectTask::query()
|
||||
->whereNotNull("complete_at")
|
||||
->whereBetween("complete_at", [$start_time->toDateTimeString(), $end_time->toDateTimeString()])
|
||||
@@ -297,33 +362,47 @@ class ReportController extends AbstractController
|
||||
if ($complete_task->isNotEmpty()) {
|
||||
foreach ($complete_task as $task) {
|
||||
$complete_at = Carbon::parse($task->complete_at);
|
||||
$pre = $type == Report::WEEKLY ? ('<span>[' . Base::Lang('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</span> ') : '';
|
||||
$completeContent .= '<li>' . $pre . $task->name . '</li>';
|
||||
$remark = $type == Report::WEEKLY ? ('<div style="text-align:center">[' . Doo::translate('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</div>') : ' ';
|
||||
$completeDatas[] = [
|
||||
$task->project->name,
|
||||
$task->name,
|
||||
$task->taskUser->where("owner", 1)->map(function ($item) {
|
||||
return User::userid2nickname($item->userid);
|
||||
})->implode(", "),
|
||||
$remark,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$completeContent = '<li> </li>';
|
||||
}
|
||||
|
||||
// 未完成的任务
|
||||
$unfinishedContent = "";
|
||||
$unfinishedDatas = [];
|
||||
$unfinished_task = ProjectTask::query()
|
||||
->whereNull("complete_at")
|
||||
->whereNotNull("start_at")
|
||||
->where("end_at", "<", $end_time->toDateTimeString())
|
||||
->join("projects", "projects.id", "=", "project_tasks.project_id")
|
||||
->whereNull("projects.archived_at")
|
||||
->whereNull("project_tasks.complete_at")
|
||||
->whereNotNull("project_tasks.start_at")
|
||||
->where("project_tasks.end_at", "<", $end_time->toDateTimeString())
|
||||
->whereHas("taskUser", function ($query) use ($user) {
|
||||
$query->where("userid", $user->userid);
|
||||
})
|
||||
->orderByDesc("id")
|
||||
->select("project_tasks.*")
|
||||
->orderByDesc("project_tasks.id")
|
||||
->get();
|
||||
if ($unfinished_task->isNotEmpty()) {
|
||||
foreach ($unfinished_task as $task) {
|
||||
empty($task->end_at) || $end_at = Carbon::parse($task->end_at);
|
||||
$pre = (!empty($end_at) && $end_at->lt($now_dt)) ? '<span style="color:#ff0000;">[' . Base::Lang('超期') . ']</span> ' : '';
|
||||
$unfinishedContent .= '<li>' . $pre . $task->name . '</li>';
|
||||
$remark = (!empty($end_at) && $end_at->lt($now_dt)) ? '<div style="color:#ff0000;text-align:center">[' . Doo::translate('超期') . ']</div>' : ' ';
|
||||
$unfinishedDatas[] = [
|
||||
$task->project->name,
|
||||
$task->name,
|
||||
$task->taskUser->where("owner", 1)->map(function ($item) {
|
||||
return User::userid2nickname($item->userid);
|
||||
})->implode(", "),
|
||||
$remark,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$unfinishedContent = '<li> </li>';
|
||||
}
|
||||
|
||||
// 生成标题
|
||||
if ($type === Report::WEEKLY) {
|
||||
$title = $user->nickname . "的周报[" . $start_time->format("m/d") . "-" . $end_time->format("m/d") . "]";
|
||||
@@ -331,16 +410,43 @@ class ReportController extends AbstractController
|
||||
} else {
|
||||
$title = $user->nickname . "的日报[" . $start_time->format("Y/m/d") . "]";
|
||||
}
|
||||
$title = Doo::translate($title);
|
||||
|
||||
// 生成内容
|
||||
$contents = [];
|
||||
$contents[] = '<h2>' . Doo::translate('已完成工作') . '</h2>';
|
||||
$contents[] = view('report', [
|
||||
'labels' => $labels,
|
||||
'datas' => $completeDatas,
|
||||
])->render();
|
||||
|
||||
$contents[] = '<p> </p>';
|
||||
$contents[] = '<h2>' . Doo::translate('未完成的工作') . '</h2>';
|
||||
$contents[] = view('report', [
|
||||
'labels' => $labels,
|
||||
'datas' => $unfinishedDatas,
|
||||
])->render();
|
||||
|
||||
if ($type === Report::WEEKLY) {
|
||||
$contents[] = '<p> </p>';
|
||||
$contents[] = "<h2>" . Doo::translate("下周拟定计划") . "[" . $start_time->addWeek()->format("m/d") . "-" . $end_time->addWeek()->format("m/d") . "]</h2>";
|
||||
$contents[] = view('report', [
|
||||
'labels' => [
|
||||
Doo::translate('计划描述'),
|
||||
Doo::translate('计划时间'),
|
||||
Doo::translate('负责人'),
|
||||
],
|
||||
'datas' => [],
|
||||
])->render();
|
||||
}
|
||||
|
||||
$data = [
|
||||
"time" => $start_time->toDateTimeString(),
|
||||
"complete_task" => $complete_task,
|
||||
"unfinished_task" => $unfinished_task,
|
||||
"content" => '<h2>' . Base::Lang('已完成工作') . '</h2><ol>' .
|
||||
$completeContent . '</ol><h2>' .
|
||||
Base::Lang('未完成的工作') . '</h2><ol>' .
|
||||
$unfinishedContent . '</ol>',
|
||||
"sign" => $sign,
|
||||
"title" => $title,
|
||||
"content" => implode("", $contents),
|
||||
];
|
||||
|
||||
if ($one) {
|
||||
$data['id'] = $one->id;
|
||||
}
|
||||
@@ -350,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 返回信息(错误描述)
|
||||
@@ -362,31 +470,149 @@ class ReportController extends AbstractController
|
||||
*/
|
||||
public function detail(): array
|
||||
{
|
||||
$id = intval(trim(Request::input("id")));
|
||||
if (empty($id))
|
||||
return Base::retError("缺少ID参数");
|
||||
|
||||
$one = Report::getOne($id);
|
||||
$one->type_val = $one->getRawOriginal("type");
|
||||
|
||||
$user = User::auth();
|
||||
// 标记为已读
|
||||
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,
|
||||
]);
|
||||
//
|
||||
$id = intval(trim(Request::input("id")));
|
||||
$code = trim(Request::input("code"));
|
||||
//
|
||||
if (empty($id) && empty($code)) {
|
||||
return Base::retError("缺少ID参数");
|
||||
}
|
||||
//
|
||||
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/last_submitter 06. 获取最后一次提交的接收人
|
||||
* @api {get} api/report/mark 06. 标记已读/未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName mark
|
||||
*
|
||||
* @apiParam {Number} id 报告id(组)
|
||||
* @apiParam {Number} action 操作
|
||||
* - read: 标记已读(默认)
|
||||
* - unread: 标记未读
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function mark(): array
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = Request::input('id');
|
||||
$action = Request::input('action');
|
||||
//
|
||||
if (is_array($id)) {
|
||||
if (count(Base::arrayRetainInt($id)) > 100) {
|
||||
return Base::retError("最多只能操作100条数据");
|
||||
}
|
||||
$builder = Report::whereIn("id", Base::arrayRetainInt($id));
|
||||
} else {
|
||||
$builder = Report::whereId(intval($id));
|
||||
}
|
||||
$builder ->chunkById(100, function ($list) use ($action, $user) {
|
||||
/** @var Report $item */
|
||||
foreach ($list as $item) {
|
||||
$item->receivesUser()->updateExistingPivot($user->userid, [
|
||||
"read" => $action === 'unread' ? 0 : 1,
|
||||
]);
|
||||
}
|
||||
});
|
||||
return Base::retSuccess("操作成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/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
|
||||
@@ -402,32 +628,34 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/unread 07. 获取未读
|
||||
* @api {get} api/report/unread 09. 获取未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName unread
|
||||
*
|
||||
* @apiParam {Number} [userid] 用户id
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function unread(): array
|
||||
{
|
||||
$userid = intval(trim(Request::input("userid")));
|
||||
$user = empty($userid) ? User::auth() : User::find($userid);
|
||||
|
||||
$data = Report::whereHas("Receives", function (Builder $query) use ($user) {
|
||||
$query->where("userid", $user->userid)->where("read", 0);
|
||||
})->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
|
||||
return Base::retSuccess("success", $data);
|
||||
$user = User::auth();
|
||||
//
|
||||
$total = Report::select('reports.id')
|
||||
->join('report_receives', 'report_receives.rid', '=', 'reports.id')
|
||||
->where('report_receives.userid', $user->userid)
|
||||
->where('report_receives.read', 0)
|
||||
->count();
|
||||
//
|
||||
return Base::retSuccess("success", compact("total"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/read 08. 标记汇报已读,可批量
|
||||
* @api {get} api/report/read 10. 标记汇报已读,可批量
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName read
|
||||
@@ -447,7 +675,7 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
if (is_string($ids)) {
|
||||
$ids = explode(",", $ids);
|
||||
$ids = Base::explodeInt($ids);
|
||||
}
|
||||
|
||||
$data = Report::with(["receivesUser" => function (BelongsToMany $query) use ($user) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
11
app/Http/Controllers/Api/TestController.php
Executable file
11
app/Http/Controllers/Api/TestController.php
Executable file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
/**
|
||||
* 测试
|
||||
*/
|
||||
class TestController extends AbstractController
|
||||
{
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,29 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Tasks\AutoArchivedTask;
|
||||
use App\Tasks\DeleteTmpTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Arr;
|
||||
use Cache;
|
||||
use Request;
|
||||
use Redirect;
|
||||
use Response;
|
||||
use App\Models\File;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Base;
|
||||
use App\Module\Extranet;
|
||||
use App\Module\RandomColor;
|
||||
use App\Tasks\LoopTask;
|
||||
use App\Tasks\AppPushTask;
|
||||
use App\Tasks\JokeSoupTask;
|
||||
use App\Tasks\DeleteTmpTask;
|
||||
use App\Tasks\EmailNoticeTask;
|
||||
use App\Tasks\AutoArchivedTask;
|
||||
use App\Tasks\DeleteBotMsgTask;
|
||||
use App\Tasks\CheckinRemindTask;
|
||||
use App\Tasks\CloseMeetingRoomTask;
|
||||
use App\Tasks\ZincSearchSyncTask;
|
||||
use App\Tasks\UnclaimedTaskRemindTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Laravolt\Avatar\Avatar;
|
||||
|
||||
|
||||
/**
|
||||
@@ -22,6 +40,9 @@ class IndexController extends InvokeController
|
||||
if ($action) {
|
||||
$app .= "__" . $action;
|
||||
}
|
||||
if ($app == 'default') {
|
||||
return '';
|
||||
}
|
||||
if (!method_exists($this, $app)) {
|
||||
$app = method_exists($this, $method) ? $method : 'main';
|
||||
}
|
||||
@@ -30,11 +51,179 @@ class IndexController extends InvokeController
|
||||
|
||||
/**
|
||||
* 首页
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
return view('main', ['version' => Base::getVersion()]);
|
||||
$hotFile = public_path('hot');
|
||||
$manifestFile = public_path('manifest.json');
|
||||
if (file_exists($hotFile)) {
|
||||
$array = Base::json2array(file_get_contents($hotFile));
|
||||
$style = null;
|
||||
$script = preg_replace("/^(\/\/(.*?))(:\d+)?\//i", "$1:" . $array['APP_DEV_PORT'] . "/", asset_main("resources/assets/js/app.js"));
|
||||
$proxyUri = Base::liveEnv('VSCODE_PROXY_URI');
|
||||
if (is_string($proxyUri) && preg_match('/^https?:\/\//i', $proxyUri)) {
|
||||
$script = preg_replace('/^(https?:\/\/|\/\/)[^\/]+/', rtrim($proxyUri, '/'), $script, 1);
|
||||
}
|
||||
} else {
|
||||
$array = Base::json2array(file_get_contents($manifestFile));
|
||||
$style = asset_main($array['resources/assets/js/app.js']['css'][0]);
|
||||
$script = asset_main($array['resources/assets/js/app.js']['file']);
|
||||
}
|
||||
return response()->view('main', [
|
||||
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
|
||||
'version' => Base::getVersion(),
|
||||
'style' => $style,
|
||||
'script' => $script,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取版本号
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function version()
|
||||
{
|
||||
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
|
||||
*/
|
||||
public function avatar()
|
||||
{
|
||||
$segment = Request::segment(2);
|
||||
if ($segment && preg_match('/.*?\.png$/i', $segment)) {
|
||||
$name = substr($segment, 0, -4);
|
||||
} else {
|
||||
$name = Request::input('name', 'D');
|
||||
}
|
||||
$size = Request::input('size', 128);
|
||||
$color = Request::input('color');
|
||||
$background = Request::input('background');
|
||||
// 移除各种括号及其内容
|
||||
$pattern = '/[((\[【{[<<『「](.*?)[))\]】}]>>』」]/u';
|
||||
$name = preg_replace($pattern, '', $name) ?: preg_replace($pattern, '$1', $name);
|
||||
// 移除常见标识词(不区分大小写)
|
||||
$filterWords = [
|
||||
// 测试相关
|
||||
'测试', '测试号', '测试账号', '内测', '体验', '试用', 'test', 'testing', 'beta',
|
||||
// 账号相关
|
||||
'账号', '帐号', '账户', '帐户', 'account', 'acc', 'id', 'uid',
|
||||
// 临时标识
|
||||
'临时', '暂用', '备用', '主号', '副号', '小号', '大号', 'temp', 'temporary', 'backup',
|
||||
// 系统相关
|
||||
'系统', '管理员', 'admin', 'administrator', 'system', 'sys', 'root',
|
||||
// 用户相关
|
||||
'用户', 'user', '会员', 'member', 'vip', 'svip', 'mvip', 'premium',
|
||||
// 官方相关
|
||||
'官方', '正式', '认证', 'official', 'verified', 'auth',
|
||||
// 客服相关
|
||||
'客服', '售后', '服务', 'service', 'support', 'helper', 'assistant',
|
||||
// 游戏相关
|
||||
'game', 'gaming', 'player', 'gamer',
|
||||
// 社交媒体相关
|
||||
'ins', 'instagram', 'fb', 'facebook', 'tiktok', 'tweet', 'weibo', 'wechat',
|
||||
// 常见后缀
|
||||
'official', 'real', 'fake', 'copy', 'channel', 'studio', 'team', 'group',
|
||||
// 职业相关
|
||||
'dev', 'developer', 'designer', 'artist', 'writer', 'editor',
|
||||
// 其他
|
||||
'bot', 'robot', 'auto', 'anonymous', 'guest', 'default', 'new', 'old'
|
||||
];
|
||||
$filterWords = array_map(function ($word) {
|
||||
return preg_quote($word, '/');
|
||||
}, $filterWords);
|
||||
$name = preg_replace('/' . implode('|', $filterWords) . '/iu', '', $name) ?: $name;
|
||||
// 移除分隔符和特殊字符
|
||||
$filterSymbols = [
|
||||
// 常见分隔符
|
||||
'-', '_', '=', '+', '/', '\\', '|',
|
||||
'~', '@', '#', '$', '%', '^', '&', '*',
|
||||
// 空格类字符
|
||||
' ', ' ', "\t", "\n", "\r",
|
||||
// 标点符号(中英文)
|
||||
'。', ',', '、', ';', ':', '?', '!',
|
||||
'.', '…', '‥', '′', '″', '℃',
|
||||
'.', ',', ';', ':', '?', '!',
|
||||
// 引号类(修正版)
|
||||
'"', "'", '‘', '’', '“', '”', '`',
|
||||
// 特殊符号
|
||||
'★', '☆', '○', '●', '◎', '◇', '◆',
|
||||
'□', '■', '△', '▲', '▽', '▼',
|
||||
'♀', '♂', '♪', '♫', '♯', '♭', '♬',
|
||||
'→', '←', '↑', '↓', '↖', '↗', '↙', '↘',
|
||||
'√', '×', '÷', '±', '∵', '∴',
|
||||
'♠', '♥', '♣', '♦',
|
||||
// emoji 表情符号范围
|
||||
'\x{1F300}-\x{1F9FF}',
|
||||
'\x{2600}-\x{26FF}',
|
||||
'\x{2700}-\x{27BF}',
|
||||
'\x{1F900}-\x{1F9FF}',
|
||||
'\x{1F600}-\x{1F64F}'
|
||||
];
|
||||
$filterSymbols = array_map(function ($symbol) {
|
||||
return preg_quote($symbol, '/');
|
||||
}, $filterSymbols);
|
||||
$name = preg_replace('/[' . implode('', $filterSymbols) . ']/u', '', $name) ?: $name;
|
||||
//
|
||||
if (preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', $name)) {
|
||||
$name = mb_substr($name, mb_strlen($name) - 2);
|
||||
}
|
||||
if (empty($name)) {
|
||||
$name = 'D';
|
||||
}
|
||||
if (empty($color)) {
|
||||
$color = '#ffffff';
|
||||
$cacheKey = "avatarBackgroundColor::" . md5($name);
|
||||
$background = Cache::rememberForever($cacheKey, function () {
|
||||
return RandomColor::one(['luminosity' => 'dark']);
|
||||
});
|
||||
}
|
||||
//
|
||||
$path = public_path('uploads/tmp/avatar/' . substr(md5($name), 0, 2));
|
||||
$file = Base::joinPath($path, md5($name) . '.png');
|
||||
if (file_exists($file)) {
|
||||
return response()->file($file, [
|
||||
'Pragma' => 'public',
|
||||
'Cache-Control' => 'max-age=1814400',
|
||||
'Content-type' => 'image/png',
|
||||
'Expires' => gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400),
|
||||
]);
|
||||
}
|
||||
Base::makeDir($path);
|
||||
//
|
||||
$avatar = new Avatar([
|
||||
'shape' => 'square',
|
||||
'width' => $size,
|
||||
'height' => $size,
|
||||
'chars' => 2,
|
||||
'fontSize' => $size / 2.9,
|
||||
'uppercase' => true,
|
||||
'fonts' => [resource_path('assets/statics/fonts/Source_Han_Sans_SC_Regular.otf')],
|
||||
'foregrounds' => [$color],
|
||||
'backgrounds' => [$background],
|
||||
'border' => [
|
||||
'size' => 0,
|
||||
'color' => 'foreground',
|
||||
'radius' => 0,
|
||||
],
|
||||
]);
|
||||
return response($avatar->create($name)->save($file))
|
||||
->header('Pragma', 'public')
|
||||
->header('Cache-Control', 'max-age=1814400')
|
||||
->header('Content-type', 'image/png')
|
||||
->header('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,96 +245,265 @@ class IndexController extends InvokeController
|
||||
// 限制内网访问
|
||||
return "Forbidden Access";
|
||||
}
|
||||
// 删除过期的临时表数据
|
||||
Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1));
|
||||
Task::deliver(new DeleteTmpTask('tmp', 24));
|
||||
// 自动归档任务
|
||||
// 自动归档
|
||||
Task::deliver(new AutoArchivedTask());
|
||||
// 邮件通知
|
||||
Task::deliver(new EmailNoticeTask());
|
||||
// App推送
|
||||
Task::deliver(new AppPushTask());
|
||||
// 删除过期的临时表数据
|
||||
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());
|
||||
// 周期任务
|
||||
Task::deliver(new LoopTask());
|
||||
// 签到提醒
|
||||
Task::deliver(new CheckinRemindTask());
|
||||
// 获取笑话/心灵鸡汤
|
||||
Task::deliver(new JokeSoupTask());
|
||||
// 未领取任务通知
|
||||
Task::deliver(new UnclaimedTaskRemindTask());
|
||||
// 关闭会议室
|
||||
Task::deliver(new CloseMeetingRoomTask());
|
||||
// ZincSearch 同步
|
||||
Task::deliver(new ZincSearchSyncTask());
|
||||
|
||||
return "success";
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取所有中文
|
||||
* @return array|string
|
||||
* 桌面客户端发布
|
||||
*/
|
||||
public function allcn()
|
||||
public function desktop__publish($name = '')
|
||||
{
|
||||
if (!Base::is_internal_ip(Base::getIp())) {
|
||||
// 限制内网访问
|
||||
return "Forbidden Access";
|
||||
$publishVersion = Request::header('publish-version');
|
||||
$latestFile = public_path("uploads/desktop/latest");
|
||||
$latestVersion = file_exists($latestFile) ? trim(file_get_contents($latestFile)) : "0.0.1";
|
||||
if (strtolower($name) === 'latest') {
|
||||
$name = $latestVersion;
|
||||
}
|
||||
$list = Base::readDir(resource_path());
|
||||
$array = [];
|
||||
foreach ($list as $item) {
|
||||
$content = file_get_contents($item);
|
||||
preg_match_all("/\\\$L\((.*?)\)/", $content, $matchs);
|
||||
if ($matchs) {
|
||||
foreach ($matchs[1] as $text) {
|
||||
$array[trim(trim($text, '"'), "'")] = trim(trim($text, '"'), "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
return array_values($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取所有中文
|
||||
* @return array|string
|
||||
*/
|
||||
public function allcn__php()
|
||||
{
|
||||
if (!Base::is_internal_ip(Base::getIp())) {
|
||||
// 限制内网访问
|
||||
return "Forbidden Access";
|
||||
}
|
||||
$list = Base::readDir(app_path());
|
||||
$array = [];
|
||||
foreach ($list as $item) {
|
||||
$content = file_get_contents($item);
|
||||
preg_match_all("/(retSuccess|retError|ApiException)\((.*?)[,|)]/", $content, $matchs);
|
||||
if ($matchs) {
|
||||
foreach ($matchs[2] as $text) {
|
||||
$array[trim(trim($text, '"'), "'")] = trim(trim($text, '"'), "'");
|
||||
// 上传(header 中包含 publish-version)
|
||||
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
|
||||
// 判断密钥
|
||||
$publishKey = Request::header('publish-key');
|
||||
if ($publishKey !== env('APP_KEY')) {
|
||||
return Base::retError("key error");
|
||||
}
|
||||
// 判断版本
|
||||
$action = Request::get('action');
|
||||
$draftPath = "uploads/desktop-draft/{$publishVersion}/";
|
||||
if ($action === 'release') {
|
||||
// 将草稿版本发布为正式版本
|
||||
$draftPath = public_path($draftPath);
|
||||
$releasePath = public_path("uploads/desktop/{$publishVersion}/");
|
||||
if (!file_exists($draftPath)) {
|
||||
return Base::retError("draft version not exists");
|
||||
}
|
||||
}
|
||||
}
|
||||
return array_values($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取所有中文
|
||||
* @return array|string
|
||||
*/
|
||||
public function allcn__all()
|
||||
{
|
||||
if (!Base::is_internal_ip(Base::getIp())) {
|
||||
// 限制内网访问
|
||||
return "Forbidden Access";
|
||||
}
|
||||
$list = array_merge(Base::readDir(app_path()), Base::readDir(resource_path()));
|
||||
$array = [];
|
||||
foreach ($list as $item) {
|
||||
if (Base::rightExists($item, "language.all.js")) {
|
||||
continue;
|
||||
}
|
||||
if (Base::rightExists($item, ".php") || Base::rightExists($item, ".vue") || Base::rightExists($item, ".js")) {
|
||||
$content = file_get_contents($item);
|
||||
preg_match_all("/(['\"])(.*?)[\u{4e00}-\u{9fa5}\u{FE30}-\u{FFA0}]+([\s\S]((?!\n).)*)\\1/u", $content, $matchs);
|
||||
if ($matchs) {
|
||||
foreach ($matchs[0] as $text) {
|
||||
$tmp = preg_replace("/\/\/(.*?)$/", "", $text);
|
||||
$tmp = preg_replace("/\/\/(.*?)\n/", "", $tmp);
|
||||
$tmp = str_replace(":", "", $tmp);
|
||||
if (!preg_match("/[\u{4e00}-\u{9fa5}\u{FE30}-\u{FFA0}]/u", $tmp)){
|
||||
continue; // 没有中文
|
||||
}
|
||||
$val = trim(trim($text, '"'), "'");
|
||||
$array[md5($val)] = $val;
|
||||
if (file_exists($releasePath)) {
|
||||
Base::deleteDirAndFile($releasePath);
|
||||
}
|
||||
Base::copyDirectory($draftPath, $releasePath);
|
||||
file_put_contents($latestFile, $publishVersion);
|
||||
// 删除旧版本
|
||||
Base::deleteDirAndFile(public_path("uploads/desktop-draft"));
|
||||
$dirs = Base::recursiveDirs(public_path("uploads/desktop"), false);
|
||||
sort($dirs);
|
||||
$num = 0;
|
||||
foreach ($dirs as $dir) {
|
||||
if (!preg_match("/\/\d+\.\d+\.\d+$/", $dir)) {
|
||||
continue;
|
||||
}
|
||||
$num++;
|
||||
if ($num < 5) {
|
||||
continue; // 保留最新的5个版本
|
||||
}
|
||||
if (filemtime($dir) > time() - 3600 * 24 * 30) {
|
||||
continue; // 保留最近30天的版本
|
||||
}
|
||||
Base::deleteDirAndFile($dir);
|
||||
}
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
// 上传草稿版本
|
||||
return Base::upload([
|
||||
"file" => Request::file('file'),
|
||||
"type" => 'publish',
|
||||
"path" => $draftPath,
|
||||
"saveName" => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// 列表(访问路径 desktop/publish/{version})
|
||||
if (preg_match("/^v*(\d+\.\d+\.\d+)$/", $name, $match)) {
|
||||
$paths = [
|
||||
"uploads/desktop/{$match[1]}/",
|
||||
"uploads/desktop/v{$match[1]}/",
|
||||
"uploads/desktop-draft/{$match[1]}/",
|
||||
"uploads/desktop-draft/v{$match[1]}/",
|
||||
];
|
||||
$avaiPath = null;
|
||||
foreach ($paths as $path) {
|
||||
$dirPath = public_path($path);
|
||||
$isDraft = str_contains($path, 'draft');
|
||||
if (is_dir($dirPath)) {
|
||||
$avaiPath = $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
abort_if(empty($avaiPath), 404);
|
||||
$lists = Base::recursiveFiles($dirPath, false);
|
||||
$files = [];
|
||||
foreach ($lists as $file) {
|
||||
if (preg_match('/\.(zip|yml|yaml|blockmap)$/i', $file) || str_ends_with($file, '-win.exe')) {
|
||||
continue;
|
||||
}
|
||||
$fileName = basename($file, $dirPath);
|
||||
$fileSize = filesize($file);
|
||||
$files[] = [
|
||||
'name' => $fileName,
|
||||
'time' => date("Y-m-d H:i:s", filemtime($file)),
|
||||
'size' => $fileSize > 0 ? Base::readableBytes($fileSize) : 0,
|
||||
'url' => Base::fillUrl(Base::joinPath($avaiPath, $fileName)),
|
||||
];
|
||||
}
|
||||
$otherVersion = [];
|
||||
$dirs = Base::recursiveDirs(public_path("uploads/desktop"), false);
|
||||
foreach ($dirs as $dir) {
|
||||
if (!preg_match("/\/\d+\.\d+\.\d+$/", $dir)) {
|
||||
continue;
|
||||
}
|
||||
$version = basename($dir);
|
||||
if ($version === $match[1]) {
|
||||
continue;
|
||||
}
|
||||
$otherVersion[] = [
|
||||
'version' => $version,
|
||||
'url' => Base::fillUrl("desktop/publish/{$version}"),
|
||||
];
|
||||
}
|
||||
//
|
||||
return view('desktop', [
|
||||
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
|
||||
'version' => $match[1],
|
||||
'files' => $files,
|
||||
'is_draft' => $isDraft,
|
||||
'latest_version' => $latestVersion,
|
||||
'other_version' => array_reverse($otherVersion),
|
||||
]);
|
||||
}
|
||||
|
||||
// 下载(Latest 版本内的文件,访问路径 desktop/publish/{fileName})
|
||||
if ($name) {
|
||||
$filePath = public_path("uploads/desktop/{$latestVersion}/{$name}");
|
||||
if (file_exists($filePath)) {
|
||||
return Response::download($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// 404
|
||||
abort(404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drawio 图标搜索
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function drawio__iconsearch()
|
||||
{
|
||||
$query = trim(Request::input('q'));
|
||||
$page = trim(Request::input('p'));
|
||||
$size = trim(Request::input('c'));
|
||||
return Extranet::drawioIconSearch($query, $page, $size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览文件
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function online__preview()
|
||||
{
|
||||
$key = trim(Request::input('key'));
|
||||
//
|
||||
$data = parse_url($key);
|
||||
$path = Arr::get($data, 'path');
|
||||
$file = public_path($path);
|
||||
// 防止 ../ 穿越获取到系统文件
|
||||
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');
|
||||
$ext = strtolower(Arr::get($query, 'ext'));
|
||||
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
|
||||
if ($ext === 'pdf') {
|
||||
// 文件超过 10m 不支持在线预览,提示下载
|
||||
if (filesize($file) > 10 * 1024 * 1024) {
|
||||
return view('download', [
|
||||
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
|
||||
'name' => $name,
|
||||
'size' => Base::readableBytes(filesize($file)),
|
||||
'url' => Base::fillUrl($path),
|
||||
'button' => Doo::translate('点击下载'),
|
||||
]);
|
||||
}
|
||||
// 浏览器类型
|
||||
$browser = 'none';
|
||||
if (str_contains($userAgent, 'chrome') || str_contains($userAgent, 'android_kuaifan_eeui')) {
|
||||
$browser = str_contains($userAgent, 'android_kuaifan_eeui') ? 'android-mobile' : 'chrome-desktop';
|
||||
} elseif (str_contains($userAgent, 'safari') || str_contains($userAgent, 'ios_kuaifan_eeui')) {
|
||||
$browser = str_contains($userAgent, 'ios_kuaifan_eeui') ? 'safari-mobile' : 'safari-desktop';
|
||||
}
|
||||
// electron 直接在线预览查看
|
||||
if (str_contains($userAgent, 'electron') || str_contains($browser, 'desktop')) {
|
||||
return Response::download($file, $name, [
|
||||
'Content-Type' => 'application/pdf'
|
||||
], 'inline');
|
||||
}
|
||||
// EEUI App 直接在线预览查看
|
||||
if (Base::isEEUIApp() && Base::judgeClientVersion("0.34.47")) {
|
||||
if ($browser === 'safari-mobile') {
|
||||
$redirectUrl = Base::fillUrl($path);
|
||||
return <<<EOF
|
||||
<script>
|
||||
window.top.postMessage({
|
||||
action: "eeuiAppSendMessage",
|
||||
data: [
|
||||
{
|
||||
action: 'setPageData', // 设置页面数据
|
||||
data: {
|
||||
showProgress: true,
|
||||
titleFixed: true,
|
||||
urlFixed: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'createTarget', // 创建目标(访问新地址)
|
||||
url: "{$redirectUrl}",
|
||||
}
|
||||
]
|
||||
}, "*")
|
||||
</script>
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
}
|
||||
return implode("\n", array_values($array));
|
||||
//
|
||||
if (in_array($ext, File::localExt)) {
|
||||
$url = Base::fillUrl($path);
|
||||
} else {
|
||||
$url = 'http://nginx/' . $path;
|
||||
}
|
||||
$url = Base::urlAddparameter($url, [
|
||||
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext
|
||||
]);
|
||||
$redirectUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
|
||||
return Redirect::to($redirectUrl, 301);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Tasks\IhttpTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
@@ -32,24 +29,7 @@ class InvokeController extends BaseController
|
||||
$msg = "404 not found (" . str_replace("__", "/", $app) . ").";
|
||||
return Base::ajaxError($msg);
|
||||
}
|
||||
// 使用websocket请求
|
||||
$apiWebsocket = Request::header('Api-Websocket');
|
||||
if ($apiWebsocket) {
|
||||
$userid = User::userid();
|
||||
if ($userid > 0) {
|
||||
$url = 'http://127.0.0.1:' . env('LARAVELS_LISTEN_PORT') . Request::getRequestUri();
|
||||
$task = new IhttpTask($url, Request::post(), [
|
||||
'Content-Type' => Request::header('Content-Type'),
|
||||
'language' => Request::header('language'),
|
||||
'token' => Request::header('token'),
|
||||
]);
|
||||
$task->setApiWebsocket($apiWebsocket);
|
||||
$task->setApiUserid($userid);
|
||||
Task::deliver($task);
|
||||
return Base::retSuccess('wait');
|
||||
}
|
||||
}
|
||||
// 正常请求
|
||||
//
|
||||
$res = $this->__before($method, $action);
|
||||
if ($res === true || Base::isSuccess($res)) {
|
||||
return $this->$app();
|
||||
|
||||
@@ -12,40 +12,10 @@ class VerifyCsrfToken extends Middleware
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
// 上传图片
|
||||
'api/system/imgupload/',
|
||||
// 接口部分
|
||||
'api/*',
|
||||
|
||||
// 上传文件
|
||||
'api/system/fileupload/',
|
||||
|
||||
// 保存任务优先级
|
||||
'api/system/priority/',
|
||||
|
||||
// 保存创建项目列表模板
|
||||
'api/system/column/template/',
|
||||
|
||||
// 添加任务
|
||||
'api/project/task/add/',
|
||||
|
||||
// 保存工作流
|
||||
'api/project/flow/save/',
|
||||
|
||||
// 修改任务
|
||||
'api/project/task/update/',
|
||||
|
||||
// 聊天发文件
|
||||
'api/dialog/msg/sendfile/',
|
||||
|
||||
// 保存文件内容
|
||||
'api/file/content/save/',
|
||||
|
||||
// 保存文件内容(office)
|
||||
'api/file/content/office/',
|
||||
|
||||
// 保存文件内容(上传)
|
||||
'api/file/content/upload/',
|
||||
|
||||
// 保存汇报
|
||||
'api/report/store/',
|
||||
// 发布桌面端
|
||||
'desktop/publish/',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ namespace App\Http\Middleware;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Module\Doo;
|
||||
use App\Services\RequestContext;
|
||||
use Closure;
|
||||
use Request;
|
||||
|
||||
class WebApi
|
||||
{
|
||||
@@ -18,20 +19,62 @@ class WebApi
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
global $_A;
|
||||
$_A = [];
|
||||
// 记录请求信息
|
||||
RequestContext::set('start_time', microtime(true));
|
||||
RequestContext::set('header_language', $request->header('language'));
|
||||
|
||||
if (Request::input('__Access-Control-Allow-Origin') || Request::header('__Access-Control-Allow-Origin')) {
|
||||
header('Access-Control-Allow-Origin:*');
|
||||
header('Access-Control-Allow-Methods:GET,POST,PUT,DELETE,OPTIONS');
|
||||
header('Access-Control-Allow-Headers:Content-Type, platform, platform-channel, token, release, Access-Control-Allow-Origin');
|
||||
// 更新请求的基本URL
|
||||
RequestContext::updateBaseUrl($request);
|
||||
|
||||
// 加载Doo类
|
||||
Doo::load();
|
||||
|
||||
// 解密请求内容
|
||||
$encrypt = Doo::pgpParseStr($request->header('encrypt'));
|
||||
if ($request->isMethod('post')) {
|
||||
$version = $request->header('version');
|
||||
if ($version && version_compare($version, '0.25.48', '<')) {
|
||||
// 旧版本兼容 php://input
|
||||
parse_str($request->getContent(), $content);
|
||||
if ($content) {
|
||||
$request->merge($content);
|
||||
}
|
||||
} elseif ($encrypt['encrypt_type'] === 'pgp' && $content = $request->input('encrypted')) {
|
||||
// 新版本解密提交的内容
|
||||
$content = Doo::pgpDecryptApi($content, $encrypt['encrypt_id']);
|
||||
if ($content) {
|
||||
$request->merge($content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 强制 https
|
||||
$APP_SCHEME = env('APP_SCHEME', 'auto');
|
||||
if (in_array(strtolower($APP_SCHEME), ['https', 'on', 'ssl', '1', 'true', 'yes'], true)) {
|
||||
$request->setTrustedProxies([$request->getClientIp()], $request::HEADER_X_FORWARDED_PROTO);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
// 执行下一个中间件
|
||||
$response = $next($request);
|
||||
|
||||
// 加密返回内容
|
||||
if ($encrypt['client_type'] === 'pgp' && $content = $response->getContent()) {
|
||||
$content = Doo::pgpEncryptApi($content, $encrypt['client_key']);
|
||||
if ($content) {
|
||||
$response->setContent(json_encode(['encrypted' => $content]));
|
||||
}
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function terminate()
|
||||
{
|
||||
// 请求结束后清理上下文
|
||||
RequestContext::clean();
|
||||
}
|
||||
}
|
||||
|
||||
251
app/Ldap/LdapUser.php
Normal file
251
app/Ldap/LdapUser.php
Normal file
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ldap;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use LdapRecord\Configuration\ConfigurationException;
|
||||
use LdapRecord\Container;
|
||||
use LdapRecord\LdapRecordException;
|
||||
use LdapRecord\Models\Model;
|
||||
|
||||
class LdapUser extends Model
|
||||
{
|
||||
protected static $init = null;
|
||||
/**
|
||||
* The object classes of the LDAP model.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $objectClasses = [
|
||||
'inetOrgPerson',
|
||||
'organizationalPerson',
|
||||
'person',
|
||||
'top',
|
||||
'posixAccount',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getPhoto()
|
||||
{
|
||||
return $this->jpegPhoto && is_array($this->jpegPhoto) ? $this->jpegPhoto[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getDisplayName()
|
||||
{
|
||||
$nickname = $this->displayName ?: $this->uid;
|
||||
return is_array($nickname) ? $nickname[0] : $nickname;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LdapUser
|
||||
*/
|
||||
public static function static(): LdapUser
|
||||
{
|
||||
return new static;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务是否打开
|
||||
* @return bool
|
||||
*/
|
||||
public static function isOpen(): bool
|
||||
{
|
||||
return Base::settingFind('thirdAccessSetting', 'ldap_open') === 'open';
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步本地是否打开
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSyncLocal(): bool
|
||||
{
|
||||
return Base::settingFind('thirdAccessSetting', 'ldap_sync_local') === 'open';
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化配置
|
||||
* @return bool
|
||||
*/
|
||||
public static function initConfig()
|
||||
{
|
||||
if (is_bool(self::$init)) {
|
||||
return self::$init;
|
||||
}
|
||||
//
|
||||
$setting = Base::setting('thirdAccessSetting');
|
||||
if ($setting['ldap_open'] !== 'open') {
|
||||
return self::$init = false;
|
||||
}
|
||||
//
|
||||
$connection = Container::getDefaultConnection();
|
||||
try {
|
||||
$connection->setConfiguration([
|
||||
"hosts" => [$setting['ldap_host']],
|
||||
"port" => intval($setting['ldap_port']),
|
||||
"base_dn" => $setting['ldap_base_dn'],
|
||||
"username" => $setting['ldap_user_dn'],
|
||||
"password" => $setting['ldap_password'],
|
||||
]);
|
||||
return self::$init = true;
|
||||
} catch (ConfigurationException $e) {
|
||||
info($e->getMessage());
|
||||
return self::$init = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取
|
||||
* @param $username
|
||||
* @param $password
|
||||
* @return Model|null
|
||||
*/
|
||||
public static function userFirst($username, $password): ?Model
|
||||
{
|
||||
if (!self::initConfig()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return self::static()
|
||||
->where([
|
||||
'cn' => $username,
|
||||
'userPassword' => $password
|
||||
])->first();
|
||||
} catch (\Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param $username
|
||||
* @param $password
|
||||
* @param User|null $user
|
||||
* @return User|mixed|null
|
||||
*/
|
||||
public static function userLogin($username, $password, $user = null)
|
||||
{
|
||||
if (!self::initConfig()) {
|
||||
return null;
|
||||
}
|
||||
$row = self::userFirst($username, $password);
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
if (empty($user)) {
|
||||
$user = User::reg($username, $password);
|
||||
}
|
||||
if ($user) {
|
||||
$userimg = $row->getPhoto();
|
||||
if ($userimg) {
|
||||
$path = "uploads/user/ldap/";
|
||||
$file = "{$path}{$user->userid}.jpeg";
|
||||
Base::makeDir(public_path($path));
|
||||
if (Base::saveContentImage(public_path($file), $userimg)) {
|
||||
$user->userimg = $file;
|
||||
}
|
||||
}
|
||||
$user->nickname = $row->getDisplayName();
|
||||
$user->save();
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步
|
||||
* @param User $user
|
||||
* @param $password
|
||||
* @return void
|
||||
*/
|
||||
public static function userSync(User $user, $password)
|
||||
{
|
||||
if ($user->isLdap()) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
if (!self::initConfig()) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
if (self::isSyncLocal()) {
|
||||
$row = self::userFirst($user->email, $password);
|
||||
if ($row) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$userimg = public_path($user->getRawOriginal('userimg'));
|
||||
if (file_exists($userimg)) {
|
||||
$userimg = file_get_contents($userimg);
|
||||
} else {
|
||||
$userimg = '';
|
||||
}
|
||||
self::static()->create([
|
||||
'cn' => $user->email,
|
||||
'gidNumber' => 0,
|
||||
'homeDirectory' => '/home/ldap/dootask/' . env("APP_NAME"),
|
||||
'sn' => $user->email,
|
||||
'uid' => $user->email,
|
||||
'uidNumber' => $user->userid,
|
||||
'userPassword' => $password,
|
||||
'displayName' => $user->nickname,
|
||||
'jpegPhoto' => $userimg,
|
||||
]);
|
||||
$user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['ldap']), ['ldap']));
|
||||
$user->save();
|
||||
} catch (LdapRecordException $e) {
|
||||
info("[LDAP] sync fail: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param $username
|
||||
* @param $array
|
||||
* @return void
|
||||
*/
|
||||
public static function userUpdate($username, $array)
|
||||
{
|
||||
if (empty($array)) {
|
||||
return;
|
||||
}
|
||||
if (!self::initConfig()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$row = self::static()
|
||||
->where([
|
||||
'cn' => $username,
|
||||
])->first();
|
||||
$row?->update($array);
|
||||
} catch (\Exception $e) {
|
||||
info("[LDAP] update fail: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param $username
|
||||
* @return void
|
||||
*/
|
||||
public static function userDelete($username)
|
||||
{
|
||||
if (!self::initConfig()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$row = self::static()
|
||||
->where([
|
||||
'cn' => $username,
|
||||
])->first();
|
||||
$row?->delete();
|
||||
} catch (\Exception $e) {
|
||||
info("[LDAP] delete fail: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* App\Model\AbstractModel
|
||||
* App\Models\AbstractModel
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel newQuery()
|
||||
@@ -21,7 +21,10 @@ use Illuminate\Support\Facades\DB;
|
||||
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|static with($relations)
|
||||
* @method static \Illuminate\Database\Query\Builder|static select($columns = [])
|
||||
* @method static \Illuminate\Database\Query\Builder|static whereIn($column, $values, $boolean = 'and', $not = false)
|
||||
* @method static \Illuminate\Database\Query\Builder|static whereNotIn($column, $values, $boolean = 'and')
|
||||
* @method int change(array $array)
|
||||
* @method int remove()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class AbstractModel extends Model
|
||||
@@ -31,6 +34,26 @@ class AbstractModel extends Model
|
||||
const ID = 'id';
|
||||
|
||||
protected $dates = [
|
||||
'top_at',
|
||||
'last_at',
|
||||
|
||||
'start_at',
|
||||
'end_at',
|
||||
|
||||
'archived_at',
|
||||
'complete_at',
|
||||
'loop_at',
|
||||
|
||||
'receive_at',
|
||||
|
||||
'line_at',
|
||||
'disable_at',
|
||||
|
||||
'clear_at',
|
||||
|
||||
'read_at',
|
||||
'done_at',
|
||||
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
@@ -38,6 +61,42 @@ class AbstractModel extends Model
|
||||
|
||||
protected $appendattrs = [];
|
||||
|
||||
/**
|
||||
* 通过模型修改数据
|
||||
* @param AbstractModel $builder
|
||||
* @param $array
|
||||
* @return int
|
||||
*/
|
||||
protected function scopeChange($builder, $array)
|
||||
{
|
||||
$line = 0;
|
||||
$rows = $builder->get();
|
||||
foreach ($rows as $row) {
|
||||
$row->updateInstance($array);
|
||||
if ($row->save()) {
|
||||
$line++;
|
||||
}
|
||||
}
|
||||
return $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过模型删除数据
|
||||
* @param AbstractModel $builder
|
||||
* @return int
|
||||
*/
|
||||
protected function scopeRemove($builder)
|
||||
{
|
||||
$line = 0;
|
||||
$rows = $builder->get();
|
||||
foreach ($rows as $row) {
|
||||
if ($row->delete()) {
|
||||
$line++;
|
||||
}
|
||||
}
|
||||
return $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据忽略错误
|
||||
* @return bool
|
||||
@@ -46,7 +105,7 @@ class AbstractModel extends Model
|
||||
{
|
||||
try {
|
||||
return $this->save();
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -92,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
|
||||
@@ -150,23 +228,46 @@ class AbstractModel extends Model
|
||||
|
||||
/**
|
||||
* 数据库更新或插入
|
||||
* @param $where
|
||||
* @param array $update 存在时更新的内容
|
||||
* @param array $insert 不存在时插入的内容,如果没有则插入更新内容
|
||||
* @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 = [])
|
||||
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;
|
||||
$array = array_merge($where, $insert ?: $update);
|
||||
if ($insert instanceof \Closure) {
|
||||
$insert = $insert();
|
||||
}
|
||||
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]);
|
||||
}
|
||||
$row->updateInstance($array);
|
||||
$isInsert = true;
|
||||
} elseif ($update) {
|
||||
if ($update instanceof \Closure) {
|
||||
$update = $update();
|
||||
}
|
||||
$row->updateInstance($update);
|
||||
$isInsert = false;
|
||||
}
|
||||
if (!$row->save()) {
|
||||
return null;
|
||||
@@ -194,10 +295,9 @@ class AbstractModel extends Model
|
||||
info($eb);
|
||||
}
|
||||
if ($e instanceof ApiException) {
|
||||
throw new ApiException($e->getMessage(), $e->getData(), $e->getCode());
|
||||
throw new ApiException( $e->getMessage() , $e->getData(), $e->getCode());
|
||||
} else {
|
||||
info($e);
|
||||
throw new ApiException($e->getMessage() ?: '处理错误');
|
||||
throw new ApiException( $e->getMessage() ?: '处理错误');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
99
app/Models/ApproveProcInstHistory.php
Normal file
99
app/Models/ApproveProcInstHistory.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
|
||||
/**
|
||||
* App\Models\ApproveProcInstHistory
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $proc_def_id 流程定义ID
|
||||
* @property string|null $proc_def_name 流程定义名
|
||||
* @property string|null $title 标题
|
||||
* @property int|null $department_id 用户部门ID
|
||||
* @property string|null $department 用户部门
|
||||
* @property string|null $company 用户公司
|
||||
* @property string|null $node_id 当前节点
|
||||
* @property string|null $candidate 审批人
|
||||
* @property int|null $task_id 当前任务
|
||||
* @property string|null $start_time 开始时间
|
||||
* @property string|null $end_time 结束时间
|
||||
* @property int|null $duration 持续时间
|
||||
* @property string|null $start_user_id 开始用户ID
|
||||
* @property string|null $start_user_name 开始用户名
|
||||
* @property int|null $is_finished 是否完成
|
||||
* @property string|null $var
|
||||
* @property int $state 当前状态: 0待审批,1审批中,2通过,3拒绝,4撤回
|
||||
* @property string|null $latest_comment
|
||||
* @property string|null $global_comment
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereCandidate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereCompany($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDepartment($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDepartmentId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDuration($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereEndTime($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereGlobalComment($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereIsFinished($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereLatestComment($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereNodeId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereProcDefId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereProcDefName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartTime($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartUserName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereState($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereTitle($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereVar($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ApproveProcInstHistory extends AbstractModel
|
||||
{
|
||||
protected $table = 'approve_proc_inst_history';
|
||||
|
||||
/**
|
||||
* 获取用户审批状态(请假、外出)
|
||||
* @param $userid
|
||||
* @return mixed|null
|
||||
*/
|
||||
public static function getUserApprovalStatus($userid)
|
||||
{
|
||||
if (empty($userid)) {
|
||||
return null;
|
||||
}
|
||||
return Cache::remember('user_is_leave_' . $userid, Carbon::now()->addMinute(), function () use ($userid) {
|
||||
return self::where([
|
||||
['start_user_id', '=', $userid],
|
||||
[DB::raw("JSON_UNQUOTE(JSON_EXTRACT(var, '$.startTime'))"), '<=', Carbon::now()->toDateTimeString()],
|
||||
[DB::raw("JSON_UNQUOTE(JSON_EXTRACT(var, '$.endTime'))"), '>=', Carbon::now()->toDateTimeString()],
|
||||
['state', '=', 2]
|
||||
])->where(function ($query) {
|
||||
$query->where('proc_def_name', 'like', '%请假%')
|
||||
->orWhere('proc_def_name', 'like', '%外出%');
|
||||
})->orderByDesc('id')->value('proc_def_name');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否请假(包含:请假、外出)
|
||||
* @param $userid
|
||||
* @return bool
|
||||
*/
|
||||
public static function userIsLeave($userid)
|
||||
{
|
||||
return (bool)self::getUserApprovalStatus($userid);
|
||||
}
|
||||
}
|
||||
34
app/Models/ApproveProcMsg.php
Normal file
34
app/Models/ApproveProcMsg.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\ApproveProcMsg
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $proc_inst_id 流程实例ID
|
||||
* @property int|null $userid 会员ID
|
||||
* @property int|null $msg_id 消息ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|ApproveProcMsg newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereMsgId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereProcInstId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ApproveProcMsg extends AbstractModel
|
||||
{
|
||||
|
||||
}
|
||||
41
app/Models/Complaint.php
Normal file
41
app/Models/Complaint.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
/**
|
||||
* App\Models\Complaint
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property int|null $userid 举报人id
|
||||
* @property int|null $type 举报类型
|
||||
* @property string|null $reason 举报原因
|
||||
* @property string|null $imgs 举报图片
|
||||
* @property int|null $status 状态 0待处理、1已处理、2已删除
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereImgs($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereReason($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Complaint whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Complaint extends AbstractModel
|
||||
{
|
||||
|
||||
}
|
||||
99
app/Models/Deleted.php
Normal file
99
app/Models/Deleted.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\Deleted
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $type 删除的数据类型(如:project、task、dialog)
|
||||
* @property int|null $did 删除的数据ID
|
||||
* @property int|null $userid 关系会员ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|Deleted newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Deleted newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Deleted query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereDid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Deleted extends AbstractModel
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
/**
|
||||
* 获取删除的ID
|
||||
* @param $type
|
||||
* @param $userid
|
||||
* @param $time
|
||||
* @return array
|
||||
*/
|
||||
public static function ids($type, $userid, $time): array
|
||||
{
|
||||
$builder = self::where([
|
||||
'type' => $type,
|
||||
'userid' => $userid
|
||||
])->orderByDesc('id');
|
||||
if (empty($time)) {
|
||||
$builder = $builder->take(50);
|
||||
} else {
|
||||
$builder = $builder->where('created_at', '>=', Carbon::parse($time))->take(500);
|
||||
}
|
||||
return $builder->pluck('did')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记(恢复或添加数据时删除记录)
|
||||
* @param $type
|
||||
* @param $id
|
||||
* @param $userid
|
||||
* @return void
|
||||
*/
|
||||
public static function forget($type, $id, $userid): void
|
||||
{
|
||||
if (is_array($userid)) {
|
||||
self::where([
|
||||
'type' => $type,
|
||||
'did' => $id,
|
||||
])->whereIn('userid', $userid)->delete();
|
||||
} else {
|
||||
self::where([
|
||||
'type' => $type,
|
||||
'did' => $id,
|
||||
'userid' => $userid,
|
||||
])->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录(删除数据时添加记录)
|
||||
* @param $type
|
||||
* @param $id
|
||||
* @param $userid
|
||||
* @return void
|
||||
*/
|
||||
public static function record($type, $id, $userid): void
|
||||
{
|
||||
$array = is_array($userid) ? $userid : [$userid];
|
||||
foreach ($array as $value) {
|
||||
if (!self::where('type', $type)->where('did', $id)->where('userid', $value)->exists()) {
|
||||
self::updateInsert([
|
||||
'type' => $type,
|
||||
'did' => $id,
|
||||
'userid' => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,21 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use Request;
|
||||
use App\Module\Apps;
|
||||
use App\Module\Base;
|
||||
use App\Tasks\PushTask;
|
||||
use App\Exceptions\ApiException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
* App\Models\File
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $pid 上级ID
|
||||
* @property string|null $pids 上级ID递归
|
||||
* @property int|null $cid 复制ID
|
||||
* @property string|null $name 名称
|
||||
* @property string|null $type 类型
|
||||
@@ -21,52 +24,365 @@ use Request;
|
||||
* @property int|null $size 大小(B)
|
||||
* @property int|null $userid 拥有者ID
|
||||
* @property int|null $share 是否共享
|
||||
* @property int|null $guest_access 是否允许游客访问
|
||||
* @property int|null $pshare 所属分享ID
|
||||
* @property int|null $created_id 创建者
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File newQuery()
|
||||
* @method static \Illuminate\Database\Query\Builder|File onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereCid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereExt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereGuestAccess($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File wherePid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File wherePids($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File wherePshare($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereShare($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereSize($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereUserid($value)
|
||||
* @method static \Illuminate\Database\Query\Builder|File withTrashed()
|
||||
* @method static \Illuminate\Database\Query\Builder|File withoutTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File withTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|File withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class File extends AbstractModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* 文件文件
|
||||
*/
|
||||
const codeExt = [
|
||||
'txt',
|
||||
'htaccess', 'htgroups', 'htpasswd', 'conf', 'bat', 'cmd', 'cpp', 'c', 'cc', 'cxx', 'h', 'hh', 'hpp', 'ino', 'cs', 'css',
|
||||
'dockerfile', 'go', 'golang', 'html', 'htm', 'xhtml', 'vue', 'we', 'wpy', 'java', 'js', 'jsm', 'jsx', 'json', 'jsp', 'less', 'lua', 'makefile', 'gnumakefile',
|
||||
'ocamlmakefile', 'make', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
|
||||
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
|
||||
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
|
||||
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
|
||||
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx', 'plist'
|
||||
];
|
||||
|
||||
/**
|
||||
* office文件
|
||||
*/
|
||||
const officeExt = [
|
||||
// 文本文件
|
||||
'doc', 'docx', // Microsoft Word 文档
|
||||
'dot', 'dotx', // Word 模板
|
||||
'odt', // OpenDocument 文本格式
|
||||
'ott', // OpenDocument 文本模板
|
||||
'rtf', // 富文本格式
|
||||
|
||||
// 电子表格
|
||||
'xls', 'xlsx', // Microsoft Excel 电子表格
|
||||
'xlsm', // Excel 含宏的工作簿
|
||||
'xlt', 'xltx', // Excel 模板
|
||||
'ods', // OpenDocument 电子表格格式
|
||||
'ots', // OpenDocument 电子表格模板
|
||||
'csv', // 逗号分隔值
|
||||
'tsv', // 制表符分隔值
|
||||
|
||||
// 演示文稿
|
||||
'ppt', 'pptx', // Microsoft PowerPoint 演示文稿
|
||||
'pps', 'ppsx', // PowerPoint 幻灯片放映
|
||||
'pot', 'potx', // PowerPoint 模板
|
||||
'odp', // OpenDocument 演示文稿格式
|
||||
'otp', // OpenDocument 演示文稿模板
|
||||
];
|
||||
|
||||
/**
|
||||
* 图片文件
|
||||
*/
|
||||
const imageExt = [
|
||||
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp'
|
||||
];
|
||||
|
||||
/**
|
||||
* 本地媒体文件
|
||||
*/
|
||||
const localExt = [
|
||||
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw',
|
||||
'tif', 'tiff',
|
||||
'mp3', 'wav', 'mp4', 'flv',
|
||||
// 'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm', // 这一排是要转换的,无法使用本地播放
|
||||
];
|
||||
|
||||
/**
|
||||
* 压缩包下载大小限制
|
||||
*/
|
||||
const zipMaxSize = 1024 * 1024 * 1024; // 1G
|
||||
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
* @param user $user
|
||||
* @param int $pid
|
||||
* @param string $type
|
||||
* @param bool $isGetparent
|
||||
* @return array
|
||||
*/
|
||||
public function getFileList($user, int $pid, $type = "all", $isGetparent = true)
|
||||
{
|
||||
$permission = 1000;
|
||||
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
|
||||
$builder = File::wherePid($pid)
|
||||
->when($type == 'dir', function ($q) {
|
||||
$q->whereType('folder');
|
||||
});
|
||||
if ($pid > 0) {
|
||||
File::permissionFind($pid, $userids, 0, $permission);
|
||||
} else {
|
||||
$builder->whereUserid($user->userid);
|
||||
}
|
||||
//
|
||||
$array = $builder->take(500)->get()->toArray();
|
||||
foreach ($array as &$item) {
|
||||
$item['permission'] = $permission;
|
||||
}
|
||||
//
|
||||
if ($pid > 0) {
|
||||
// 遍历获取父级
|
||||
if ($isGetparent) {
|
||||
while ($pid > 0) {
|
||||
$file = File::whereId($pid)->first();
|
||||
if (empty($file)) {
|
||||
break;
|
||||
}
|
||||
$pid = $file->pid;
|
||||
$temp = $file->toArray();
|
||||
$temp['permission'] = $file->getPermission($userids);
|
||||
$array[] = $temp;
|
||||
}
|
||||
}
|
||||
// 去除没有权限的文件
|
||||
$isUnset = false;
|
||||
foreach ($array as $index1 => $item1) {
|
||||
if ($item1['permission'] === -1) {
|
||||
foreach ($array as $index2 => $item2) {
|
||||
if ($item2['pid'] === $item1['id']) {
|
||||
$array[$index2]['pid'] = 0;
|
||||
}
|
||||
}
|
||||
$isUnset = true;
|
||||
unset($array[$index1]);
|
||||
}
|
||||
}
|
||||
if ($isUnset) {
|
||||
$array = array_values($array);
|
||||
}
|
||||
} else {
|
||||
// 获取共享相关
|
||||
DB::statement("SET SQL_MODE=''");
|
||||
$pre = DB::connection()->getTablePrefix();
|
||||
$list = File::select(["files.*", DB::raw("MAX({$pre}file_users.permission) as permission")])
|
||||
->join('file_users', 'files.id', '=', 'file_users.file_id')
|
||||
->where('files.userid', '!=', $user->userid)
|
||||
->whereIn('file_users.userid', $userids)
|
||||
->groupBy('files.id')
|
||||
->take(100)
|
||||
->when($type == 'dir', function ($q) {
|
||||
$q->where('files.type', 'folder');
|
||||
})
|
||||
->get();
|
||||
if ($list->isNotEmpty()) {
|
||||
foreach ($list as $file) {
|
||||
$temp = $file->toArray();
|
||||
$temp['pid'] = 0;
|
||||
$array[] = $temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 图片直接返回预览地址
|
||||
foreach ($array as &$item) {
|
||||
$item = File::handleImageUrl($item);
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件内容(上传文件)
|
||||
* @param user $user
|
||||
* @param int $pid
|
||||
* @param string $webkitRelativePath
|
||||
* @param bool $overwrite
|
||||
* @return array
|
||||
*/
|
||||
public function contentUpload($user, int $pid, $webkitRelativePath, $overwrite = false)
|
||||
{
|
||||
$userid = $user->userid;
|
||||
if ($pid > 0) {
|
||||
if (File::wherePid($pid)->count() >= 300) {
|
||||
return Base::retError('每个文件夹里最多只能创建300个文件或文件夹');
|
||||
}
|
||||
$row = File::permissionFind($pid, $user, 1);
|
||||
$userid = $row->userid;
|
||||
} else {
|
||||
if (File::whereUserid($user->userid)->wherePid(0)->count() >= 300) {
|
||||
return Base::retError('每个文件夹里最多只能创建300个文件或文件夹');
|
||||
}
|
||||
}
|
||||
//
|
||||
$dirs = explode("/", $webkitRelativePath);
|
||||
$addItem = [];
|
||||
while (count($dirs) > 1) {
|
||||
$dirName = array_shift($dirs);
|
||||
if ($dirName) {
|
||||
AbstractModel::transaction(function () use ($dirName, $user, $userid, &$pid, &$addItem) {
|
||||
$dirRow = File::wherePid($pid)->whereType('folder')->whereName($dirName)->lockForUpdate()->first();
|
||||
if (empty($dirRow)) {
|
||||
$dirRow = File::createInstance([
|
||||
'pid' => $pid,
|
||||
'type' => 'folder',
|
||||
'name' => $dirName,
|
||||
'userid' => $userid,
|
||||
'created_id' => $user->userid,
|
||||
]);
|
||||
$dirRow->handleDuplicateName();
|
||||
if ($dirRow->saveBeforePP()) {
|
||||
$addItem[] = File::find($dirRow->id);
|
||||
}
|
||||
}
|
||||
if (empty($dirRow)) {
|
||||
throw new ApiException('创建文件夹失败');
|
||||
}
|
||||
$pid = $dirRow->id;
|
||||
});
|
||||
foreach ($addItem as $tmpRow) {
|
||||
$tmpRow->pushMsg('add', $tmpRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
$path = 'uploads/tmp/file/' . date("Ym") . '/';
|
||||
$data = Base::upload([
|
||||
"file" => Request::file('files'),
|
||||
"type" => 'more',
|
||||
"autoThumb" => false,
|
||||
"path" => $path,
|
||||
"quality" => true
|
||||
]);
|
||||
if (Base::isError($data)) {
|
||||
throw new ApiException($data['msg']);
|
||||
}
|
||||
$data = $data['data'];
|
||||
//
|
||||
$type = match ($data['ext']) {
|
||||
'text', 'md', 'markdown' => 'document',
|
||||
'drawio' => 'drawio',
|
||||
'mind' => 'mind',
|
||||
'doc', 'docx', 'dot', 'dotx', 'odt', 'ott', 'rtf' => "word",
|
||||
'xls', 'xlsx', 'xlsm', 'xlt', 'xltx', 'ods', 'ots', 'csv', 'tsv' => "excel",
|
||||
'ppt', 'pptx', 'pps', 'ppsx', 'pot', 'potx', 'odp', 'otp' => "ppt",
|
||||
'wps' => "wps",
|
||||
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw', 'svg' => "picture",
|
||||
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z', 'gz', 'apk', 'dmg' => "archive",
|
||||
'tif', 'tiff' => "tif",
|
||||
'dwg', 'dxf' => "cad",
|
||||
'ofd' => "ofd",
|
||||
'pdf' => "pdf",
|
||||
'txt' => "txt",
|
||||
'htaccess', 'htgroups', 'htpasswd', 'conf', 'bat', 'cmd', 'cpp', 'c', 'cc', 'cxx', 'h', 'hh', 'hpp', 'ino', 'cs', 'css',
|
||||
'dockerfile', 'go', 'golang', 'html', 'htm', 'xhtml', 'vue', 'we', 'wpy', 'java', 'js', 'jsm', 'jsx', 'json', 'jsp', 'less', 'lua', 'makefile', 'gnumakefile',
|
||||
'ocamlmakefile', 'make', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
|
||||
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
|
||||
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
|
||||
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
|
||||
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx', 'plist' => "code",
|
||||
'mp3', 'wav', 'mp4', 'flv',
|
||||
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm' => "media",
|
||||
'xmind' => "xmind",
|
||||
'rp' => "axure",
|
||||
default => "",
|
||||
};
|
||||
if ($data['ext'] == 'markdown') {
|
||||
$data['ext'] = 'md';
|
||||
}
|
||||
$file = null;
|
||||
$params = [
|
||||
'pid' => $pid,
|
||||
'name' => Base::rightDelete($data['name'], '.' . $data['ext']),
|
||||
'type' => $type,
|
||||
'ext' => $data['ext'],
|
||||
'userid' => $userid,
|
||||
'created_id' => $user->userid,
|
||||
];
|
||||
if ($overwrite) {
|
||||
$file = self::wherePid($params['pid'])->whereExt($params['ext'])->whereName($params['name'])->first();
|
||||
}
|
||||
if (!$file) {
|
||||
$overwrite = false;
|
||||
$file = File::createInstance($params);
|
||||
$file->handleDuplicateName();
|
||||
}
|
||||
// 开始创建
|
||||
return AbstractModel::transaction(function () use ($overwrite, $addItem, $webkitRelativePath, $type, $user, $data, $file) {
|
||||
$file->size = $data['size'] * 1024;
|
||||
$file->saveBeforePP();
|
||||
//
|
||||
$data = Base::uploadMove($data, "uploads/file/" . $file->type . "/" . date("Ym") . "/" . $file->id . "/");
|
||||
$content = [
|
||||
'from' => '',
|
||||
'type' => $type,
|
||||
'ext' => $data['ext'],
|
||||
'url' => $data['path'],
|
||||
];
|
||||
if (isset($data['width'])) {
|
||||
$content['width'] = $data['width'];
|
||||
$content['height'] = $data['height'];
|
||||
}
|
||||
$content = FileContent::createInstance([
|
||||
'fid' => $file->id,
|
||||
'content' => $content,
|
||||
'text' => '',
|
||||
'size' => $file->size,
|
||||
'userid' => $user->userid,
|
||||
]);
|
||||
$content->save();
|
||||
//
|
||||
$tmpRow = File::find($file->id);
|
||||
$tmpRow->pushMsg('add', $tmpRow);
|
||||
//
|
||||
$data = File::handleImageUrl($tmpRow->toArray());
|
||||
$data['full_name'] = $webkitRelativePath ?: ($data['name'] . '.' . $data['ext']);
|
||||
$data['overwrite'] = $overwrite ? 1 : 0;
|
||||
//
|
||||
$addItem[] = $data;
|
||||
|
||||
return ['data' => $data, 'addItem' => $addItem];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有访问权限
|
||||
* @param $userid
|
||||
* @param array $userids
|
||||
* @return int -1:没有权限,0:访问权限,1:读写权限,1000:所有者或创建者
|
||||
*/
|
||||
public function getPermission($userid)
|
||||
public function getPermission(array $userids)
|
||||
{
|
||||
if ($userid == $this->userid || $userid == $this->created_id) {
|
||||
$validUserIds = array_filter($userids);
|
||||
if (in_array($this->userid, $validUserIds) || in_array($this->created_id, $validUserIds)) {
|
||||
// ① 自己的文件夹 或 自己创建的文件夹
|
||||
return 1000;
|
||||
}
|
||||
$row = $this->getShareInfo();
|
||||
if ($row) {
|
||||
$fileUser = FileUser::whereFileId($row->id)->where(function ($query) use ($userid) {
|
||||
$query->where('userid', 0);
|
||||
$query->orWhere('userid', $userid);
|
||||
})->orderByDesc('permission')->first();
|
||||
$fileUser = FileUser::whereFileId($row->id)->whereIn('userid', $userids)->orderByDesc('permission')->first();
|
||||
if ($fileUser) {
|
||||
// ② 在指定共享成员内
|
||||
return $fileUser->permission;
|
||||
@@ -100,7 +416,7 @@ class File extends AbstractModel
|
||||
|
||||
/**
|
||||
* 是否处于共享文件夹内(不含自身)
|
||||
* @return bool
|
||||
* @return File|false
|
||||
*/
|
||||
public function isNnShare()
|
||||
{
|
||||
@@ -111,19 +427,28 @@ class File extends AbstractModel
|
||||
break;
|
||||
}
|
||||
if ($row->share) {
|
||||
return true;
|
||||
return $row;
|
||||
}
|
||||
$pid = $row->pid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录内是否存在共享文件或文件夹
|
||||
* @return bool
|
||||
*/
|
||||
public function isSubShare()
|
||||
{
|
||||
return $this->type == 'folder' && File::where("pids", "like", "%,{$this->id},%")->whereShare(1)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置/关闭 共享(同时遍历取消里面的共享)
|
||||
* @param $share
|
||||
* @return bool
|
||||
*/
|
||||
public function setShare($share = null)
|
||||
public function updataShare($share = null)
|
||||
{
|
||||
if ($share === null) {
|
||||
$share = FileUser::whereFileId($this->id)->count() == 0 ? 0 : 1;
|
||||
@@ -131,11 +456,16 @@ class File extends AbstractModel
|
||||
if ($this->share != $share) {
|
||||
AbstractModel::transaction(function () use ($share) {
|
||||
$this->share = $share;
|
||||
$this->pshare = $share ? $this->id : 0;
|
||||
$this->save();
|
||||
File::where("pids", "like", "%,{$this->id},%")->update(['pshare' => $share ? $this->id : 0]);
|
||||
if ($share === 0) {
|
||||
FileUser::deleteFileAll($this->id, $this->userid);
|
||||
}
|
||||
$list = self::wherePid($this->id)->get();
|
||||
if ($list->isNotEmpty()) {
|
||||
foreach ($list as $item) {
|
||||
$item->setShare(0);
|
||||
$item->updataShare(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -143,6 +473,75 @@ class File extends AbstractModel
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理重名
|
||||
* @return void
|
||||
*/
|
||||
public function handleDuplicateName()
|
||||
{
|
||||
$builder = self::wherePid($this->pid)->whereUserid($this->userid)->whereExt($this->ext);
|
||||
$exist = $builder->clone()->whereName($this->name)->exists();
|
||||
if (!$exist) {
|
||||
return; // 未重名,不需要处理
|
||||
}
|
||||
// 发现重名,自动重命名
|
||||
$nextNum = 2;
|
||||
if (preg_match("/(.*?)(\s+\(\d+\))*$/", $this->name)) {
|
||||
$preName = preg_replace("/(.*?)(\s+\(\d+\))*$/", "$1", $this->name);
|
||||
$nextNum = $builder->clone()->where("name", "LIKE", "{$preName}%")->count() + 1;
|
||||
}
|
||||
$newName = "{$this->name} ({$nextNum})";
|
||||
if ($builder->clone()->whereName($newName)->exists()) {
|
||||
$nextNum = rand(100, 9999);
|
||||
$newName = "{$this->name} ({$nextNum})";
|
||||
}
|
||||
$this->name = $newName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前更新pids/pshare
|
||||
* @return bool
|
||||
*/
|
||||
public function saveBeforePP()
|
||||
{
|
||||
$pid = $this->pid;
|
||||
$pshare = $this->share ? $this->id : 0;
|
||||
$array = [];
|
||||
while ($pid > 0) {
|
||||
$array[] = $pid;
|
||||
$file = self::select(['id', 'pid', 'share'])->find($pid);
|
||||
if ($file) {
|
||||
$pid = $file->pid;
|
||||
if ($file->share) {
|
||||
$pshare = $file->id;
|
||||
}
|
||||
} else {
|
||||
$pid = 0;
|
||||
}
|
||||
}
|
||||
$opids = $this->pids;
|
||||
if ($array) {
|
||||
$array = array_values(array_reverse($array));
|
||||
$this->pids = ',' . implode(',', $array) . ',';
|
||||
} else {
|
||||
$this->pids = '';
|
||||
}
|
||||
$this->pshare = $pshare;
|
||||
if (!$this->save()) {
|
||||
return false;
|
||||
}
|
||||
// 更新子文件(夹)
|
||||
if ($opids != $this->pids) {
|
||||
self::wherePid($this->id)->chunkById(100, function ($lists) {
|
||||
/** @var self $item */
|
||||
foreach ($lists as $item) {
|
||||
$item->saveBeforePP();
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历删除文件(夹)
|
||||
* @return bool
|
||||
@@ -152,8 +551,7 @@ class File extends AbstractModel
|
||||
AbstractModel::transaction(function () {
|
||||
$this->delete();
|
||||
$this->pushMsg('delete');
|
||||
FileLink::whereFileId($this->id)->delete();
|
||||
FileUser::whereFileId($this->id)->delete();
|
||||
FileUser::deleteFileAll($this->id);
|
||||
FileContent::whereFid($this->id)->delete();
|
||||
$list = self::wherePid($this->id)->get();
|
||||
if ($list->isNotEmpty()) {
|
||||
@@ -165,6 +563,50 @@ class File extends AbstractModel
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制删除文件
|
||||
* @return true
|
||||
*/
|
||||
public function forceDeleteFile()
|
||||
{
|
||||
AbstractModel::transaction(function () {
|
||||
$this->forceDelete();
|
||||
FileContent::withTrashed()
|
||||
->whereFid($this->id)
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($contents) {
|
||||
/** @var FileContent $content */
|
||||
foreach ($contents as $content) {
|
||||
$content->forceDeleteContent();
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件分享链接
|
||||
* @param $userid
|
||||
* @param $refresh
|
||||
* @return array
|
||||
*/
|
||||
public function getShareLink($userid, $refresh = false)
|
||||
{
|
||||
if ($this->type == 'folder') {
|
||||
throw new ApiException('文件夹不支持分享');
|
||||
}
|
||||
return FileLink::generateLink($this->id, $userid, $refresh);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件名称加后缀
|
||||
* @return string|null
|
||||
*/
|
||||
public function getNameAndExt()
|
||||
{
|
||||
return $this->ext ? "{$this->name}.{$this->ext}" : $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送消息
|
||||
* @param $action
|
||||
@@ -179,19 +621,7 @@ class File extends AbstractModel
|
||||
];
|
||||
}
|
||||
//
|
||||
if ($userid === null) {
|
||||
$userid = [$this->userid];
|
||||
if ($this->share == 1) {
|
||||
$builder = WebSocket::select(['userid']);
|
||||
if ($action == 'content') {
|
||||
$builder->wherePath('file/content/' . $this->id);
|
||||
}
|
||||
$userid = array_merge($userid, $builder->pluck('userid')->toArray());
|
||||
} elseif ($this->share == 2) {
|
||||
$userid = array_merge($userid, FileUser::whereFileId($this->id)->pluck('userid')->toArray());
|
||||
}
|
||||
$userid = array_values(array_filter(array_unique($userid)));
|
||||
}
|
||||
$userid = $this->pushUserid($action, $userid);
|
||||
if (empty($userid)) {
|
||||
return;
|
||||
}
|
||||
@@ -215,28 +645,376 @@ class File extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件并检测权限
|
||||
* @param $id
|
||||
* @param int $limit 要求权限: 0-访问权限、1-读写权限、1000-所有者或创建者
|
||||
* @param $permission
|
||||
* 文件推送消息
|
||||
* @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
|
||||
* @param $userid
|
||||
* @return array|int[]|mixed|null[]
|
||||
*/
|
||||
public function pushUserid($action, $userid = null) {
|
||||
$wherePath = "/manage/file";
|
||||
if ($userid === null) {
|
||||
$array = [$this->userid];
|
||||
if ($action == 'add' && $this->pid == 0) {
|
||||
return $array;
|
||||
}
|
||||
if ($action == 'content') {
|
||||
$wherePath = "/single/file/{$this->id}";
|
||||
} elseif ($this->pid > 0) {
|
||||
$wherePath = "/manage/file/{$this->pid}";
|
||||
} else {
|
||||
$tmpArray = FileUser::whereFileId($this->id)->pluck('userid')->toArray();
|
||||
if (empty($tmpArray)) {
|
||||
return $array;
|
||||
}
|
||||
if (!in_array(0, $tmpArray)) {
|
||||
return $tmpArray;
|
||||
}
|
||||
}
|
||||
$tmpArray = WebSocket::wherePath($wherePath)->pluck('userid')->toArray();
|
||||
if (empty($tmpArray)) {
|
||||
return $array;
|
||||
}
|
||||
$array = array_values(array_filter(array_unique(array_merge($array, $tmpArray))));
|
||||
} else {
|
||||
$array = is_array($userid) ? $userid : [$userid];
|
||||
if (in_array(0, $array)) {
|
||||
return WebSocket::wherePath($wherePath)->pluck('userid')->toArray();
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* code获取文件ID、名称
|
||||
* @param $code
|
||||
* @return File
|
||||
*/
|
||||
public static function permissionFind($id, $limit = 0, &$permission = -1)
|
||||
public static function code2IdName($code) {
|
||||
$arr = explode(",", base64_decode($code));
|
||||
if (empty($arr)) {
|
||||
return null;
|
||||
}
|
||||
$fileId = intval($arr[0]);
|
||||
if (empty($fileId)) {
|
||||
return null;
|
||||
}
|
||||
return File::select(['id', 'name'])->find($fileId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理返回图片地址
|
||||
* @param array $item
|
||||
* @return array
|
||||
*/
|
||||
public static function handleImageUrl($item)
|
||||
{
|
||||
$file = File::find($id);
|
||||
if (in_array($item['ext'], self::imageExt) ) {
|
||||
$content = Base::json2array(FileContent::whereFid($item['id'])->orderByDesc('id')->value('content'));
|
||||
if ($content) {
|
||||
$item['image_url'] = Base::fillUrl($content['url']);
|
||||
$item['image_width'] = intval($content['width']);
|
||||
$item['image_height'] = intval($content['height']);
|
||||
}
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件并检测权限
|
||||
* @param int $id
|
||||
* @param User|array|int $user 要求权限的用户,如:[0, 1]
|
||||
* @param int $limit 要求权限: 0-访问权限、1-读写权限、1000-所有者或创建者
|
||||
* @param int $permission
|
||||
* @return File
|
||||
*/
|
||||
public static function permissionFind($id, $user, int $limit = 0, int &$permission = -1)
|
||||
{
|
||||
$file = File::find(intval($id));
|
||||
if (empty($file)) {
|
||||
throw new ApiException('文件不存在或已被删除');
|
||||
}
|
||||
//
|
||||
$permission = $file->getPermission(User::userid());
|
||||
if ($user instanceof User) {
|
||||
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
|
||||
} else {
|
||||
$userids = is_array($user) ? $user : [$user];
|
||||
}
|
||||
$permission = $file->getPermission($userids);
|
||||
if ($permission < $limit) {
|
||||
$msg = match ($limit) {
|
||||
1000 => '仅限所有者或创建者操作',
|
||||
1 => '没有读写权限',
|
||||
default => '没有访问权限',
|
||||
1 => '没有修改写入权限',
|
||||
default => '没有查看访问权限',
|
||||
};
|
||||
throw new ApiException($msg);
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化内容数据
|
||||
* @param array $data [path, size, ext, name]
|
||||
* @return array
|
||||
*/
|
||||
public static function formatFileData(array $data)
|
||||
{
|
||||
$fileName = $data['name'];
|
||||
$filePath = $data['path'];
|
||||
$fileSize = $data['size'];
|
||||
$fileExt = $data['ext'];
|
||||
$publicPath = public_path($filePath);
|
||||
//
|
||||
switch ($fileExt) {
|
||||
case 'md':
|
||||
case 'text':
|
||||
// 文本
|
||||
$data['content'] = [
|
||||
'type' => $fileExt,
|
||||
'content' => file_get_contents($publicPath) ?: 'Content deleted',
|
||||
];
|
||||
$data['file_mode'] = $fileExt;
|
||||
break;
|
||||
|
||||
case 'drawio':
|
||||
// 图表
|
||||
$data['content'] = [
|
||||
'xml' => file_get_contents($publicPath)
|
||||
];
|
||||
$data['file_mode'] = $fileExt;
|
||||
break;
|
||||
|
||||
case 'mind':
|
||||
// 思维导图
|
||||
$data['content'] = Base::json2array(file_get_contents($publicPath));
|
||||
$data['file_mode'] = $fileExt;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (in_array($fileExt, self::codeExt) && $fileSize < 2 * 1024 * 1024)
|
||||
{
|
||||
// 文本预览,限制2M内的文件
|
||||
$data['content'] = [
|
||||
'content' => file_get_contents($publicPath) ?: 'Content deleted',
|
||||
];
|
||||
$data['file_mode'] = 'code';
|
||||
}
|
||||
elseif (in_array($fileExt, File::officeExt))
|
||||
{
|
||||
// office预览
|
||||
$data['content'] = json_decode('{}');
|
||||
$data['file_mode'] = 'office';
|
||||
}
|
||||
else
|
||||
{
|
||||
// 其他预览
|
||||
$name = Base::rightDelete($fileName, ".{$fileExt}") . ".{$fileExt}";
|
||||
$data['content'] = [
|
||||
'preview' => true,
|
||||
'name' => $name,
|
||||
'key' => urlencode(Base::urlAddparameter($filePath, [
|
||||
'name' => $name,
|
||||
'ext' => $fileExt
|
||||
])),
|
||||
];
|
||||
$data['file_mode'] = 'preview';
|
||||
}
|
||||
break;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移交文件
|
||||
* @param $originalUserid
|
||||
* @param $newUserid
|
||||
* @return void
|
||||
*/
|
||||
public static function transfer($originalUserid, $newUserid)
|
||||
{
|
||||
if (!self::whereUserid($originalUserid)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建一个文件夹存放移交的文件
|
||||
$name = User::userid2nickname($originalUserid) ?: ('ID:' . $originalUserid);
|
||||
$file = File::createInstance([
|
||||
'pid' => 0,
|
||||
'name' => "【{$name}】移交的文件",
|
||||
'type' => "folder",
|
||||
'ext' => "",
|
||||
'userid' => $newUserid,
|
||||
'created_id' => 0,
|
||||
]);
|
||||
$file->handleDuplicateName();
|
||||
$file->saveBeforePP();
|
||||
|
||||
// 移交文件
|
||||
self::whereUserid($originalUserid)->chunkById(100, function($list) use ($file, $newUserid) {
|
||||
/** @var self $item */
|
||||
foreach ($list as $item) {
|
||||
if ($item->pid === 0) {
|
||||
$item->pid = $file->id;
|
||||
}
|
||||
$item->userid = $newUserid;
|
||||
$item->saveBeforePP();
|
||||
}
|
||||
});
|
||||
|
||||
// 移交文件权限
|
||||
FileUser::whereUserid($originalUserid)->chunkById(100, function ($list) use ($newUserid) {
|
||||
/** @var FileUser $item */
|
||||
foreach ($list as $item) {
|
||||
$row = FileUser::whereFileId($item->file_id)->whereUserid($newUserid)->first();
|
||||
if ($row) {
|
||||
// 已存在则删除原数据,判断改变已存在的数据
|
||||
$row->permission = max($row->permission, $item->permission);
|
||||
$row->save();
|
||||
$item->delete();
|
||||
} else {
|
||||
// 不存在则改变原数据
|
||||
$item->userid = $newUserid;
|
||||
$item->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件树并计算文件总大小
|
||||
*
|
||||
* @param int $fileId
|
||||
* @param User $user
|
||||
* @param int $permission 0-访问权限、1-读写权限、1000-所有者或创建者
|
||||
* @param string $path
|
||||
* @param int $totalSize
|
||||
* @return object
|
||||
*/
|
||||
public static function getFilesTree(int $fileId, User $user, $permission = 1, $path = '', &$totalSize = 0) {
|
||||
$file = File::permissionFind($fileId, $user, $permission);
|
||||
$file->path = ltrim($path . '/' . $file->name, '/');
|
||||
$file->children = [];
|
||||
if ($file->type == 'folder') {
|
||||
$files = $file->getFileList($user, $fileId, 'all', false);
|
||||
foreach ($files as &$childFile) {
|
||||
$childFile['path'] = $file->path . '/' . $childFile['name'];
|
||||
if ($childFile['type'] == 'folder') {
|
||||
$childFile['children'] = self::getFilesTree($childFile['id'], $user, $permission, $file->path, $totalSize);
|
||||
} else {
|
||||
$totalSize += $childFile['size'];
|
||||
}
|
||||
}
|
||||
$file->children = $files;
|
||||
} else {
|
||||
$totalSize += $file->size;
|
||||
}
|
||||
$file->totalSize = $totalSize;
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件夹文件添加到压缩文件
|
||||
*
|
||||
* @param \ZipArchive $zip
|
||||
* @param object $file
|
||||
* @return void
|
||||
*/
|
||||
public static function addFileTreeToZip($zip, $file)
|
||||
{
|
||||
if ($file->type != 'folder' && $file->name != '') {
|
||||
$content = FileContent::whereFid($file->id)->orderByDesc('id')->first();
|
||||
$content = Base::json2array($content?->content ?: []);
|
||||
$typeExtensions = [
|
||||
'word' => 'docx',
|
||||
'excel' => 'xlsx',
|
||||
'ppt' => 'pptx',
|
||||
];
|
||||
if (array_key_exists($file->type, $typeExtensions)) {
|
||||
$filePath = empty($content) ? public_path('assets/office/empty.' . $typeExtensions[$file->type]) : public_path($content['url']);
|
||||
}
|
||||
//
|
||||
$relativePath = $file->path . '.' . $file->ext;
|
||||
if (file_exists($filePath)) {
|
||||
$zip->addFile($filePath, $relativePath);
|
||||
} else {
|
||||
if (empty($content['url'])) {
|
||||
$zip->addFromString($relativePath, $content['content']);
|
||||
} else {
|
||||
$filePath = public_path($content['url']);
|
||||
$zip->addFile($filePath, $relativePath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isset($file->children)) {
|
||||
foreach ($file->children as $childFile) {
|
||||
try {
|
||||
self::addFileTreeToZip($zip, (object)$childFile);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 在压缩包中创建文件夹
|
||||
$zip->addEmptyDir($file->path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件类型判断是否需要安装应用
|
||||
* @param $type
|
||||
* @return void
|
||||
*/
|
||||
public static function isNeedInstallApp($type): void
|
||||
{
|
||||
// 文件类型与应用的映射配置
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Timer;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* App\Models\FileContent
|
||||
@@ -19,10 +19,16 @@ use Response;
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|FileContent newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent newQuery()
|
||||
* @method static \Illuminate\Database\Query\Builder|FileContent onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereContent($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereDeletedAt($value)
|
||||
@@ -32,8 +38,8 @@ use Response;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereText($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereUserid($value)
|
||||
* @method static \Illuminate\Database\Query\Builder|FileContent withTrashed()
|
||||
* @method static \Illuminate\Database\Query\Builder|FileContent withoutTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent withTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class FileContent extends AbstractModel
|
||||
@@ -41,11 +47,67 @@ class FileContent extends AbstractModel
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* 获取格式内容(或下载)
|
||||
* 强制删除文件内容
|
||||
* @return void
|
||||
*/
|
||||
public function forceDeleteContent()
|
||||
{
|
||||
$this->forceDelete();
|
||||
$content = Base::json2array($this->content ?: []);
|
||||
if (str_starts_with($content['url'], 'uploads/')) {
|
||||
$path = public_path($content['url']);
|
||||
if (file_exists($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转预览地址
|
||||
* @param array $array
|
||||
* @return string
|
||||
*/
|
||||
public static function toPreviewUrl($array)
|
||||
{
|
||||
$fileExt = $array['ext'];
|
||||
$fileName = $array['name'];
|
||||
$filePath = $array['path'];
|
||||
$name = Base::rightDelete($fileName, ".{$fileExt}") . ".{$fileExt}";
|
||||
$key = urlencode(Base::urlAddparameter($filePath, [
|
||||
'name' => $name,
|
||||
'ext' => $fileExt
|
||||
]));
|
||||
return Base::fillUrl("online/preview/{$name}?key={$key}&version=" . Base::getVersion() . "&__=" . Timer::msecTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转预览地址
|
||||
* @param File $file
|
||||
* @param $content
|
||||
* @return string
|
||||
*/
|
||||
public static function formatPreview($file, $content)
|
||||
{
|
||||
$content = Base::json2array($content ?: []);
|
||||
$filePath = $content['url'];
|
||||
if (in_array($file->type, ['word', 'excel', 'ppt'])) {
|
||||
if (empty($content)) {
|
||||
$filePath = 'assets/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type);
|
||||
}
|
||||
}
|
||||
return self::toPreviewUrl([
|
||||
'ext' => $file->ext,
|
||||
'name' => $file->name,
|
||||
'path' => $filePath,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取格式内容(或下载)
|
||||
* @param $file
|
||||
* @param $content
|
||||
* @param $download
|
||||
* @return array|\Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
* @return array|StreamedResponse
|
||||
*/
|
||||
public static function formatContent($file, $content, $download = false)
|
||||
{
|
||||
@@ -53,47 +115,64 @@ class FileContent extends AbstractModel
|
||||
$content = Base::json2array($content ?: []);
|
||||
if (in_array($file->type, ['word', 'excel', 'ppt'])) {
|
||||
if (empty($content)) {
|
||||
return Response::download(resource_path('assets/statics/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type)), $name);
|
||||
$filePath = public_path('assets/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type));
|
||||
} else {
|
||||
$filePath = public_path($content['url']);
|
||||
}
|
||||
return Response::download(public_path($content['url']), $name);
|
||||
return Base::DownloadFileResponse($filePath, $name);
|
||||
}
|
||||
if (empty($content)) {
|
||||
$content = match ($file->type) {
|
||||
'document' => [
|
||||
"type" => "md",
|
||||
"type" => $file->ext,
|
||||
"content" => "",
|
||||
],
|
||||
default => json_decode('{}'),
|
||||
};
|
||||
if ($download) {
|
||||
abort(403, "This file is empty.");
|
||||
}
|
||||
abort_if($download, 403, "This file is empty.");
|
||||
} else {
|
||||
$content['preview'] = false;
|
||||
$path = $content['url'];
|
||||
if ($file->ext) {
|
||||
$filePath = public_path($content['url']);
|
||||
if (in_array($file->type, ['txt', 'code']) && $file->size < 2 * 1024 * 1024) {
|
||||
// 支持编辑,限制2M内的文件
|
||||
$content['content'] = file_get_contents($filePath);
|
||||
} else {
|
||||
// 支持预览
|
||||
if (in_array($file->type, ['picture', 'image', 'tif', 'media'])) {
|
||||
$url = Base::fillUrl($content['url']);
|
||||
} else {
|
||||
$url = 'http://' . env('APP_IPPR') . '.3/' . $content['url'];
|
||||
}
|
||||
$content['url'] = base64_encode($url);
|
||||
$content['preview'] = true;
|
||||
}
|
||||
$res = File::formatFileData([
|
||||
'path' => $path,
|
||||
'ext' => $file->ext,
|
||||
'size' => $file->size,
|
||||
'name' => $file->name,
|
||||
]);
|
||||
$content = $res['content'];
|
||||
} else {
|
||||
$content['preview'] = false;
|
||||
}
|
||||
if ($download) {
|
||||
if (isset($filePath)) {
|
||||
return Response::download($filePath, $name);
|
||||
} else {
|
||||
abort(403, "This file not support download.");
|
||||
}
|
||||
$filePath = public_path($path);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,35 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\FileLink
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $file_id 项目ID
|
||||
* @property int|null $file_id 文件ID
|
||||
* @property int|null $num 累计访问
|
||||
* @property string|null $code 链接码
|
||||
* @property int|null $userid 会员ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\File|null $file
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|FileLink newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereCode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereFileId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class FileLink extends AbstractModel
|
||||
@@ -32,4 +42,35 @@ class FileLink extends AbstractModel
|
||||
{
|
||||
return $this->hasOne(File::class, 'id', 'file_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成链接
|
||||
* @param $fileId
|
||||
* @param $userid
|
||||
* @param $refresh
|
||||
* @return array
|
||||
*/
|
||||
public static function generateLink($fileId, $userid, $refresh = false)
|
||||
{
|
||||
$fileLink = FileLink::whereFileId($fileId)->whereUserid($userid)->first();
|
||||
if (empty($fileLink)) {
|
||||
$fileLink = FileLink::createInstance([
|
||||
'file_id' => $fileId,
|
||||
'userid' => $userid,
|
||||
'code' => base64_encode("{$fileId},{$userid}," . Base::generatePassword()),
|
||||
]);
|
||||
$fileLink->save();
|
||||
} else {
|
||||
if ($refresh == 'yes') {
|
||||
$fileLink->code = base64_encode("{$fileId},{$userid}," . Base::generatePassword());
|
||||
$fileLink->save();
|
||||
}
|
||||
}
|
||||
return [
|
||||
'id' => $fileId,
|
||||
'url' => Base::fillUrl('single/file/' . $fileLink->code),
|
||||
'code' => $fileLink->code,
|
||||
'num' => $fileLink->num
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,15 @@ namespace App\Models;
|
||||
* @property int|null $permission 权限:0只读,1读写
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|FileUser newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileUser newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileUser query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereFileId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereId($value)
|
||||
@@ -25,4 +31,34 @@ namespace App\Models;
|
||||
*/
|
||||
class FileUser extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 删除所有共享成员(同时删除成员分享的链接)
|
||||
* @param $file_id
|
||||
* @param int $retain_link_userid 保留指定会员的链接
|
||||
* @return mixed
|
||||
*/
|
||||
public static function deleteFileAll($file_id, $retain_link_userid = 0)
|
||||
{
|
||||
return AbstractModel::transaction(function() use ($retain_link_userid, $file_id) {
|
||||
if ($retain_link_userid > 0) {
|
||||
FileLink::whereFileId($file_id)->where('userid', '!=', $retain_link_userid)->delete();
|
||||
} else {
|
||||
FileLink::whereFileId($file_id)->delete();
|
||||
}
|
||||
FileUser::whereFileId($file_id)->delete();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 删除指定共享成员(同时删除成员分享的链接)
|
||||
* @param $file_id
|
||||
* @param $userid
|
||||
* @return mixed
|
||||
*/
|
||||
public static function deleteFileUser($file_id, $userid)
|
||||
{
|
||||
return AbstractModel::transaction(function() use ($userid, $file_id) {
|
||||
FileLink::whereFileId($file_id)->whereUserid($userid)->delete();
|
||||
return self::whereFileId($file_id)->whereUserid($userid)->delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
97
app/Models/Meeting.php
Normal file
97
app/Models/Meeting.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Cache;
|
||||
use App\Module\Base;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\Meeting
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $meetingid 会议ID,不是数字
|
||||
* @property string|null $name 会议主题
|
||||
* @property string|null $channel 频道
|
||||
* @property int|null $userid 创建人
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Carbon|null $end_at
|
||||
* @property Carbon|null $deleted_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereChannel($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereEndAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereMeetingid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Meeting extends AbstractModel
|
||||
{
|
||||
const CACHE_KEY = 'meeting_share_link_code';
|
||||
const CACHE_EXPIRED_TIME = 6; // 小时
|
||||
|
||||
/**
|
||||
* 获取分享链接
|
||||
* @return mixed
|
||||
*/
|
||||
public function getShareLink()
|
||||
{
|
||||
$code = base64_encode("{$this->meetingid}" . Base::generatePassword());
|
||||
Cache::put(self::CACHE_KEY . '_' . $code, [
|
||||
'id' => $this->id,
|
||||
'meetingid' => $this->meetingid,
|
||||
'channel' => $this->channel,
|
||||
], Carbon::now()->addHours(self::CACHE_EXPIRED_TIME));
|
||||
return Base::fillUrl("meeting/{$this->meetingid}/" . $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分享信息
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getShareInfo($code)
|
||||
{
|
||||
if (Cache::has(self::CACHE_KEY . '_' . $code)) {
|
||||
return Cache::get(self::CACHE_KEY . '_' . $code);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存访客信息
|
||||
* @return void
|
||||
*/
|
||||
public static function setTouristInfo($data)
|
||||
{
|
||||
Cache::put(Meeting::CACHE_KEY . '_' . $data['uid'], [
|
||||
'uid' => $data['uid'],
|
||||
'userimg' => $data['userimg'],
|
||||
'nickname' => $data['nickname'],
|
||||
], Carbon::now()->addHours(self::CACHE_EXPIRED_TIME));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取访客信息
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getTouristInfo($touristId)
|
||||
{
|
||||
if (Cache::has(Meeting::CACHE_KEY . '_' . $touristId)) {
|
||||
return Cache::get(Meeting::CACHE_KEY . '_' . $touristId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
34
app/Models/MeetingMsg.php
Normal file
34
app/Models/MeetingMsg.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\MeetingMsg
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $meetingid 会议ID
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property int|null $msg_id 消息ID
|
||||
* @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|MeetingMsg newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg whereMeetingid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|MeetingMsg whereMsgId($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class MeetingMsg extends AbstractModel
|
||||
{
|
||||
function __construct(array $attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
$this->timestamps = false;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Tasks\PushTask;
|
||||
use Arr;
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
@@ -18,25 +19,37 @@ use Request;
|
||||
* @property string|null $name 名称
|
||||
* @property string|null $desc 描述、备注
|
||||
* @property int|null $userid 创建人
|
||||
* @property int|null $personal 是否个人项目
|
||||
* @property string|null $archive_method 自动归档方式
|
||||
* @property int|null $archive_days 自动归档天数
|
||||
* @property string|null $user_simple 成员总数|1,2,3
|
||||
* @property int|null $dialog_id 聊天会话ID
|
||||
* @property string|null $archived_at 归档时间
|
||||
* @property \Illuminate\Support\Carbon|null $archived_at 归档时间
|
||||
* @property int|null $archived_userid 归档会员
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property-read int $owner_userid
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectColumn[] $projectColumn
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectColumn> $projectColumn
|
||||
* @property-read int|null $project_column_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectLog[] $projectLog
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectLog> $projectLog
|
||||
* @property-read int|null $project_log_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectUser[] $projectUser
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectUser> $projectUser
|
||||
* @property-read int|null $project_user_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project allData($userid = null)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project authData($userid = null, $owner = null)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|Project newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project newQuery()
|
||||
* @method static \Illuminate\Database\Query\Builder|Project onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchiveDays($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchiveMethod($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereCreatedAt($value)
|
||||
@@ -45,10 +58,12 @@ use Request;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project wherePersonal($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUserSimple($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUserid($value)
|
||||
* @method static \Illuminate\Database\Query\Builder|Project withTrashed()
|
||||
* @method static \Illuminate\Database\Query\Builder|Project withoutTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project withTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Project extends AbstractModel
|
||||
@@ -113,6 +128,8 @@ class Project extends AbstractModel
|
||||
->select([
|
||||
'projects.*',
|
||||
'project_users.owner',
|
||||
'project_users.top_at',
|
||||
'project_users.sort',
|
||||
])
|
||||
->leftJoin('project_users', function ($leftJoin) use ($userid) {
|
||||
$leftJoin
|
||||
@@ -136,6 +153,8 @@ class Project extends AbstractModel
|
||||
->select([
|
||||
'projects.*',
|
||||
'project_users.owner',
|
||||
'project_users.top_at',
|
||||
'project_users.sort',
|
||||
])
|
||||
->join('project_users', 'projects.id', '=', 'project_users.project_id')
|
||||
->where('project_users.userid', $userid);
|
||||
@@ -200,9 +219,16 @@ class Project extends AbstractModel
|
||||
WebSocketDialogUser::updateInsert([
|
||||
'dialog_id' => $this->dialog_id,
|
||||
'userid' => $userid,
|
||||
]);
|
||||
], [
|
||||
'important' => 1
|
||||
], function () use ($userid) {
|
||||
return [
|
||||
'important' => 1,
|
||||
'bot' => User::isBot($userid) ? 1 : 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->delete();
|
||||
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,8 +268,8 @@ class Project extends AbstractModel
|
||||
$this->archived_at = null;
|
||||
$this->archived_userid = User::userid();
|
||||
$this->addLog("项目取消归档");
|
||||
$this->pushMsg('add', $this);
|
||||
ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->update([
|
||||
$this->pushMsg('recovery', $this);
|
||||
ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->change([
|
||||
'archived_at' => null,
|
||||
'archived_follow' => 0
|
||||
]);
|
||||
@@ -253,7 +279,7 @@ class Project extends AbstractModel
|
||||
$this->archived_userid = User::userid();
|
||||
$this->addLog("项目归档");
|
||||
$this->pushMsg('archived');
|
||||
ProjectTask::whereProjectId($this->id)->whereArchivedAt(null)->update([
|
||||
ProjectTask::whereProjectId($this->id)->whereArchivedAt(null)->change([
|
||||
'archived_at' => $archived_at,
|
||||
'archived_follow' => 1
|
||||
]);
|
||||
@@ -310,44 +336,65 @@ class Project extends AbstractModel
|
||||
/**
|
||||
* 推送消息
|
||||
* @param string $action
|
||||
* @param array|self $data 发送内容,默认为[id=>项目ID]
|
||||
* @param array|self $data 推送内容
|
||||
* @param array $userid 指定会员,默认为项目所有成员
|
||||
*/
|
||||
public function pushMsg($action, $data = null, $userid = null)
|
||||
{
|
||||
if ($data === null) {
|
||||
$data = ['id' => $this->id];
|
||||
} elseif ($data instanceof self) {
|
||||
// 处理数据
|
||||
if ($data instanceof self) {
|
||||
$data = $data->toArray();
|
||||
}
|
||||
//
|
||||
$array = [$userid, []];
|
||||
|
||||
$data = is_array($data) ? $data : [];
|
||||
$data['id'] = $this->id;
|
||||
$data['name'] = $this->name;
|
||||
$data['desc'] = $this->desc;
|
||||
|
||||
// 处理接收用户
|
||||
$recipients = [$userid, []];
|
||||
if ($userid === null) {
|
||||
$array[0] = $this->relationUserids();
|
||||
$recipients[0] = $this->relationUserids();
|
||||
} elseif (!is_array($userid)) {
|
||||
$array[0] = [$userid];
|
||||
$recipients[0] = [$userid];
|
||||
}
|
||||
//
|
||||
|
||||
// 移除不需要的字段
|
||||
unset($data['top_at']);
|
||||
|
||||
// 处理所有者权限
|
||||
if (isset($data['owner'])) {
|
||||
$owners = ProjectUser::whereProjectId($data['id'])->whereOwner(1)->pluck('userid')->toArray();
|
||||
$array = [array_intersect($array[0], $owners), array_diff($array[0], $owners)];
|
||||
$owners = ProjectUser::whereProjectId($data['id'])
|
||||
->whereOwner(1)
|
||||
->pluck('userid')
|
||||
->toArray();
|
||||
$recipients = [
|
||||
array_intersect($recipients[0], $owners),
|
||||
array_diff($recipients[0], $owners)
|
||||
];
|
||||
}
|
||||
//
|
||||
foreach ($array as $index => $item) {
|
||||
|
||||
// 发送推送
|
||||
foreach ($recipients as $index => $userids) {
|
||||
if (empty($userids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($index > 0) {
|
||||
$data['owner'] = 0;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'ignoreFd' => Request::header('fd'),
|
||||
'userid' => array_values($item),
|
||||
'userid' => array_values($userids),
|
||||
'msg' => [
|
||||
'type' => 'project',
|
||||
'action' => $action,
|
||||
'data' => $data,
|
||||
]
|
||||
];
|
||||
$task = new PushTask($params, false);
|
||||
Task::deliver($task);
|
||||
|
||||
Task::deliver(new PushTask($params, false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,34 +421,46 @@ class Project extends AbstractModel
|
||||
$idc = [];
|
||||
$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,
|
||||
'usertype' => trim($item['usertype']),
|
||||
'userlimit' => $userlimit,
|
||||
]);
|
||||
'columnid' => $columnid,
|
||||
], [], $isInsert);
|
||||
if ($flow) {
|
||||
$ids[] = $flow->id;
|
||||
if ($flow->id != $id) {
|
||||
@@ -413,6 +472,9 @@ class Project extends AbstractModel
|
||||
if ($flow->status == 'end') {
|
||||
$hasEnd = true;
|
||||
}
|
||||
if (!$isInsert) {
|
||||
$upTaskList[$flow->id] = $flow->status . "|" . $flow->name . "|" . $flow->color;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$hasStart) {
|
||||
@@ -427,6 +489,12 @@ class Project extends AbstractModel
|
||||
}
|
||||
});
|
||||
//
|
||||
foreach ($upTaskList as $id => $value) {
|
||||
ProjectTask::whereFlowItemId($id)->change([
|
||||
'flow_item_name' => $value
|
||||
]);
|
||||
}
|
||||
//
|
||||
$projectFlow = ProjectFlow::with(['projectFlowItem'])->whereProjectId($this->id)->find($projectFlow->id);
|
||||
$itemIds = $projectFlow->projectFlowItem->pluck('id')->toArray();
|
||||
foreach ($projectFlow->projectFlowItem as $item) {
|
||||
@@ -449,6 +517,93 @@ class Project extends AbstractModel
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建项目
|
||||
* @param $params
|
||||
* - name 项目名称
|
||||
* - desc
|
||||
* - flow
|
||||
* - personal
|
||||
* - columns
|
||||
* @return array
|
||||
*/
|
||||
public static function createProject($params, $userid)
|
||||
{
|
||||
$name = trim(Arr::get($params, 'name', ''));
|
||||
$desc = trim(Arr::get($params, 'desc', ''));
|
||||
$flow = trim(Arr::get($params, 'flow', 'close'));
|
||||
$isPersonal = intval(Arr::get($params, 'personal'));
|
||||
if (mb_strlen($name) < 2) {
|
||||
return Base::retError('项目名称不可以少于2个字');
|
||||
} elseif (mb_strlen($name) > 32) {
|
||||
return Base::retError('项目名称最多只能设置32个字');
|
||||
}
|
||||
if (mb_strlen($desc) > 255) {
|
||||
return Base::retError('项目介绍最多只能设置255个字');
|
||||
}
|
||||
// 列表
|
||||
$columns = explode(",", Arr::get($params, 'columns'));
|
||||
$insertColumns = [];
|
||||
$sort = 0;
|
||||
foreach ($columns AS $column) {
|
||||
$column = trim($column);
|
||||
if ($column) {
|
||||
$insertColumns[] = [
|
||||
'name' => $column,
|
||||
'sort' => $sort++,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (empty($insertColumns)) {
|
||||
$insertColumns[] = [
|
||||
'name' => 'Default',
|
||||
'sort' => 0,
|
||||
];
|
||||
}
|
||||
if (count($insertColumns) > 30) {
|
||||
return Base::retError('项目列表最多不能超过30个');
|
||||
}
|
||||
// 开始创建
|
||||
$project = Project::createInstance([
|
||||
'name' => $name,
|
||||
'desc' => $desc,
|
||||
'userid' => $userid,
|
||||
]);
|
||||
if ($isPersonal) {
|
||||
if (Project::whereUserid($userid)->wherePersonal(1)->exists()) {
|
||||
return Base::retError('个人项目已存在,无须重复创建');
|
||||
}
|
||||
$project->personal = 1;
|
||||
}
|
||||
AbstractModel::transaction(function() use ($flow, $insertColumns, $project) {
|
||||
$project->save();
|
||||
ProjectUser::createInstance([
|
||||
'project_id' => $project->id,
|
||||
'userid' => $project->userid,
|
||||
'owner' => 1,
|
||||
])->save();
|
||||
foreach ($insertColumns AS $column) {
|
||||
$column['project_id'] = $project->id;
|
||||
ProjectColumn::createInstance($column)->save();
|
||||
}
|
||||
$dialog = WebSocketDialog::createGroup($project->name, $project->userid, 'project');
|
||||
if (empty($dialog)) {
|
||||
throw new ApiException('创建项目聊天室失败');
|
||||
}
|
||||
$project->dialog_id = $dialog->id;
|
||||
$project->save();
|
||||
//
|
||||
if ($flow == 'open') {
|
||||
$project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-14,"name":"已取消","status":"end","color":"#999999","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0}]'));
|
||||
}
|
||||
});
|
||||
//
|
||||
$data = Project::find($project->id);
|
||||
$data->addLog("创建项目");
|
||||
$data->pushMsg('add', $data);
|
||||
return Base::retSuccess('添加成功', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息(用于判断会员是否存在项目内)
|
||||
* @param int $project_id
|
||||
|
||||
@@ -20,12 +20,18 @@ use Request;
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property-read \App\Models\Project|null $project
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectTask[] $projectTask
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTask> $projectTask
|
||||
* @property-read int|null $project_task_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|ProjectColumn newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn newQuery()
|
||||
* @method static \Illuminate\Database\Query\Builder|ProjectColumn onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereColor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereDeletedAt($value)
|
||||
@@ -34,8 +40,8 @@ use Request;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereSort($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Query\Builder|ProjectColumn withTrashed()
|
||||
* @method static \Illuminate\Database\Query\Builder|ProjectColumn withoutTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn withTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectColumn extends AbstractModel
|
||||
@@ -68,7 +74,9 @@ class ProjectColumn extends AbstractModel
|
||||
AbstractModel::transaction(function () use ($pushMsg) {
|
||||
$tasks = ProjectTask::whereColumnId($this->id)->get();
|
||||
foreach ($tasks as $task) {
|
||||
$task->deleteTask($pushMsg);
|
||||
if(!$task->archived_at){
|
||||
$task->deleteTask($pushMsg);
|
||||
}
|
||||
}
|
||||
$this->delete();
|
||||
$this->addLog("删除列表:" . $this->name);
|
||||
@@ -119,7 +127,7 @@ class ProjectColumn extends AbstractModel
|
||||
$userid = $this->project->relationUserids();
|
||||
}
|
||||
$params = [
|
||||
'ignoreFd' => Request::header('fd'),
|
||||
'ignoreFd' => $action == 'recovery' ? 0 : Request::header('fd'),
|
||||
'userid' => $userid,
|
||||
'msg' => [
|
||||
'type' => 'projectColumn',
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectFlow
|
||||
*
|
||||
@@ -12,11 +10,17 @@ use App\Module\Base;
|
||||
* @property string|null $name 流程名称
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectFlowItem[] $projectFlowItem
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectFlowItem> $projectFlowItem
|
||||
* @property-read int|null $project_flow_item_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|ProjectFlow newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereName($value)
|
||||
|
||||
@@ -12,17 +12,27 @@ 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 array $userids 状态负责人ID
|
||||
* @property string|null $usertype 流转模式
|
||||
* @property int|null $userlimit 限制负责人
|
||||
* @property int|null $columnid 对应的项目列表
|
||||
* @property int|null $sort 排序
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\ProjectFlow|null $projectFlow
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|ProjectFlowItem newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereColor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereColumnid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereFlowId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereId($value)
|
||||
@@ -81,7 +91,7 @@ class ProjectFlowItem extends AbstractModel
|
||||
*/
|
||||
public function deleteFlowItem()
|
||||
{
|
||||
ProjectTask::whereFlowItemId($this->id)->update([
|
||||
ProjectTask::whereFlowItemId($this->id)->change([
|
||||
'flow_item_id' => 0,
|
||||
'flow_item_name' => "",
|
||||
]);
|
||||
|
||||
@@ -13,9 +13,15 @@ namespace App\Models;
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read bool $already
|
||||
* @property-read \App\Models\Project|null $project
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereCode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereId($value)
|
||||
|
||||
@@ -10,7 +10,8 @@ use App\Module\Base;
|
||||
* @property int $id
|
||||
* @property int|null $project_id 项目ID
|
||||
* @property int|null $column_id 列表ID
|
||||
* @property int|null $task_id 项目ID
|
||||
* @property int|null $task_id 任务ID
|
||||
* @property int|null $task_only 仅任务日志:0否,1是
|
||||
* @property int|null $userid 会员ID
|
||||
* @property string|null $detail 详细信息
|
||||
* @property array $record 记录数据
|
||||
@@ -18,9 +19,15 @@ use App\Module\Base;
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\ProjectTask|null $projectTask
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|ProjectLog newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereColumnId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereDetail($value)
|
||||
@@ -28,12 +35,16 @@ use App\Module\Base;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereRecord($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereTaskOnly($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectLog extends AbstractModel
|
||||
{
|
||||
protected $hidden = [
|
||||
'task_only',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
|
||||
211
app/Models/ProjectPermission.php
Normal file
211
app/Models/ProjectPermission.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectPermission
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $project_id 项目ID
|
||||
* @property array $permissions 权限
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission wherePermissions($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectPermission extends AbstractModel
|
||||
{
|
||||
|
||||
const TASK_LIST_ADD = 'task_list_add'; // 添加列
|
||||
const TASK_LIST_UPDATE = 'task_list_update'; // 修改列
|
||||
const TASK_LIST_REMOVE = 'task_list_remove'; // 删除列
|
||||
const TASK_LIST_SORT = 'task_list_sort'; // 列表排序
|
||||
const TASK_ADD = 'task_add'; // 任务添加
|
||||
const TASK_UPDATE = 'task_update'; // 任务更新
|
||||
const TASK_TIME = 'task_time'; // 任务时间
|
||||
const TASK_STATUS = 'task_status'; // 任务状态
|
||||
const TASK_REMOVE = 'task_remove'; // 任务删除
|
||||
const TASK_ARCHIVED = 'task_archived'; // 任务归档
|
||||
const TASK_MOVE = 'task_move'; // 任务移动
|
||||
|
||||
// 权限列表
|
||||
const PERMISSIONS = [
|
||||
'project_leader' => 1, // 项目负责人
|
||||
'project_member' => 2, // 项目成员
|
||||
'task_leader' => 3, // 任务负责人
|
||||
'task_assist' => 4, // 任务协助人
|
||||
];
|
||||
|
||||
// 权限描述
|
||||
const PERMISSIONS_DESC = [
|
||||
1 => "项目负责人",
|
||||
2 => "项目成员",
|
||||
3 => "任务负责人",
|
||||
4 => "任务协助人",
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['project_id', 'permissions'];
|
||||
|
||||
/**
|
||||
* 权限
|
||||
* @param $value
|
||||
* @return array
|
||||
*/
|
||||
public function getPermissionsAttribute($value)
|
||||
{
|
||||
return Base::json2array($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限值
|
||||
*
|
||||
* @param int $projectId
|
||||
* @param string $key
|
||||
* @return object|array
|
||||
*/
|
||||
public static function getPermission($projectId, $key = '')
|
||||
{
|
||||
$projectPermission = self::initPermissions($projectId);
|
||||
$currentPermissions = $projectPermission->permissions;
|
||||
if ($key) {
|
||||
if (!isset($currentPermissions[$key])) {
|
||||
throw new ApiException('项目权限设置不存在');
|
||||
}
|
||||
return $currentPermissions[$key];
|
||||
}
|
||||
return $projectPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化项目权限
|
||||
*
|
||||
* @param int $projectId
|
||||
* @return ProjectPermission
|
||||
*/
|
||||
public static function initPermissions($projectId)
|
||||
{
|
||||
$permissions = [
|
||||
self::TASK_LIST_ADD => $projectTaskList = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['project_member']],
|
||||
self::TASK_LIST_UPDATE => $projectTaskList,
|
||||
self::TASK_LIST_REMOVE => [self::PERMISSIONS['project_leader']],
|
||||
self::TASK_LIST_SORT => $projectTaskList,
|
||||
self::TASK_ADD => $projectTaskList,
|
||||
self::TASK_UPDATE => $taskUpdate = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader'], self::PERMISSIONS['task_assist']],
|
||||
self::TASK_TIME => $taskUpdate,
|
||||
self::TASK_STATUS => $taskStatus = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader']],
|
||||
self::TASK_REMOVE => $taskStatus,
|
||||
self::TASK_ARCHIVED => $taskStatus,
|
||||
self::TASK_MOVE => $taskStatus
|
||||
];
|
||||
return self::firstOrCreate(
|
||||
['project_id' => $projectId],
|
||||
['permissions' => Base::array2json($permissions)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项目权限
|
||||
*
|
||||
* @param int $projectId
|
||||
* @param $newPermissions
|
||||
* @return ProjectPermission
|
||||
*/
|
||||
public static function updatePermissions($projectId, $newPermissions)
|
||||
{
|
||||
$projectPermission = self::initPermissions($projectId);
|
||||
$currentPermissions = $projectPermission->permissions;
|
||||
$mergedPermissions = empty($newPermissions) ? $currentPermissions : array_merge($currentPermissions, $newPermissions);
|
||||
|
||||
$projectPermission->permissions = Base::array2json($mergedPermissions);
|
||||
$projectPermission->save();
|
||||
|
||||
return $projectPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有执行特定动作的权限
|
||||
* @param Project $project 项目实例
|
||||
* @param string $action 动作名称
|
||||
* @param ProjectTask|null $task 任务实例
|
||||
* @return bool
|
||||
*/
|
||||
public static function userTaskPermission(Project $project, $action, ProjectTask $task = null)
|
||||
{
|
||||
$userid = User::userid();
|
||||
$permissions = self::getPermission($project->id, $action);
|
||||
switch ($action) {
|
||||
// 任务添加,任务更新, 任务状态, 任务删除, 任务完成, 任务归档, 任务移动
|
||||
case self::TASK_LIST_ADD:
|
||||
case self::TASK_LIST_UPDATE:
|
||||
case self::TASK_LIST_REMOVE:
|
||||
case self::TASK_LIST_SORT:
|
||||
case self::TASK_ADD:
|
||||
case self::TASK_UPDATE:
|
||||
case self::TASK_TIME:
|
||||
case self::TASK_STATUS:
|
||||
case self::TASK_REMOVE:
|
||||
case self::TASK_ARCHIVED:
|
||||
case self::TASK_MOVE:
|
||||
$verify = false;
|
||||
// 项目负责人
|
||||
if (in_array(self::PERMISSIONS['project_leader'], $permissions)) {
|
||||
if ($project->owner) {
|
||||
$verify = true;
|
||||
}
|
||||
}
|
||||
// 项目成员
|
||||
if (!$verify && in_array(self::PERMISSIONS['project_member'], $permissions)) {
|
||||
$user = ProjectUser::whereProjectId($project->id)->whereUserid(intval($userid))->first();
|
||||
if (!empty($user)) {
|
||||
$verify = true;
|
||||
}
|
||||
}
|
||||
// 任务负责人
|
||||
if (!$verify && $task && in_array(self::PERMISSIONS['task_leader'], $permissions)) {
|
||||
if ($task->isOwner()) {
|
||||
$verify = true;
|
||||
}
|
||||
}
|
||||
// 任务协助人
|
||||
if (!$verify && $task && in_array(self::PERMISSIONS['task_assist'], $permissions)) {
|
||||
if ($task->isAssister()) {
|
||||
$verify = true;
|
||||
}
|
||||
}
|
||||
//
|
||||
if (!$verify) {
|
||||
$desc = [];
|
||||
rsort($permissions);
|
||||
foreach ($permissions as $permission) {
|
||||
$desc[] = Doo::translate(self::PERMISSIONS_DESC[$permission]);
|
||||
}
|
||||
$desc = array_reverse($desc);
|
||||
throw new ApiException(sprintf("仅限%s操作", implode('、', $desc)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
67
app/Models/ProjectTag.php
Normal file
67
app/Models/ProjectTag.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTag
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $project_id 项目ID
|
||||
* @property string $name 标签名称
|
||||
* @property string|null $desc 标签描述
|
||||
* @property string|null $color 颜色
|
||||
* @property int $sort 排序
|
||||
* @property int $userid 创建人
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Project $project
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereColor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereDesc($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereSort($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTag extends AbstractModel
|
||||
{
|
||||
protected $hidden = [
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'name',
|
||||
'desc',
|
||||
'color',
|
||||
'sort',
|
||||
'userid'
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联项目
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function project()
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,30 +2,105 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Exceptions\ApiException;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTaskContent
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $project_id 项目ID
|
||||
* @property int|null $task_id 任务ID
|
||||
* @property int|null $userid 用户ID
|
||||
* @property string|null $desc 内容描述
|
||||
* @property string|null $content 内容
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereContent($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereDesc($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTaskContent extends AbstractModel
|
||||
{
|
||||
protected $hidden = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取内容详情
|
||||
* @return array
|
||||
*/
|
||||
public function getContentInfo()
|
||||
{
|
||||
$content = Base::json2array($this->content);
|
||||
if (isset($content['url'])) {
|
||||
$filePath = public_path($content['url']);
|
||||
$array = $this->toArray();
|
||||
$array['content'] = file_get_contents($filePath) ?: '';
|
||||
if ($array['content']) {
|
||||
$replace = Base::fillUrl('uploads');
|
||||
$array['content'] = str_replace('{{RemoteURL}}uploads', $replace, $array['content']);
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存任务详情至文件并返回文件路径
|
||||
* @param $task_id
|
||||
* @param $content
|
||||
* @return string
|
||||
*/
|
||||
public static function saveContent($task_id, $content)
|
||||
{
|
||||
@ini_set("pcre.backtrack_limit", 999999999);
|
||||
//
|
||||
$oldContent = $content;
|
||||
$path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/';
|
||||
//
|
||||
preg_match_all('/<img[^>]*?src=\\\\?["\']data:image\/(png|jpg|jpeg|webp);base64,(.*?)\\\\?["\']/s', $content, $matchs);
|
||||
foreach ($matchs[2] as $key => $text) {
|
||||
$tmpPath = $path . 'attached/';
|
||||
Base::makeDir(public_path($tmpPath));
|
||||
$tmpPath .= md5($text) . "." . $matchs[1][$key];
|
||||
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
|
||||
$paramet = getimagesize(public_path($tmpPath));
|
||||
$content = str_replace($matchs[0][$key], '<img src="{{RemoteURL}}' . $tmpPath . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
|
||||
}
|
||||
}
|
||||
preg_match_all('/(<img[^>]*?src=\\\\?["\'])(https?:\/\/[^\/]+\/)(uploads\/[^\s"\'>]+)(\\\\?["\'][^>]*?>)/i', $content, $matches);
|
||||
foreach ($matches[0] as $key => $fullMatch) {
|
||||
$filePath = public_path($matches[3][$key]);
|
||||
if (file_exists($filePath)) {
|
||||
$replacement = $matches[1][$key] . '{{RemoteURL}}' . $matches[3][$key] . $matches[4][$key];
|
||||
$content = str_replace($fullMatch, $replacement, $content);
|
||||
}
|
||||
}
|
||||
//
|
||||
$filePath = $path . md5($content);
|
||||
$publicPath = public_path($filePath);
|
||||
Base::makeDir(dirname($publicPath));
|
||||
$result = file_put_contents($publicPath, $content);
|
||||
if(!$result && $oldContent){
|
||||
throw new ApiException("保存任务详情至文件失败,请重试");
|
||||
}
|
||||
//
|
||||
return $filePath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use Cache;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTaskFile
|
||||
@@ -19,9 +20,17 @@ use App\Module\Base;
|
||||
* @property int|null $download 下载次数
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read int $height
|
||||
* @property-read int $width
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|ProjectTaskFile newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile whereDownload($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile whereExt($value)
|
||||
@@ -38,6 +47,11 @@ use App\Module\Base;
|
||||
*/
|
||||
class ProjectTaskFile extends AbstractModel
|
||||
{
|
||||
protected $appends = [
|
||||
'width',
|
||||
'height',
|
||||
];
|
||||
|
||||
/**
|
||||
* 地址
|
||||
* @param $value
|
||||
@@ -57,4 +71,50 @@ class ProjectTaskFile extends AbstractModel
|
||||
{
|
||||
return Base::fillUrl($value ?: Base::extIcon($this->ext));
|
||||
}
|
||||
|
||||
/**
|
||||
* 宽
|
||||
* @return int
|
||||
*/
|
||||
public function getWidthAttribute()
|
||||
{
|
||||
$this->generateSizeData();
|
||||
return $this->appendattrs['width'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 高
|
||||
* @return int
|
||||
*/
|
||||
public function getHeightAttribute()
|
||||
{
|
||||
$this->generateSizeData();
|
||||
return $this->appendattrs['height'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成尺寸数据
|
||||
*/
|
||||
private function generateSizeData()
|
||||
{
|
||||
if (!isset($this->appendattrs['width'])) {
|
||||
$width = -1;
|
||||
$height = -1;
|
||||
if (in_array($this->ext, ['jpg', 'jpeg', 'webp', 'gif', 'png'])) {
|
||||
$path = public_path($this->getRawOriginal('path'));
|
||||
[$width, $height] = Cache::remember("File::size-" . md5($path), now()->addDays(7), function () use ($path) {
|
||||
$width = -1;
|
||||
$height = -1;
|
||||
if (file_exists($path)) {
|
||||
$paramet = getimagesize($path);
|
||||
$width = $paramet[0];
|
||||
$height = $paramet[1];
|
||||
}
|
||||
return [$width, $height];
|
||||
});
|
||||
}
|
||||
$this->appendattrs['width'] = $width;
|
||||
$this->appendattrs['height'] = $height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
app/Models/ProjectTaskFlowChange.php
Normal file
40
app/Models/ProjectTaskFlowChange.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTaskFlowChange
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $task_id 任务ID
|
||||
* @property int|null $userid 会员ID
|
||||
* @property int|null $before_flow_item_id (变化前)工作流状态ID
|
||||
* @property string|null $before_flow_item_name (变化前)工作流状态名称
|
||||
* @property int|null $after_flow_item_id (变化后)工作流状态ID
|
||||
* @property string|null $after_flow_item_name (变化后)工作流状态名称
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|ProjectTaskFlowChange newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereAfterFlowItemId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereAfterFlowItemName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereBeforeFlowItemId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereBeforeFlowItemName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTaskFlowChange extends AbstractModel
|
||||
{
|
||||
|
||||
}
|
||||
45
app/Models/ProjectTaskPushLog.php
Normal file
45
app/Models/ProjectTaskPushLog.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTaskPushLog
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 用户id
|
||||
* @property int|null $task_id 任务id
|
||||
* @property int|null $type 提醒类型:0 任务开始提醒,1 距离到期提醒,2到期超时提醒
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|ProjectTaskPushLog newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog withTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTaskPushLog extends AbstractModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
}
|
||||
154
app/Models/ProjectTaskRelation.php
Normal file
154
app/Models/ProjectTaskRelation.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTaskRelation
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $task_id 任务ID
|
||||
* @property int $related_task_id 关联任务ID
|
||||
* @property string $direction 关系方向: mention/mentioned_by
|
||||
* @property int|null $dialog_id 来源会话ID
|
||||
* @property int|null $msg_id 来源消息ID
|
||||
* @property int|null $userid 提及人
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\ProjectTask|null $relatedTask
|
||||
* @property-read \App\Models\ProjectTask|null $task
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereDirection($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereMsgId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereRelatedTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskRelation whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTaskRelation extends AbstractModel
|
||||
{
|
||||
public const DIRECTION_MENTION = 'mention';
|
||||
public const DIRECTION_MENTIONED_BY = 'mentioned_by';
|
||||
|
||||
protected $fillable = [
|
||||
'task_id',
|
||||
'related_task_id',
|
||||
'direction',
|
||||
'dialog_id',
|
||||
'msg_id',
|
||||
'userid',
|
||||
];
|
||||
|
||||
public function task(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ProjectTask::class, 'task_id');
|
||||
}
|
||||
|
||||
public function relatedTask(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ProjectTask::class, 'related_task_id');
|
||||
}
|
||||
|
||||
public static function recordMentionsFromMessage(WebSocketDialogMsg $msg): void
|
||||
{
|
||||
if ($msg->type !== 'text') {
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = $msg->msg;
|
||||
if (!is_array($payload)) {
|
||||
$payload = Base::json2array($msg->getRawOriginal('msg'));
|
||||
}
|
||||
|
||||
$text = $payload['text'] ?? '';
|
||||
if (!$text || !preg_match_all('/<span class="mention task" data-id="(\d+)">#?(.*?)<\/span>/i', $text, $matches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$targetIds = array_values(array_unique(array_filter(array_map('intval', $matches[1] ?? []))));
|
||||
if (empty($targetIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sourceTasks = ProjectTask::with('project')->whereDialogId($msg->dialog_id)->get();
|
||||
if ($sourceTasks->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$targetTasks = ProjectTask::with('project')->whereIn('id', $targetIds)->get()->keyBy('id');
|
||||
if ($targetTasks->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pushTasks = [];
|
||||
foreach ($sourceTasks as $sourceTask) {
|
||||
foreach ($targetIds as $targetId) {
|
||||
if ($targetId === $sourceTask->id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetTask = $targetTasks->get($targetId);
|
||||
if (!$targetTask) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mentionRelation = static::updateOrCreate(
|
||||
[
|
||||
'task_id' => $sourceTask->id,
|
||||
'related_task_id' => $targetTask->id,
|
||||
'direction' => self::DIRECTION_MENTION,
|
||||
],
|
||||
[
|
||||
'dialog_id' => $msg->dialog_id,
|
||||
'msg_id' => $msg->id,
|
||||
'userid' => $msg->userid,
|
||||
]
|
||||
);
|
||||
|
||||
if ($mentionRelation->wasRecentlyCreated || $mentionRelation->wasChanged()) {
|
||||
$pushTasks[$sourceTask->id] = $sourceTask;
|
||||
}
|
||||
|
||||
$reverseRelation = static::updateOrCreate(
|
||||
[
|
||||
'task_id' => $targetTask->id,
|
||||
'related_task_id' => $sourceTask->id,
|
||||
'direction' => self::DIRECTION_MENTIONED_BY,
|
||||
],
|
||||
[
|
||||
'dialog_id' => $msg->dialog_id,
|
||||
'msg_id' => $msg->id,
|
||||
'userid' => $msg->userid,
|
||||
]
|
||||
);
|
||||
|
||||
if ($reverseRelation->wasRecentlyCreated || $reverseRelation->wasChanged()) {
|
||||
$pushTasks[$targetTask->id] = $targetTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pushTasks as $task) {
|
||||
$task->loadMissing('project');
|
||||
if (!$task->project) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$task->pushMsg('relation', null, null, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,15 @@ namespace App\Models;
|
||||
* @property string|null $color 颜色
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereColor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereId($value)
|
||||
|
||||
77
app/Models/ProjectTaskTemplate.php
Normal file
77
app/Models/ProjectTaskTemplate.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTaskTemplate
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $project_id 项目ID
|
||||
* @property string $name 模板名称
|
||||
* @property string|null $title 任务标题
|
||||
* @property string|null $content 任务内容
|
||||
* @property int $sort 排序
|
||||
* @property int $is_default 是否默认模板
|
||||
* @property int $userid 创建人
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Project $project
|
||||
* @property-read \App\Models\User $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereContent($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereIsDefault($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereSort($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereTitle($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTaskTemplate extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'name',
|
||||
'title',
|
||||
'content',
|
||||
'sort',
|
||||
'is_default',
|
||||
'userid'
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联项目
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function project()
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联创建者
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid');
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,16 @@ namespace App\Models;
|
||||
* @property int|null $owner 是否任务负责人
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\ProjectTask|null $projectTask
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|ProjectTaskUser newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser whereOwner($value)
|
||||
@@ -29,4 +36,56 @@ namespace App\Models;
|
||||
class ProjectTaskUser extends AbstractModel
|
||||
{
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function projectTask(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(ProjectTask::class, 'id', 'task_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 移交任务身份
|
||||
* @param $originalUserid
|
||||
* @param $newUserid
|
||||
* @return void
|
||||
*/
|
||||
public static function transfer($originalUserid, $newUserid)
|
||||
{
|
||||
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
|
||||
$tastIds = [];
|
||||
/** @var self $item */
|
||||
foreach ($list as $item) {
|
||||
$row = self::whereTaskId($item->task_id)->whereUserid($newUserid)->first();
|
||||
if ($row) {
|
||||
// 已存在则删除原数据,判断改变已存在的数据
|
||||
$row->owner = max($row->owner, $item->owner);
|
||||
$row->save();
|
||||
$item->delete();
|
||||
} else {
|
||||
// 不存在则改变原数据
|
||||
$item->userid = $newUserid;
|
||||
$item->save();
|
||||
}
|
||||
if ($item->projectTask) {
|
||||
$item->projectTask->addLog("移交{任务}身份", [
|
||||
'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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
43
app/Models/ProjectTaskVisibilityUser.php
Normal file
43
app/Models/ProjectTaskVisibilityUser.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTaskVisibilityUser
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $project_id 项目ID
|
||||
* @property int|null $task_id 任务ID
|
||||
* @property int|null $userid 成员ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\ProjectTask|null $projectTask
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|ProjectTaskVisibilityUser newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTaskVisibilityUser extends AbstractModel
|
||||
{
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function projectTask(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(ProjectTask::class, 'id', 'task_id');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,16 +11,26 @@ use App\Module\Base;
|
||||
* @property int|null $project_id 项目ID
|
||||
* @property int|null $userid 成员ID
|
||||
* @property int|null $owner 是否负责人
|
||||
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
|
||||
* @property int|null $sort 排序(ASC)
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Project|null $project
|
||||
* @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|ProjectUser newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereOwner($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereSort($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereTopAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
@@ -36,6 +46,68 @@ class ProjectUser extends AbstractModel
|
||||
return $this->hasOne(Project::class, 'id', 'project_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 移交项目身份
|
||||
* @param $originalUserid
|
||||
* @param $newUserid
|
||||
* @return void
|
||||
*/
|
||||
public static function transfer($originalUserid, $newUserid)
|
||||
{
|
||||
$projectIds = [];
|
||||
// 移交项目身份
|
||||
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid, &$projectIds) {
|
||||
/** @var self $item */
|
||||
foreach ($list as $item) {
|
||||
$row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first();
|
||||
if ($row) {
|
||||
// 已存在则删除原数据,判断改变已存在的数据
|
||||
$row->owner = max($row->owner, $item->owner);
|
||||
$row->save();
|
||||
$item->delete();
|
||||
} else {
|
||||
// 不存在则改变原数据
|
||||
$item->userid = $newUserid;
|
||||
$item->save();
|
||||
}
|
||||
if ($item->project) {
|
||||
if ($item->project->personal) {
|
||||
$name = User::userid2nickname($originalUserid) ?: ('ID:' . $originalUserid);
|
||||
$item->project->name = "【{$name}】{$item->project->name}";
|
||||
$item->project->save();
|
||||
}
|
||||
$item->project->addLog("移交项目身份", [
|
||||
'change' => [
|
||||
[
|
||||
'type' => 'user',
|
||||
'data' => $originalUserid
|
||||
],
|
||||
[
|
||||
'type' => 'user',
|
||||
'data' => $newUserid
|
||||
],
|
||||
],
|
||||
]);
|
||||
$item->project->syncDialogUser();
|
||||
$projectIds[] = $item->project_id;
|
||||
}
|
||||
}
|
||||
});
|
||||
// 移交工作流状态负责人
|
||||
if ($projectIds) {
|
||||
ProjectFlowItem::whereIn('project_id', $projectIds)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
|
||||
/** @var ProjectFlowItem $item */
|
||||
foreach ($list as $item) {
|
||||
if (in_array($originalUserid, $item->userids)) {
|
||||
$userids = array_values(array_diff($item->userids, [$originalUserid]));
|
||||
$item->userids = Base::array2json(array_merge($userids, [$newUserid]));
|
||||
$item->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出项目
|
||||
*/
|
||||
@@ -45,15 +117,13 @@ class ProjectUser extends AbstractModel
|
||||
->whereUserid($this->userid)
|
||||
->chunk(100, function ($list) {
|
||||
$tastIds = [];
|
||||
/** @var ProjectTaskUser $item */
|
||||
foreach ($list as $item) {
|
||||
$item->delete();
|
||||
if (!in_array($item->task_pid, $tastIds)) {
|
||||
$tastIds[] = $item->task_pid;
|
||||
$item->projectTask?->syncDialogUser();
|
||||
}
|
||||
$item->delete();
|
||||
}
|
||||
$tasks = ProjectTask::whereIn('id', $tastIds)->get();
|
||||
foreach ($tasks as $task) {
|
||||
$task->syncDialogUser();
|
||||
}
|
||||
});
|
||||
$this->delete();
|
||||
|
||||
@@ -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;
|
||||
@@ -23,15 +24,21 @@ use JetBrains\PhpStorm\Pure;
|
||||
* @property int $userid
|
||||
* @property string $content
|
||||
* @property string $sign 汇报唯一标识
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ReportReceive[] $Receives
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ReportReceive> $Receives
|
||||
* @property-read int|null $receives_count
|
||||
* @property-read mixed $receives
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\User[] $receivesUser
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $receivesUser
|
||||
* @property-read int|null $receives_user_count
|
||||
* @property-read \App\Models\User|null $sendUser
|
||||
* @method static \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 Builder|Report newModelQuery()
|
||||
* @method static Builder|Report newQuery()
|
||||
* @method static Builder|Report query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static Builder|Report whereContent($value)
|
||||
* @method static Builder|Report whereCreatedAt($value)
|
||||
* @method static Builder|Report whereId($value)
|
||||
@@ -68,7 +75,7 @@ class Report extends AbstractModel
|
||||
public function receivesUser(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, ReportReceive::class, "rid", "userid")
|
||||
->withPivot("receive_time", "read");
|
||||
->withPivot("receive_at", "read");
|
||||
}
|
||||
|
||||
public function sendUser()
|
||||
@@ -76,15 +83,6 @@ class Report extends AbstractModel
|
||||
return $this->hasOne(User::class, "userid", "userid");
|
||||
}
|
||||
|
||||
public function getTypeAttribute($value): string
|
||||
{
|
||||
return match ($value) {
|
||||
Report::WEEKLY => "周报",
|
||||
Report::DAILY => "日报",
|
||||
default => "",
|
||||
};
|
||||
}
|
||||
|
||||
public function getContentAttribute($value): string
|
||||
{
|
||||
return htmlspecialchars_decode($value);
|
||||
@@ -98,18 +96,34 @@ 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
|
||||
* @param User|null $user
|
||||
* @return Report|Builder|Model|object|null
|
||||
* @throw ApiException
|
||||
*/
|
||||
public static function getOne($id, User $user = null)
|
||||
public static function getOne($id)
|
||||
{
|
||||
$user === null && $user = User::auth();
|
||||
$one = self::whereUserid($user->userid)->whereId($id)->first();
|
||||
if ( empty($one) )
|
||||
$one = self::whereId($id)->first();
|
||||
if (empty($one))
|
||||
throw new ApiException("记录不存在");
|
||||
return $one;
|
||||
}
|
||||
@@ -144,12 +158,12 @@ class Report extends AbstractModel
|
||||
// 如果设置了周期偏移量
|
||||
empty( $offset ) || $now_dt->subWeeks( abs( $offset ) );
|
||||
$now_dt->startOfWeek(); // 设置为当周第一天
|
||||
return $now_dt->year . $now_dt->weekOfYear;
|
||||
return now()->year . $now_dt->weekOfYear;
|
||||
},
|
||||
Report::DAILY => function() use ($now_dt, $offset) {
|
||||
// 如果设置了周期偏移量
|
||||
empty( $offset ) || $now_dt->subDays( abs( $offset ) );
|
||||
return $now_dt->format("Ymd");
|
||||
return now()->format("Ymd");
|
||||
},
|
||||
default => "",
|
||||
};
|
||||
|
||||
86
app/Models/ReportLink.php
Normal file
86
app/Models/ReportLink.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\ReportLink
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $rid 报告ID
|
||||
* @property int|null $num 累计访问
|
||||
* @property string|null $code 链接码
|
||||
* @property int|null $userid 会员ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Report|null $report
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereRid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ReportLink extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function report(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(Report::class, 'id', 'report_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成链接
|
||||
* @param $rid
|
||||
* @param $userid
|
||||
* @param $refresh
|
||||
* @return array
|
||||
*/
|
||||
public static function generateLink($rid, $userid, $refresh = false)
|
||||
{
|
||||
$report = Report::find($rid);
|
||||
if (empty($report)) {
|
||||
throw new ApiException('报告不存在或已被删除');
|
||||
}
|
||||
if ($report->userid != $userid) {
|
||||
if (!ReportReceive::whereRid($rid)->whereUserid($userid)->exists()) {
|
||||
throw new ApiException('您没有权限查看该报告');
|
||||
}
|
||||
}
|
||||
$reportLink = ReportLink::whereRid($rid)->whereUserid($userid)->first();
|
||||
if (empty($reportLink)) {
|
||||
$reportLink = ReportLink::createInstance([
|
||||
'rid' => $rid,
|
||||
'userid' => $userid,
|
||||
'code' => base64_encode("{$rid},{$userid}," . Base::generatePassword()),
|
||||
]);
|
||||
$reportLink->save();
|
||||
} else {
|
||||
if ($refresh == 'yes') {
|
||||
$reportLink->code = base64_encode("{$rid},{$userid}," . Base::generatePassword());
|
||||
$reportLink->save();
|
||||
}
|
||||
}
|
||||
return [
|
||||
'id' => $rid,
|
||||
'url' => Base::fillUrl('single/report/detail/' . $reportLink->code),
|
||||
'code' => $reportLink->code,
|
||||
'num' => $reportLink->num
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,21 @@ use Illuminate\Database\Eloquent\Model;
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $rid
|
||||
* @property string|null $receive_time 接收时间
|
||||
* @property \Illuminate\Support\Carbon|null $receive_at 接收时间
|
||||
* @property int $userid 接收人
|
||||
* @property int $read 是否已读
|
||||
* @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|ReportReceive newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereRead($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereReceiveTime($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereReceiveAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereRid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
@@ -32,7 +38,7 @@ class ReportReceive extends AbstractModel
|
||||
|
||||
protected $fillable = [
|
||||
"rid",
|
||||
"receive_time",
|
||||
"receive_at",
|
||||
"userid",
|
||||
"read",
|
||||
];
|
||||
|
||||
@@ -2,18 +2,30 @@
|
||||
|
||||
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
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $name
|
||||
* @property string|null $desc 参数描述、备注
|
||||
* @property string|null $setting
|
||||
* @property array $setting
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @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|Setting newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Setting newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Setting query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Setting whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Setting whereDesc($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Setting whereId($value)
|
||||
@@ -24,5 +36,274 @@ namespace App\Models;
|
||||
*/
|
||||
class Setting extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 格式化设置参数
|
||||
* @param $value
|
||||
* @return array
|
||||
*/
|
||||
public function getSettingAttribute($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
$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';
|
||||
$value['image_quality'] = min(100, max(0, intval($value['image_quality']) ?: 90));
|
||||
$value['image_save_local'] = $value['image_save_local'] ?: 'open';
|
||||
if (!is_array($value['task_default_time']) || count($value['task_default_time']) != 2 || !Timer::isTime($value['task_default_time'][0]) || !Timer::isTime($value['task_default_time'][1])) {
|
||||
$value['task_default_time'] = ['09:00', '18:00'];
|
||||
}
|
||||
break;
|
||||
|
||||
// 文件设置
|
||||
case 'fileSetting':
|
||||
$value['permission_pack_type'] = $value['permission_pack_type'] ?: 'all';
|
||||
$value['permission_pack_userids'] = is_array($value['permission_pack_userids']) ? $value['permission_pack_userids'] : [];
|
||||
break;
|
||||
|
||||
// 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', 'grok', 'ollama', 'zhipu', 'qianwen', 'wenxin'];
|
||||
$fieldList = ['key', 'secret', 'models', 'model', 'base_url', 'agency', 'temperature', 'system'];
|
||||
foreach ($aiList as $aiName) {
|
||||
foreach ($fieldList as $fieldName) {
|
||||
$key = $aiName . '_' . $fieldName;
|
||||
$content = $value[$key] ? trim($value[$key]) : '';
|
||||
switch ($fieldName) {
|
||||
case 'models':
|
||||
if ($content) {
|
||||
$content = explode("\n", $content);
|
||||
$content = array_filter($content);
|
||||
}
|
||||
if (empty($content)) {
|
||||
$content = self::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] = $content;
|
||||
}
|
||||
}
|
||||
$value = $array;
|
||||
break;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否开启 AI 助理
|
||||
* @return bool
|
||||
*/
|
||||
public static function AIOpen()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱地址(过滤忽略地址)
|
||||
* @param $array
|
||||
* @param \Closure $resultClosure
|
||||
* @param \Closure|null $emptyClosure
|
||||
* @return array|mixed
|
||||
*/
|
||||
public static function validateAddr($array, $resultClosure, $emptyClosure = null)
|
||||
{
|
||||
if (!is_array($array)) {
|
||||
$array = [$array];
|
||||
}
|
||||
$ignoreAddr = Base::settingFind('emailSetting', 'ignore_addr');
|
||||
$ignoreAddr = explode("\n", $ignoreAddr);
|
||||
$ignoreArray = ['admin@dootask.com', 'test@dootask.com'];
|
||||
foreach ($ignoreAddr as $item) {
|
||||
if (Base::isEmail($item)) {
|
||||
$ignoreArray[] = trim($item);
|
||||
}
|
||||
}
|
||||
if ($ignoreArray) {
|
||||
$array = array_diff($array, $ignoreArray);
|
||||
}
|
||||
if ($array) {
|
||||
if ($resultClosure instanceof \Closure) {
|
||||
foreach ($array as $value) {
|
||||
$resultClosure($value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($emptyClosure instanceof \Closure) {
|
||||
$emptyClosure();
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息限制
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
app/Models/TaskWorker.php
Normal file
43
app/Models/TaskWorker.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* App\Models\TaskWorker
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $args
|
||||
* @property string|null $error
|
||||
* @property \Illuminate\Support\Carbon|null $start_at 开始时间
|
||||
* @property \Illuminate\Support\Carbon|null $end_at 结束时间
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|TaskWorker newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereArgs($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereEndAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereError($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereStartAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker withTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class TaskWorker extends AbstractModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
}
|
||||
@@ -11,9 +11,15 @@ namespace App\Models;
|
||||
* @property string|null $content
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Tmp newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Tmp newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Tmp query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Tmp whereContent($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Tmp whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Tmp whereId($value)
|
||||
|
||||
262
app/Models/UmengAlias.php
Normal file
262
app/Models/UmengAlias.php
Normal file
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
use Hedeqiang\UMeng\Android;
|
||||
use Hedeqiang\UMeng\IOS;
|
||||
|
||||
/**
|
||||
* App\Models\UmengAlias
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 会员ID
|
||||
* @property string|null $alias 别名
|
||||
* @property string|null $platform 平台类型
|
||||
* @property string|null $device 设备类型
|
||||
* @property string|null $device_hash 设备哈希值,用于关联UserDevice表
|
||||
* @property string|null $version 应用版本号
|
||||
* @property string|null $ua userAgent
|
||||
* @property int|null $is_notified 通知权限
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @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|UmengAlias newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @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 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
|
||||
* @return string
|
||||
*/
|
||||
private static function specialCharacters($string)
|
||||
{
|
||||
return str_replace(["\r\n", "\r", "\n"], '', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推送配置
|
||||
* @return array|false
|
||||
*/
|
||||
public static function getPushConfig()
|
||||
{
|
||||
$setting = Base::setting('appPushSetting');
|
||||
if ($setting['push'] !== 'open') {
|
||||
return false;
|
||||
}
|
||||
$config = [];
|
||||
if ($setting['ios_key']) {
|
||||
$config['iOS'] = [
|
||||
'appKey' => $setting['ios_key'],
|
||||
'appMasterSecret' => $setting['ios_secret'],
|
||||
'production_mode' => true,
|
||||
];
|
||||
}
|
||||
if ($setting['android_key']) {
|
||||
$config['Android'] = [
|
||||
'appKey' => $setting['android_key'],
|
||||
'appMasterSecret' => $setting['android_secret'],
|
||||
'production_mode' => true,
|
||||
];
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送消息
|
||||
* @param string $alias
|
||||
* @param string $platform
|
||||
* @param array $array [title, subtitle, body, description, extra, seconds, badge]
|
||||
* @return void
|
||||
*/
|
||||
private static function pushMsgToAlias($alias, $platform, $array)
|
||||
{
|
||||
$config = self::getPushConfig();
|
||||
if ($config === false) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
$title = self::specialCharacters($array['title'] ?: ''); // 标题
|
||||
$subtitle = self::specialCharacters($array['subtitle'] ?: ''); // 副标题(iOS)
|
||||
$body = self::specialCharacters($array['body'] ?: ''); // 通知内容
|
||||
$description = $array['description'] ?: 'no description'; // 描述
|
||||
$extra = is_array($array['extra']) ? $array['extra'] : []; // 额外参数
|
||||
$seconds = intval($array['seconds']) ?: 86400; // 有效时间(单位:秒)
|
||||
$badge = intval($array['badge']) ?: 0; // 角标数(iOS)
|
||||
//
|
||||
switch ($platform) {
|
||||
case 'ios':
|
||||
if (!isset($config['iOS'])) {
|
||||
return;
|
||||
}
|
||||
self::sendTask([
|
||||
'platform' => $platform,
|
||||
'config' => $config,
|
||||
'data' => [
|
||||
'description' => $description,
|
||||
'payload' => array_merge([
|
||||
'aps' => [
|
||||
'alert' => [
|
||||
'title' => $title,
|
||||
'subtitle' => $subtitle,
|
||||
'body' => $body,
|
||||
],
|
||||
'sound' => 'default',
|
||||
'badge' => $badge,
|
||||
],
|
||||
], $extra),
|
||||
'type' => 'customizedcast',
|
||||
'alias_type' => 'userid',
|
||||
'alias' => $alias,
|
||||
'policy' => [
|
||||
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
|
||||
],
|
||||
]
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'android':
|
||||
if (!isset($config['Android'])) {
|
||||
return;
|
||||
}
|
||||
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(),
|
||||
],
|
||||
'category' => 1,
|
||||
'channel_properties' => [
|
||||
'oppo_channel_id' => 'dootask',
|
||||
'vivo_category' => 'IM',
|
||||
'huawei_channel_importance' => 'NORMAL',
|
||||
'huawei_channel_category' => 'IM',
|
||||
'channel_fcm' => 0,
|
||||
],
|
||||
]
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送给指定会员
|
||||
* @param array|int $userid
|
||||
* @param array $array
|
||||
* @return void
|
||||
*/
|
||||
public static function pushMsgToUserid($userid, $array)
|
||||
{
|
||||
$builder = self::select(['id', 'platform', 'alias', 'userid'])->where('updated_at', '>', Carbon::now()->subMonth());
|
||||
if (is_array($userid)) {
|
||||
$builder->whereIn('userid', $userid);
|
||||
} elseif (Base::isNumber($userid)) {
|
||||
$builder->whereUserid($userid);
|
||||
}
|
||||
$builder
|
||||
->orderByDesc('updated_at')
|
||||
->chunkById(100, function ($datas) use ($array) {
|
||||
$uids = $datas->groupBy('userid');
|
||||
foreach ($uids as $uid => $rows) {
|
||||
$array['badge'] = WebSocketDialogMsgRead::whereUserid($uid)->whereSilence(0)->whereReadAt(null)->count();
|
||||
$lists = $rows->take(5)->groupBy('platform'); // 每个会员最多推送5个别名
|
||||
foreach ($lists as $platform => $list) {
|
||||
$alias = $list->pluck('alias')->implode(',');
|
||||
try {
|
||||
self::pushMsgToAlias($alias, $platform, $array);
|
||||
} catch (\Exception $e) {
|
||||
info("[PushMsg] fail: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Table\OnlineData;
|
||||
use App\Services\RequestContext;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
|
||||
@@ -13,8 +15,11 @@ use Carbon\Carbon;
|
||||
*
|
||||
* @property int $userid
|
||||
* @property array $identity 身份
|
||||
* @property array $department 所属部门
|
||||
* @property string|null $az A-Z
|
||||
* @property string|null $pinyin 拼音(主要用于搜索)
|
||||
* @property string|null $email 邮箱
|
||||
* @property string|null $tel 联系电话
|
||||
* @property string $nickname 昵称
|
||||
* @property string|null $profession 职位/职称
|
||||
* @property string $userimg 头像
|
||||
@@ -23,26 +28,39 @@ use Carbon\Carbon;
|
||||
* @property int|null $changepass 登录需要修改密码
|
||||
* @property int|null $login_num 累计登录次数
|
||||
* @property string|null $last_ip 最后登录IP
|
||||
* @property string|null $last_at 最后登录时间
|
||||
* @property \Illuminate\Support\Carbon|null $last_at 最后登录时间
|
||||
* @property string|null $line_ip 最后在线IP(接口)
|
||||
* @property string|null $line_at 最后在线时间(接口)
|
||||
* @property \Illuminate\Support\Carbon|null $line_at 最后在线时间(接口)
|
||||
* @property int|null $task_dialog_id 最后打开的任务会话ID
|
||||
* @property string|null $created_ip 注册IP
|
||||
* @property string|null $disable_at 禁用时间
|
||||
* @property \Illuminate\Support\Carbon|null $disable_at 禁用时间(离职时间)
|
||||
* @property int|null $email_verity 邮箱是否已验证
|
||||
* @property int|null $bot 是否机器人
|
||||
* @property string|null $lang 语言首选项
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Database\Factories\UserFactory factory(...$parameters)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereAz($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereBot($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereChangepass($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedIp($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereDepartment($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereDisableAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerity($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereEncrypt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereIdentity($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereLang($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereLastAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereLastIp($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereLineAt($value)
|
||||
@@ -50,8 +68,10 @@ use Carbon\Carbon;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereLoginNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereNickname($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User wherePassword($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User wherePinyin($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereProfession($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereTaskDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereTel($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereUserimg($value)
|
||||
@@ -62,22 +82,14 @@ class User extends AbstractModel
|
||||
protected $primaryKey = 'userid';
|
||||
|
||||
protected $hidden = [
|
||||
'disable_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 更新数据校验
|
||||
* @param array $param
|
||||
*/
|
||||
public function updateInstance(array $param)
|
||||
{
|
||||
parent::updateInstance($param);
|
||||
//
|
||||
if (isset($param['line_at']) && $this->userid) {
|
||||
Cache::put("User::online:" . $this->userid, time(), Carbon::now()->addSeconds(30));
|
||||
}
|
||||
}
|
||||
// 默认头像类型:auto自动生成,system系统默认
|
||||
public static $defaultAvatarMode = 'auto';
|
||||
|
||||
// 基本信息的字段
|
||||
public static $basicField = ['userid', 'email', 'nickname', 'profession', 'department', 'userimg', 'bot', 'az', 'pinyin', 'line_at', 'disable_at'];
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
@@ -86,7 +98,13 @@ class User extends AbstractModel
|
||||
*/
|
||||
public function getNicknameAttribute($value)
|
||||
{
|
||||
return $value ?: Base::cardFormat($this->email);
|
||||
if ($value) {
|
||||
if (UserBot::isSystemBot($this->email)) {
|
||||
return Doo::translate($value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
return Base::formatName($this->email);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,11 +114,7 @@ class User extends AbstractModel
|
||||
*/
|
||||
public function getUserimgAttribute($value)
|
||||
{
|
||||
if ($value) {
|
||||
return Base::fillUrl($value);
|
||||
}
|
||||
$name = ($this->userid - 1) % 21 + 1;
|
||||
return url("images/avatar/default_{$name}.png");
|
||||
return self::getAvatar($this->userid, $value, $this->email, $this->nickname);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,13 +130,70 @@ class User extends AbstractModel
|
||||
return array_filter(is_array($value) ? $value : explode(",", trim($value, ",")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门
|
||||
* @param $value
|
||||
* @return array
|
||||
*/
|
||||
public function getDepartmentAttribute($value)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return [];
|
||||
}
|
||||
return array_filter(is_array($value) ? $value : Base::explodeInt($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所属部门名称
|
||||
* @return string
|
||||
*/
|
||||
public function getDepartmentName()
|
||||
{
|
||||
if (empty($this->department)) {
|
||||
return "";
|
||||
}
|
||||
$key = "UserDepartment::" . md5(Cache::get("UserDepartment::rand") . '-' . implode(',' , $this->department));
|
||||
$list = Cache::remember($key, now()->addMonth(), function() {
|
||||
$list = UserDepartment::select(['id', 'owner_userid', 'name'])->whereIn('id', $this->department)->take(10)->get();
|
||||
return $list->toArray();
|
||||
});
|
||||
$array = [];
|
||||
foreach ($list as $item) {
|
||||
$array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? ' (M)' : '');
|
||||
}
|
||||
return implode(', ', $array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为部门负责人
|
||||
*/
|
||||
public function isDepartmentOwner()
|
||||
{
|
||||
return UserDepartment::where('owner_userid', $this->userid)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器人所有者
|
||||
* @return int
|
||||
*/
|
||||
public function getBotOwner()
|
||||
{
|
||||
if (!$this->bot) {
|
||||
return 0;
|
||||
}
|
||||
$key = "userBotOwner::" . $this->userid;
|
||||
return intval(Cache::remember($key, now()->addMonth(), function() {
|
||||
return intval(UserBot::whereBotId($this->userid)->value('userid')) ?: $this->userid;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否在线
|
||||
* @return bool
|
||||
*/
|
||||
public function getOnlineStatus()
|
||||
{
|
||||
$online = intval(Cache::get("User::online:" . $this->userid, 0));
|
||||
$online = $this->bot || OnlineData::live($this->userid) > 0;
|
||||
if ($online) {
|
||||
return true;
|
||||
}
|
||||
@@ -130,9 +201,74 @@ class User extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否管理员
|
||||
* 返回是否LDAP用户
|
||||
* @return bool
|
||||
*/
|
||||
public function isLdap()
|
||||
{
|
||||
return in_array('ldap', $this->identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否临时帐号
|
||||
* @return bool
|
||||
*/
|
||||
public function isTemp()
|
||||
{
|
||||
return in_array('temp', $this->identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否禁用帐号(离职)
|
||||
* @param bool $incAt 是否包含禁用时间
|
||||
* @return bool
|
||||
*/
|
||||
public function isDisable($incAt = false)
|
||||
{
|
||||
if ($incAt) {
|
||||
return in_array('disable', $this->identity) || $this->disable_at;
|
||||
}
|
||||
return in_array('disable', $this->identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否管理员
|
||||
* @return bool
|
||||
*/
|
||||
public function isAdmin()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否管理员
|
||||
*/
|
||||
public function checkAdmin()
|
||||
{
|
||||
$this->identity('admin');
|
||||
}
|
||||
@@ -167,6 +303,56 @@ class User extends AbstractModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会员
|
||||
* @param $reason
|
||||
* @return bool|null
|
||||
*/
|
||||
public function deleteUser($reason)
|
||||
{
|
||||
return AbstractModel::transaction(function () use ($reason) {
|
||||
// 删除原因
|
||||
$userDelete = UserDelete::createInstance([
|
||||
'operator' => User::userid(),
|
||||
'userid' => $this->userid,
|
||||
'email' => $this->email,
|
||||
'reason' => $reason,
|
||||
'cache' => array_merge($this->getRawOriginal(), [
|
||||
'department_name' => $this->getDepartmentName()
|
||||
])
|
||||
]);
|
||||
$userDelete->save();
|
||||
// 删除未读
|
||||
WebSocketDialogMsgRead::whereUserid($this->userid)->delete();
|
||||
// 删除待办
|
||||
WebSocketDialogMsgTodo::whereUserid($this->userid)->delete();
|
||||
// 删除邮箱验证记录
|
||||
UserEmailVerification::whereEmail($this->email)->delete();
|
||||
//
|
||||
return $this->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查发送聊天内容前必须设置昵称、电话
|
||||
* @return void
|
||||
*/
|
||||
public function checkChatInformation()
|
||||
{
|
||||
if ($this->bot) {
|
||||
return;
|
||||
}
|
||||
$chatInformation = Base::settingFind('system', 'chat_information');
|
||||
if ($chatInformation == 'required') {
|
||||
if (empty($this->getRawOriginal('nickname'))) {
|
||||
throw new ApiException('请设置昵称', [], -2);
|
||||
}
|
||||
if (empty($this->getRawOriginal('tel'))) {
|
||||
throw new ApiException('请设置联系电话', [], -3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
@@ -180,99 +366,47 @@ class User extends AbstractModel
|
||||
*/
|
||||
public static function reg($email, $password, $other = [])
|
||||
{
|
||||
//邮箱
|
||||
if (!Base::isMail($email)) {
|
||||
// 邮箱
|
||||
if (!Base::isEmail($email)) {
|
||||
throw new ApiException('请输入正确的邮箱地址');
|
||||
}
|
||||
if (User::email2userid($email) > 0) {
|
||||
$user = self::whereEmail($email)->first();
|
||||
if ($user) {
|
||||
$isRegVerify = Base::settingFind('emailSetting', 'reg_verify') === 'open';
|
||||
if ($isRegVerify && $user->email_verity === 0) {
|
||||
UserEmailVerification::userEmailSend($user);
|
||||
throw new ApiException('您的帐号已注册过,请验证邮箱', ['code' => 'email']);
|
||||
}
|
||||
throw new ApiException('邮箱地址已存在');
|
||||
}
|
||||
//密码
|
||||
// 密码
|
||||
self::passwordPolicy($password);
|
||||
//开始注册
|
||||
$encrypt = Base::generatePassword(6);
|
||||
$inArray = [
|
||||
'encrypt' => $encrypt,
|
||||
'email' => $email,
|
||||
'password' => Base::md52($password, $encrypt),
|
||||
'created_ip' => Base::getIp(),
|
||||
];
|
||||
// 开始注册
|
||||
$user = Doo::userCreate($email, $password);
|
||||
if ($other) {
|
||||
$inArray = array_merge($inArray, $other);
|
||||
$user->updateInstance($other);
|
||||
}
|
||||
$user->az = Base::getFirstCharter($user->nickname);
|
||||
$user->pinyin = Base::cn2pinyin($user->nickname);
|
||||
$user->created_ip = Base::getIp();
|
||||
if ($user->save()) {
|
||||
$setting = Base::setting('system');
|
||||
$reg_identity = $setting['reg_identity'] ?: 'normal';
|
||||
$all_group_autoin = $setting['all_group_autoin'] ?: 'yes';
|
||||
// 注册临时身份
|
||||
if ($reg_identity === 'temp') {
|
||||
$user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['temp']), ['temp']));
|
||||
$user->save();
|
||||
}
|
||||
// 加入全员群组
|
||||
if ($all_group_autoin === 'yes') {
|
||||
$dialog = WebSocketDialog::whereGroupType('all')->orderByDesc('id')->first();
|
||||
$dialog?->joinGroup($user->userid, 0);
|
||||
}
|
||||
}
|
||||
$user = User::createInstance($inArray);
|
||||
$user->save();
|
||||
User::AZUpdate($user->userid);
|
||||
return $user->find($user->userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱获取userid
|
||||
* @param $email
|
||||
* @return int
|
||||
*/
|
||||
public static function email2userid($email)
|
||||
{
|
||||
if (empty($email)) {
|
||||
return 0;
|
||||
}
|
||||
return intval(self::whereEmail($email)->value('userid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* token获取会员userid
|
||||
* @return int
|
||||
*/
|
||||
public static function token2userid()
|
||||
{
|
||||
return self::authFind('userid', Base::getToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* token获取会员邮箱
|
||||
* @return int
|
||||
*/
|
||||
public static function token2email()
|
||||
{
|
||||
return self::authFind('email', Base::getToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* token获取encrypt
|
||||
* @return mixed|string
|
||||
*/
|
||||
public static function token2encrypt()
|
||||
{
|
||||
return self::authFind('encrypt', Base::getToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取token身份信息
|
||||
* @param $find
|
||||
* @param null $token
|
||||
* @return array|mixed|string
|
||||
*/
|
||||
public static function authFind($find, $token = null)
|
||||
{
|
||||
if ($token === null) {
|
||||
$token = Base::getToken();
|
||||
}
|
||||
list($userid, $email, $encrypt, $timestamp) = explode("#$", base64_decode($token) . "#$#$#$#$");
|
||||
$array = [
|
||||
'userid' => intval($userid),
|
||||
'email' => $email ?: '',
|
||||
'encrypt' => $encrypt ?: '',
|
||||
'timestamp' => intval($timestamp),
|
||||
];
|
||||
if (isset($array[$find])) {
|
||||
return $array[$find];
|
||||
}
|
||||
if ($find == 'all') {
|
||||
return $array;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的ID
|
||||
* @return int
|
||||
@@ -308,14 +442,15 @@ class User extends AbstractModel
|
||||
{
|
||||
$user = self::authInfo();
|
||||
if (!$user) {
|
||||
$authorization = Base::getToken();
|
||||
if ($authorization) {
|
||||
$token = Base::token();
|
||||
if ($token) {
|
||||
UserDevice::forget($token);
|
||||
throw new ApiException('身份已失效,请重新登录', [], -1);
|
||||
} else {
|
||||
throw new ApiException('请登录后继续...', [], -1);
|
||||
}
|
||||
}
|
||||
if (in_array('disable', $user->identity)) {
|
||||
if ($user->isDisable()) {
|
||||
throw new ApiException('帐号已停用...', [], -1);
|
||||
}
|
||||
if ($identity) {
|
||||
@@ -330,61 +465,90 @@ class User extends AbstractModel
|
||||
*/
|
||||
private static function authInfo()
|
||||
{
|
||||
global $_A;
|
||||
if (isset($_A["__static_auth"])) {
|
||||
return $_A["__static_auth"];
|
||||
if (RequestContext::has('auth')) {
|
||||
// 缓存
|
||||
return RequestContext::get('auth');
|
||||
}
|
||||
$authorization = Base::getToken();
|
||||
if ($authorization) {
|
||||
$authInfo = self::authFind('all', $authorization);
|
||||
if ($authInfo['userid'] > 0) {
|
||||
$loginValid = floatval(Base::settingFind('system', 'loginValid')) ?: 720;
|
||||
$loginValid *= 3600;
|
||||
if ($authInfo['timestamp'] + $loginValid > time()) {
|
||||
$row = self::whereUserid($authInfo['userid'])->whereEmail($authInfo['email'])->whereEncrypt($authInfo['encrypt'])->first();
|
||||
if ($row) {
|
||||
$upArray = [];
|
||||
if (Base::getIp() && $row->line_ip != Base::getIp()) {
|
||||
$upArray['line_ip'] = Base::getIp();
|
||||
}
|
||||
if (Carbon::parse($row->line_at)->addSeconds(30)->lt(Carbon::now())) {
|
||||
$upArray['line_at'] = Carbon::now();
|
||||
}
|
||||
if ($upArray) {
|
||||
$row->updateInstance($upArray);
|
||||
$row->save();
|
||||
}
|
||||
return $_A["__static_auth"] = $row;
|
||||
}
|
||||
}
|
||||
if (Doo::userId() <= 0) {
|
||||
// 没有登录
|
||||
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;
|
||||
}
|
||||
}
|
||||
return $_A["__static_auth"] = false;
|
||||
if ($upArray) {
|
||||
$user->updateInstance($upArray);
|
||||
$user->save();
|
||||
}
|
||||
return RequestContext::save('auth', $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成token
|
||||
* 生成 token
|
||||
* @param self $userinfo
|
||||
* @param bool $refresh 获取新的token
|
||||
* @return string
|
||||
*/
|
||||
public static function token($userinfo)
|
||||
public static function generateToken($userinfo, $refresh = false)
|
||||
{
|
||||
$userinfo->token = base64_encode($userinfo->userid . '#$' . $userinfo->email . '#$' . $userinfo->encrypt . '#$' . time() . '#$' . Base::generatePassword(6));
|
||||
if (!$refresh) {
|
||||
if (Doo::userId() != $userinfo->userid
|
||||
|| Doo::userEmail() != $userinfo->email
|
||||
|| Doo::userEncrypt() != $userinfo->encrypt) {
|
||||
$refresh = true;
|
||||
}
|
||||
}
|
||||
if ($refresh) {
|
||||
$days = $userinfo->bot ? 0 : max(1, intval(Base::settingFind('system', 'token_valid_days', 30)));
|
||||
$token = Doo::tokenEncode($userinfo->userid, $userinfo->email, $userinfo->encrypt, $days);
|
||||
} else {
|
||||
$token = Doo::userToken();
|
||||
}
|
||||
UserDevice::record($token);
|
||||
unset($userinfo->encrypt);
|
||||
unset($userinfo->password);
|
||||
return $userinfo->token;
|
||||
return $userinfo->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户权限(身份)
|
||||
* @param $identity
|
||||
* @param $userIdentity
|
||||
* @return bool
|
||||
* 生成无设备的 token(主要用于接口调用,此 token 不检查设备是否存在)
|
||||
* @param self $userinfo
|
||||
* @param $ttl
|
||||
* @return mixed
|
||||
*/
|
||||
public static function identityRaw($identity, $userIdentity)
|
||||
public static function generateTokenNoDevice($userinfo, $ttl)
|
||||
{
|
||||
$userIdentity = is_array($userIdentity) ? $userIdentity : explode(",", trim($userIdentity, ","));
|
||||
return $identity && in_array($identity, $userIdentity);
|
||||
$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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -392,22 +556,21 @@ class User extends AbstractModel
|
||||
* @param int $userid 会员ID
|
||||
* @return self
|
||||
*/
|
||||
public static function userid2basic($userid)
|
||||
public static function userid2basic($userid, $addField = [])
|
||||
{
|
||||
global $_A;
|
||||
if (empty($userid)) {
|
||||
return null;
|
||||
}
|
||||
$userid = intval($userid);
|
||||
if (isset($_A["__static_userid2basic_" . $userid])) {
|
||||
return $_A["__static_userid2basic_" . $userid];
|
||||
if (RequestContext::has("userid2basic_" . $userid)) {
|
||||
return RequestContext::get("userid2basic_" . $userid);
|
||||
}
|
||||
$fields = ['userid', 'email', 'nickname', 'profession', 'userimg'];
|
||||
$userInfo = self::whereUserid($userid)->select($fields)->first();
|
||||
$userInfo = self::whereUserid($userid)->select(array_merge(User::$basicField, $addField))->first();
|
||||
if ($userInfo) {
|
||||
$userInfo->online = $userInfo->getOnlineStatus();
|
||||
$userInfo->department_name = $userInfo->getDepartmentName();
|
||||
}
|
||||
return $_A["__static_userid2basic_" . $userid] = ($userInfo ?: []);
|
||||
return RequestContext::save("userid2basic_" . $userid, $userInfo ?: []);
|
||||
}
|
||||
|
||||
|
||||
@@ -418,21 +581,7 @@ class User extends AbstractModel
|
||||
*/
|
||||
public static function userid2nickname($userid)
|
||||
{
|
||||
$basic = self::userid2basic($userid);
|
||||
return $basic ? $basic->nickname : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新首字母
|
||||
* @param $userid
|
||||
*/
|
||||
public static function AZUpdate($userid)
|
||||
{
|
||||
$row = self::whereUserid($userid)->first();
|
||||
if ($row) {
|
||||
$row->az = Base::getFirstCharter($row->nickname);
|
||||
$row->save();
|
||||
}
|
||||
return self::userid2basic($userid)?->nickname ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -459,6 +608,72 @@ class User extends AbstractModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 临时帐号别名
|
||||
* @return mixed|string
|
||||
*/
|
||||
public static function tempAccountAlias()
|
||||
{
|
||||
$alias = Base::settingFind('system', 'temp_account_alias');
|
||||
return $alias ?: Doo::translate("临时帐号");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取头像
|
||||
* @param $userid
|
||||
* @param $userimg
|
||||
* @param $email
|
||||
* @param $nickname
|
||||
* @return string
|
||||
*/
|
||||
public static function getAvatar($userid, $userimg, $email, $nickname)
|
||||
{
|
||||
// 自定义头像
|
||||
if ($userimg && !str_contains($userimg, 'avatar/')) {
|
||||
return Base::fillUrl($userimg);
|
||||
}
|
||||
// 机器人头像
|
||||
switch ($email) {
|
||||
case 'system-msg@bot.system':
|
||||
return url("images/avatar/default_system.png");
|
||||
case 'task-alert@bot.system':
|
||||
return url("images/avatar/default_task.png");
|
||||
case 'check-in@bot.system':
|
||||
return url("images/avatar/default_checkin.png");
|
||||
case 'anon-msg@bot.system':
|
||||
return url("images/avatar/default_anon.png");
|
||||
case 'approval-alert@bot.system':
|
||||
return url("images/avatar/default_approval.png");
|
||||
case 'okr-alert@bot.system':
|
||||
return url("images/avatar/default_okr.png");
|
||||
case 'ai-openai@bot.system':
|
||||
return url("images/avatar/default_openai.png");
|
||||
case 'ai-claude@bot.system':
|
||||
return url("images/avatar/default_claude.png");
|
||||
case 'ai-deepseek@bot.system':
|
||||
return url("images/avatar/default_deepseek.png");
|
||||
case 'ai-gemini@bot.system':
|
||||
return url("images/avatar/default_gemini.png");
|
||||
case 'ai-grok@bot.system':
|
||||
return url("images/avatar/default_grok.png");
|
||||
case 'ai-ollama@bot.system':
|
||||
return url("images/avatar/default_ollama.png");
|
||||
case 'ai-zhipu@bot.system':
|
||||
return url("images/avatar/default_zhipu.png");
|
||||
case 'bot-manager@bot.system':
|
||||
return url("images/avatar/default_bot.png");
|
||||
case 'meeting-alert@bot.system':
|
||||
return url("images/avatar/default_meeting.png");
|
||||
}
|
||||
// 生成文字头像
|
||||
if (self::$defaultAvatarMode === 'auto') {
|
||||
return url("avatar/" . urlencode($nickname) . ".png");
|
||||
}
|
||||
// 系统默认头像
|
||||
$name = ($userid - 1) % 21 + 1;
|
||||
return url("images/avatar/default_{$name}.png");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测密码策略是否符合
|
||||
* @param $password
|
||||
@@ -489,4 +704,84 @@ class User extends AbstractModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器人或创建
|
||||
* @param $key
|
||||
* @param $update
|
||||
* @param $userid
|
||||
* @return self|null
|
||||
*/
|
||||
public static function botGetOrCreate($key, $update = [], $userid = 0)
|
||||
{
|
||||
$email = "{$key}@bot.system";
|
||||
$botUser = self::whereEmail($email)->first();
|
||||
if (empty($botUser)) {
|
||||
$botUser = Doo::userCreate($email, Base::generatePassword(32));
|
||||
if (empty($botUser)) {
|
||||
return null;
|
||||
}
|
||||
$botUser->updateInstance([
|
||||
'created_ip' => Base::getIp(),
|
||||
]);
|
||||
$botUser->save();
|
||||
if ($userid > 0) {
|
||||
UserBot::createInstance([
|
||||
'userid' => $userid,
|
||||
'bot_id' => $botUser->userid,
|
||||
])->save();
|
||||
}
|
||||
//
|
||||
if (empty($update['nickname'])) {
|
||||
$update['nickname'] = UserBot::systemBotName($email);
|
||||
}
|
||||
}
|
||||
if ($update) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否机器人
|
||||
* @param $userid
|
||||
* @return bool
|
||||
*/
|
||||
public static function isBot($userid)
|
||||
{
|
||||
if (empty($userid)) {
|
||||
return false;
|
||||
}
|
||||
// 这个不会有变化,所以可以使用永久缓存
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
482
app/Models/UserBot.php
Normal file
482
app/Models/UserBot.php
Normal file
@@ -0,0 +1,482 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Extranet;
|
||||
use App\Module\Timer;
|
||||
use App\Tasks\JokeSoupTask;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\UserBot
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 所属人ID
|
||||
* @property int|null $bot_id 机器人ID
|
||||
* @property int|null $clear_day 消息自动清理天数
|
||||
* @property \Illuminate\Support\Carbon|null $clear_at 下一次清理时间
|
||||
* @property string|null $webhook_url 消息webhook地址
|
||||
* @property int|null $webhook_num 消息webhook请求次数
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|UserBot newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereBotId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereClearAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereClearDay($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereWebhookNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereWebhookUrl($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserBot extends AbstractModel
|
||||
{
|
||||
|
||||
/**
|
||||
* 判断是否系统机器人
|
||||
* @param $email
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSystemBot($email)
|
||||
{
|
||||
return str_ends_with($email, '@bot.system') && self::systemBotName($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统机器人名称
|
||||
* @param $name string 邮箱 或 邮箱前缀
|
||||
* @return string
|
||||
*/
|
||||
public static function systemBotName($name)
|
||||
{
|
||||
if (str_contains($name, "@")) {
|
||||
$name = explode("@", $name)[0];
|
||||
}
|
||||
$name = match ($name) {
|
||||
'system-msg' => '系统消息',
|
||||
'task-alert' => '任务提醒',
|
||||
'check-in' => '签到打卡',
|
||||
'anon-msg' => '匿名消息',
|
||||
'approval-alert' => '审批',
|
||||
'ai-openai' => 'ChatGPT',
|
||||
'ai-claude' => 'Claude',
|
||||
'ai-deepseek' => 'DeepSeek',
|
||||
'ai-gemini' => 'Gemini',
|
||||
'ai-grok' => 'Grok',
|
||||
'ai-ollama' => 'Ollama',
|
||||
'ai-zhipu' => '智谱清言',
|
||||
'ai-qianwen' => '通义千问',
|
||||
'ai-wenxin' => '文心一言',
|
||||
'bot-manager' => '机器人管理',
|
||||
'meeting-alert' => '会议通知',
|
||||
'okr-alert' => 'OKR提醒',
|
||||
default => '', // 不是系统机器人时返回空(也可以拿来判断是否是系统机器人)
|
||||
};
|
||||
return Doo::translate($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人菜单
|
||||
* @param $email
|
||||
* @return array|array[]
|
||||
*/
|
||||
public static function quickMsgs($email)
|
||||
{
|
||||
switch ($email) {
|
||||
case 'check-in@bot.system':
|
||||
$menu = [];
|
||||
$setting = Base::setting('checkinSetting');
|
||||
if ($setting['open'] !== 'open') {
|
||||
return $menu;
|
||||
}
|
||||
if (in_array('locat', $setting['modes']) && Base::isEEUIApp()) {
|
||||
$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[] = [
|
||||
'key' => 'manual-checkin',
|
||||
'label' => Doo::translate('手动签到')
|
||||
];
|
||||
}
|
||||
return $menu;
|
||||
|
||||
case 'anon-msg@bot.system':
|
||||
return [
|
||||
[
|
||||
'key' => 'help',
|
||||
'label' => Doo::translate('使用说明')
|
||||
], [
|
||||
'key' => 'privacy',
|
||||
'label' => Doo::translate('隐私说明')
|
||||
],
|
||||
];
|
||||
|
||||
case 'meeting-alert@bot.system':
|
||||
if (!Base::judgeClientVersion('0.39.89')) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
[
|
||||
'key' => 'meeting-create',
|
||||
'label' => Doo::translate('新会议')
|
||||
],
|
||||
[
|
||||
'key' => 'meeting-join',
|
||||
'label' => Doo::translate('加入会议')
|
||||
],
|
||||
];
|
||||
|
||||
case 'bot-manager@bot.system':
|
||||
return [
|
||||
[
|
||||
'key' => '/help',
|
||||
'label' => Doo::translate('帮助指令')
|
||||
], [
|
||||
'key' => '/api',
|
||||
'label' => Doo::translate('API接口文档')
|
||||
], [
|
||||
'key' => '/list',
|
||||
'label' => Doo::translate('我的机器人')
|
||||
],
|
||||
];
|
||||
|
||||
default:
|
||||
if (preg_match('/^(ai-|user-session-)(.*?)@bot\.system$/', $email, $match)) {
|
||||
$menus = [
|
||||
[
|
||||
'key' => '~ai-session-create',
|
||||
'label' => Doo::translate('开启新会话'),
|
||||
],
|
||||
[
|
||||
'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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 签到机器人
|
||||
* @param $command
|
||||
* @param $userid
|
||||
* @param $extra
|
||||
* @return string
|
||||
*/
|
||||
public static function checkinBotQuickMsg($command, $userid, $extra = [])
|
||||
{
|
||||
if (Cache::get("UserBot::checkinBotQuickMsg:{$userid}") === "yes") {
|
||||
return "操作频繁!";
|
||||
}
|
||||
Cache::put("UserBot::checkinBotQuickMsg:{$userid}", "yes", Carbon::now()->addSecond());
|
||||
//
|
||||
if ($command === 'manual-checkin') {
|
||||
$setting = Base::setting('checkinSetting');
|
||||
if ($setting['open'] !== 'open') {
|
||||
return '暂未开启签到功能。';
|
||||
}
|
||||
if (!in_array('manual', $setting['modes'])) {
|
||||
return '暂未开放手动签到。';
|
||||
}
|
||||
UserBot::checkinBotCheckin('manual-' . $userid, Timer::time(), true);
|
||||
} elseif ($command === 'locat-checkin') {
|
||||
$setting = Base::setting('checkinSetting');
|
||||
if ($setting['open'] !== 'open') {
|
||||
return '暂未开启签到功能。';
|
||||
}
|
||||
if (!in_array('locat', $setting['modes'])) {
|
||||
return '暂未开放定位签到。';
|
||||
}
|
||||
if (empty($extra)) {
|
||||
return '当前客户端版本低(所需版本≥v0.39.75)。';
|
||||
}
|
||||
if (in_array($extra['type'], ['baidu', 'amap', 'tencent'])) {
|
||||
// todo 判断距离
|
||||
} else {
|
||||
return '错误的定位签到。';
|
||||
}
|
||||
UserBot::checkinBotCheckin('locat-' . $userid, Timer::time(), true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 签到机器人签到
|
||||
* @param mixed $mac
|
||||
* - 多个使用,分隔
|
||||
* - 支持:mac地址、(manual|locat|face|checkin)-userid
|
||||
* @param $time
|
||||
* @param bool $alreadyTip 签到过是否提示
|
||||
*/
|
||||
public static function checkinBotCheckin($mac, $time, $alreadyTip = false)
|
||||
{
|
||||
$setting = Base::setting('checkinSetting');
|
||||
$times = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00'];
|
||||
$advance = (intval($setting['advance']) ?: 120) * 60;
|
||||
$delay = (intval($setting['delay']) ?: 120) * 60;
|
||||
//
|
||||
$nowDate = date("Y-m-d");
|
||||
$nowTime = date("H:i:s");
|
||||
//
|
||||
$timeStart = strtotime("{$nowDate} {$times[0]}");
|
||||
$timeEnd = strtotime("{$nowDate} {$times[1]}");
|
||||
$timeAdvance = max($timeStart - $advance, strtotime($nowDate));
|
||||
$timeDelay = min($timeEnd + $delay, strtotime("{$nowDate} 23:59:59"));
|
||||
$errorTime = false;
|
||||
if (Timer::time() < $timeAdvance || $timeDelay < Timer::time()) {
|
||||
$errorTime = "不在有效时间内,有效时间为:" . date("H:i", $timeAdvance) . "-" . date("H:i", $timeDelay);
|
||||
}
|
||||
//
|
||||
$macs = explode(",", $mac);
|
||||
$checkins = [];
|
||||
$array = [];
|
||||
foreach ($macs as $mac) {
|
||||
$mac = strtoupper($mac);
|
||||
if (Base::isMac($mac)) {
|
||||
// 路由器签到
|
||||
if ($UserCheckinMac = UserCheckinMac::whereMac($mac)->first()) {
|
||||
$array[] = [
|
||||
'userid' => $UserCheckinMac->userid,
|
||||
'mac' => $UserCheckinMac->mac,
|
||||
'date' => $nowDate,
|
||||
];
|
||||
$checkins[] = [
|
||||
'userid' => $UserCheckinMac->userid,
|
||||
'remark' => $UserCheckinMac->remark,
|
||||
];
|
||||
}
|
||||
} elseif (preg_match('/^(manual|locat|face|checkin)-(\d+)$/i', $mac, $match)) {
|
||||
// 机器签到、手动签到、定位签到
|
||||
$type = str_replace('checkin', 'face', strtolower($match[1]));
|
||||
$mac = intval($match[2]);
|
||||
$remark = match ($type) {
|
||||
'manual' => $setting['manual_remark'] ?: 'Manual',
|
||||
'locat' => $setting['locat_remark'] ?: 'Location',
|
||||
'face' => $setting['face_remark'] ?: 'Machine',
|
||||
default => '',
|
||||
};
|
||||
if ($UserInfo = User::whereUserid($mac)->whereBot(0)->first()) {
|
||||
$array[] = [
|
||||
'userid' => $UserInfo->userid,
|
||||
'mac' => '00:00:00:00:00:00',
|
||||
'date' => $nowDate,
|
||||
];
|
||||
$checkins[] = [
|
||||
'userid' => $UserInfo->userid,
|
||||
'remark' => $remark,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$errorTime) {
|
||||
foreach ($array as $item) {
|
||||
$record = UserCheckinRecord::where($item)->first();
|
||||
if (empty($record)) {
|
||||
$record = UserCheckinRecord::createInstance($item);
|
||||
}
|
||||
$record->times = Base::array2json(array_merge($record->times, [$nowTime]));
|
||||
$record->report_time = $time;
|
||||
$record->save();
|
||||
}
|
||||
}
|
||||
//
|
||||
if ($checkins && $botUser = User::botGetOrCreate('check-in')) {
|
||||
$getJokeSoup = function($type, $userid) {
|
||||
$pre = $type == "up" ? "每日开心:" : "心灵鸡汤:";
|
||||
$key = $type == "up" ? "jokes" : "soups";
|
||||
$array = Base::json2array(Cache::get(JokeSoupTask::keyName($key)));
|
||||
if ($array) {
|
||||
$item = $array[array_rand($array)];
|
||||
if ($item) {
|
||||
Doo::setLanguage($userid);
|
||||
return Doo::translate($pre . $item);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
$sendMsg = function($type, $checkin) use ($errorTime, $alreadyTip, $getJokeSoup, $botUser, $nowDate) {
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $checkin['userid']);
|
||||
if (!$dialog) {
|
||||
return;
|
||||
}
|
||||
// 判断错误
|
||||
if ($errorTime) {
|
||||
if ($alreadyTip) {
|
||||
$text = $errorTime;
|
||||
$text .= $checkin['remark'] ? " ({$checkin['remark']})": "";
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'content' => $text,
|
||||
], $botUser->userid, false, false, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 判断已打卡
|
||||
$cacheKey = "Checkin::sendMsg-{$nowDate}-{$type}:" . $checkin['userid'];
|
||||
$typeContent = $type == "up" ? "上班" : "下班";
|
||||
if (Cache::get($cacheKey) === "yes") {
|
||||
if ($alreadyTip) {
|
||||
$text = "今日已{$typeContent}打卡,无需重复打卡。";
|
||||
$text .= $checkin['remark'] ? " ({$checkin['remark']})": "";
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'content' => $text,
|
||||
], $botUser->userid, false, false, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Cache::put($cacheKey, "yes", Carbon::now()->addDay());
|
||||
// 打卡成功
|
||||
$hi = date("H:i");
|
||||
$remark = $checkin['remark'] ? " ({$checkin['remark']})": "";
|
||||
$subcontent = $getJokeSoup($type, $checkin['userid']);
|
||||
$title = "{$typeContent}打卡成功,打卡时间: {$hi}{$remark}";
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => 'content',
|
||||
'title' => $title,
|
||||
'content' => [
|
||||
[
|
||||
'content' => $title
|
||||
], [
|
||||
'content' => $subcontent,
|
||||
'language' => false,
|
||||
'style' => 'padding-top:4px;opacity:0.6',
|
||||
]
|
||||
],
|
||||
], $botUser->userid, false, false, $type != "up");
|
||||
};
|
||||
if ($timeAdvance <= Timer::time() && Timer::time() < $timeEnd) {
|
||||
// 上班打卡通知(从最早打卡时间 到 下班打卡时间)
|
||||
foreach ($checkins as $checkin) {
|
||||
$sendMsg('up', $checkin);
|
||||
}
|
||||
}
|
||||
if ($timeEnd <= Timer::time() && Timer::time() <= $timeDelay) {
|
||||
// 下班打卡通知(下班打卡时间 到 最晚打卡时间)
|
||||
foreach ($checkins as $checkin) {
|
||||
$sendMsg('down', $checkin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐私机器人
|
||||
* @param $command
|
||||
* @return array
|
||||
*/
|
||||
public static function anonBotQuickMsg($command)
|
||||
{
|
||||
return match ($command) {
|
||||
"help" => [
|
||||
"title" => "匿名消息使用说明",
|
||||
"content" => "使用说明:打开你想要发匿名消息的个人对话,点击输入框右边的 ⊕ 号,选择「匿名消息」即可输入你想要发送的匿名消息内容。"
|
||||
],
|
||||
"privacy" => [
|
||||
"title" => "匿名消息隐私说明",
|
||||
"content" => "匿名消息将通过「匿名消息(机器人)」发送给对方,不会记录你的身份信息。"
|
||||
],
|
||||
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);
|
||||
}
|
||||
}
|
||||
108
app/Models/UserCheckinFace.php
Normal file
108
app/Models/UserCheckinFace.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Apps;
|
||||
use App\Module\Base;
|
||||
use App\Module\Ihttp;
|
||||
|
||||
/**
|
||||
* App\Models\UserCheckinFace
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 会员id
|
||||
* @property string|null $faceimg 人脸图片
|
||||
* @property int|null $status 状态
|
||||
* @property string|null $remark 备注
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereFaceimg($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereRemark($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserCheckinFace extends AbstractModel
|
||||
{
|
||||
|
||||
public static function saveFace($userid, $nickname, $faceimg, $remark = '')
|
||||
{
|
||||
Apps::isInstalledThrow('face');
|
||||
// 取上传图片的URL
|
||||
$faceimg = Base::unFillUrl($faceimg);
|
||||
$record = "";
|
||||
if ($faceimg != '') {
|
||||
$faceFile = public_path($faceimg);
|
||||
$record = base64_encode(file_get_contents($faceFile));
|
||||
}
|
||||
|
||||
$url = "http://face:7788/user";
|
||||
$data = [
|
||||
'name' => $nickname,
|
||||
'enrollid' => $userid,
|
||||
'admin' => 0,
|
||||
'backupnum' => 50,
|
||||
];
|
||||
if ($record != '') {
|
||||
$data['record'] = $record;
|
||||
}
|
||||
|
||||
$res = Ihttp::ihttp_post($url, json_encode($data), 15);
|
||||
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) {
|
||||
$checkinFace = self::query()->whereUserid($userid)->first();
|
||||
if ($checkinFace) {
|
||||
self::updateData(['id' => $checkinFace->id], [
|
||||
'faceimg' => $faceimg,
|
||||
'status' => 1,
|
||||
'remark' => $remark
|
||||
]);
|
||||
} else {
|
||||
$checkinFace = new UserCheckinFace();
|
||||
$checkinFace->faceimg = $faceimg;
|
||||
$checkinFace->userid = $userid;
|
||||
$checkinFace->remark = $remark;
|
||||
$checkinFace->save();
|
||||
}
|
||||
if ($faceimg == '') {
|
||||
UserCheckinFace::deleteDeviceUser($userid);
|
||||
}
|
||||
return Base::retSuccess('设置成功');
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new ApiException($data->msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
app/Models/UserCheckinMac.php
Normal file
72
app/Models/UserCheckinMac.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\UserCheckinMac
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 会员id
|
||||
* @property string|null $mac MAC地址
|
||||
* @property string|null $remark 备注
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|UserCheckinMac newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereMac($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereRemark($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserCheckinMac extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 保存mac地址
|
||||
* @param $userid
|
||||
* @param $array
|
||||
* @return mixed
|
||||
*/
|
||||
public static function saveMac($userid, $array)
|
||||
{
|
||||
return AbstractModel::transaction(function() use ($array, $userid) {
|
||||
$ids = [];
|
||||
$list = [];
|
||||
foreach ($array as $item) {
|
||||
if (self::whereMac($item['mac'])->where('userid', '!=', $userid)->exists()) {
|
||||
throw new ApiException("{$item['mac']} 已被其他成员设置");
|
||||
}
|
||||
$update = [];
|
||||
if ($item['remark']) {
|
||||
$update = [
|
||||
'remark' => $item['remark']
|
||||
];
|
||||
}
|
||||
$row = self::updateInsert([
|
||||
'userid' => $userid,
|
||||
'mac' => $item['mac']
|
||||
], $update);
|
||||
if ($row) {
|
||||
$ids[] = $row->id;
|
||||
$list[] = $row;
|
||||
}
|
||||
}
|
||||
self::whereUserid($userid)->whereNotIn('id', $ids)->delete();
|
||||
//
|
||||
return Base::retSuccess('修改成功', $list);
|
||||
});
|
||||
}
|
||||
}
|
||||
140
app/Models/UserCheckinRecord.php
Normal file
140
app/Models/UserCheckinRecord.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\UserCheckinRecord
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 会员id
|
||||
* @property string|null $mac MAC地址
|
||||
* @property string|null $date 签到日期
|
||||
* @property array $times 签到时间
|
||||
* @property int|null $report_time 上报的时间戳
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|UserCheckinRecord newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereDate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereMac($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereReportTime($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereTimes($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserCheckinRecord extends AbstractModel
|
||||
{
|
||||
|
||||
/**
|
||||
* 签到记录
|
||||
* @param $value
|
||||
* @return array
|
||||
*/
|
||||
public function getTimesAttribute($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
return Base::json2array($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签到时间
|
||||
* @param int $userid
|
||||
* @param array $betweenTimes
|
||||
* @return array
|
||||
*/
|
||||
public static function getTimes(int $userid, array $betweenTimes)
|
||||
{
|
||||
$array = [];
|
||||
$records = self::whereUserid($userid)->whereBetween('created_at', $betweenTimes)->orderBy('id')->get();
|
||||
/** @var self $record */
|
||||
foreach ($records as $record) {
|
||||
$times = array_map(function ($time) {
|
||||
return preg_replace("/(\d+):(\d+):\d+$/", "$1:$2", $time);
|
||||
}, $record->times);
|
||||
if (isset($array[$record->date])) {
|
||||
$array[$record->date] = array_merge($array[$record->date], $times);
|
||||
} else {
|
||||
$array[$record->date] = $times;
|
||||
}
|
||||
}
|
||||
//
|
||||
foreach ($array as $date => $times) {
|
||||
$times = array_values(array_filter(array_unique($times)));
|
||||
$inOrder = [];
|
||||
foreach ($times as $key => $time) {
|
||||
$inOrder[$key] = strtotime("2022-01-01 {$time}");
|
||||
}
|
||||
array_multisort($inOrder, SORT_ASC, $times);
|
||||
$array[$date] = $times;
|
||||
}
|
||||
//
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间收集
|
||||
* @param string $data
|
||||
* @param array $times
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public static function atCollect($data, $times)
|
||||
{
|
||||
$sameTimes = array_map(function($time) use ($data) {
|
||||
return [
|
||||
"datetime" => "{$data} {$time}",
|
||||
"timestamp" => strtotime("{$data} {$time}")
|
||||
];
|
||||
}, $times);
|
||||
return collect($sameTimes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 签到时段
|
||||
* @param array $times
|
||||
* @param int $diff 多长未签到算失效(秒)
|
||||
* @return array
|
||||
*/
|
||||
public static function atSection($times, $diff = 3600)
|
||||
{
|
||||
$start = "";
|
||||
$end = "";
|
||||
$array = [];
|
||||
foreach ($times as $time) {
|
||||
$time = preg_replace("/(\d+):(\d+):\d+$/", "$1:$2", $time);
|
||||
if (empty($start)) {
|
||||
$start = $time;
|
||||
continue;
|
||||
}
|
||||
if (empty($end)) {
|
||||
$end = $time;
|
||||
continue;
|
||||
}
|
||||
if (strtotime("2022-01-01 {$time}") - strtotime("2022-01-01 {$end}") > $diff) {
|
||||
$array[] = [$start, $end];
|
||||
$start = $time;
|
||||
$end = "";
|
||||
continue;
|
||||
}
|
||||
$end = $time;
|
||||
}
|
||||
if ($start) {
|
||||
$array[] = [$start, $end];
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
84
app/Models/UserDelete.php
Normal file
84
app/Models/UserDelete.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\UserDelete
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $operator 操作人员
|
||||
* @property int|null $userid 用户id
|
||||
* @property string|null $email 邮箱帐号
|
||||
* @property string|null $reason 注销原因
|
||||
* @property string|null $cache 会员资料缓存
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereCache($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereEmail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereOperator($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereReason($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserDelete extends AbstractModel
|
||||
{
|
||||
public function getCacheAttribute($value)
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
$value = Base::json2array($value);
|
||||
// 昵称
|
||||
if (!$value['nickname']) {
|
||||
$value['nickname'] = Base::formatName($value['email']);
|
||||
}
|
||||
// 头像
|
||||
$value['userimg'] = User::getAvatar($value['userid'], $value['userimg'], $value['email'], $value['nickname']);
|
||||
// 部门
|
||||
$value['department'] = array_filter(is_array($value['department']) ? $value['department'] : Base::explodeInt($value['department']));
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* userid 获取 基础信息
|
||||
* @param int $userid 会员ID
|
||||
* @return array|null
|
||||
*/
|
||||
public static function userid2basic($userid)
|
||||
{
|
||||
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'] ?? '';
|
||||
}
|
||||
}
|
||||
235
app/Models/UserDepartment.php
Normal file
235
app/Models/UserDepartment.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use Cache;
|
||||
|
||||
/**
|
||||
* App\Models\UserDepartment
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $name 部门名称
|
||||
* @property int|null $dialog_id 聊天会话ID
|
||||
* @property int|null $parent_id 上级部门
|
||||
* @property int|null $owner_userid 部门负责人
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|UserDepartment newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereOwnerUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereParentId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserDepartment extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 获取所有父级部门
|
||||
* @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
|
||||
* @param $dialogUseid
|
||||
*/
|
||||
public function saveDepartment($data = [], $dialogUseid = 0) {
|
||||
AbstractModel::transaction(function () use ($dialogUseid, $data) {
|
||||
$oldUser = null;
|
||||
$newUser = null;
|
||||
if ($data['owner_userid'] !== $this->owner_userid) {
|
||||
$oldUser = User::find($this->owner_userid);
|
||||
$newUser = User::find($data['owner_userid']);
|
||||
}
|
||||
$this->updateInstance($data);
|
||||
//
|
||||
if ($this->dialog_id > 0) {
|
||||
// 已有群
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
if ($dialog) {
|
||||
$dialog->name = $this->name;
|
||||
$dialog->owner_id = $this->owner_userid;
|
||||
if ($dialog->save()) {
|
||||
$dialog->joinGroup($this->owner_userid, 0, true);
|
||||
$dialog->pushMsg("groupUpdate", [
|
||||
'id' => $dialog->id,
|
||||
'name' => $dialog->name,
|
||||
'owner_id' => $dialog->owner_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
} elseif ($dialogUseid > 0) {
|
||||
// 使用现有群
|
||||
$dialog = WebSocketDialog::whereType('group')->whereGroupType('user')->find($dialogUseid);
|
||||
if (empty($dialog)) {
|
||||
throw new ApiException("选择现有聊天群不存在");
|
||||
}
|
||||
$dialog->name = $this->name;
|
||||
$dialog->owner_id = $this->owner_userid;
|
||||
$dialog->group_type = 'department';
|
||||
if ($dialog->save()) {
|
||||
$dialog->joinGroup($this->owner_userid, 0, true);
|
||||
$dialog->pushMsg("groupUpdate", [
|
||||
'id' => $dialog->id,
|
||||
'name' => $dialog->name,
|
||||
'owner_id' => $dialog->owner_id,
|
||||
'group_type' => $dialog->group_type,
|
||||
]);
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'notice', [
|
||||
'notice' => User::nickname() . " 将此群改为部门群"
|
||||
], User::userid(), true, true);
|
||||
}
|
||||
$this->dialog_id = $dialog->id;
|
||||
} else {
|
||||
// 创建群
|
||||
$dialog = WebSocketDialog::createGroup($this->name, [$this->owner_userid], 'department', $this->owner_userid);
|
||||
if (empty($dialog)) {
|
||||
throw new ApiException("创建群组失败");
|
||||
}
|
||||
$this->dialog_id = $dialog->id;
|
||||
}
|
||||
$this->save();
|
||||
//
|
||||
if ($oldUser) {
|
||||
$oldUser->department = array_diff($oldUser->department, [$this->id]);
|
||||
$oldUser->department = "," . implode(",", $oldUser->department) . ",";
|
||||
$oldUser->save();
|
||||
}
|
||||
if ($newUser) {
|
||||
$newUser->department = array_diff($newUser->department, [$this->id]);
|
||||
$newUser->department = array_merge($newUser->department, [$this->id]);
|
||||
$newUser->department = "," . implode(",", $newUser->department) . ",";
|
||||
$newUser->save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除部门
|
||||
* @return void
|
||||
*/
|
||||
public function deleteDepartment() {
|
||||
// 删除子部门
|
||||
$list = self::whereParentId($this->id)->get();
|
||||
foreach ($list as $item) {
|
||||
$item->deleteDepartment();
|
||||
}
|
||||
// 移出成员
|
||||
User::where("department", "like", "%,{$this->id},%")->chunk(100, function($items) {
|
||||
/** @var User $user */
|
||||
foreach ($items as $user) {
|
||||
$user->department = array_diff($user->department, [$this->id]);
|
||||
$user->department = "," . implode(",", $user->department) . ",";
|
||||
$user->save();
|
||||
}
|
||||
});
|
||||
// 解散群组
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
$dialog?->deleteDialog();
|
||||
//
|
||||
$this->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移交部门身份
|
||||
* @param $originalUserid
|
||||
* @param $newUserid
|
||||
* @return void
|
||||
*/
|
||||
public static function transfer($originalUserid, $newUserid)
|
||||
{
|
||||
self::whereOwnerUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
|
||||
/** @var self $item */
|
||||
foreach ($list as $item) {
|
||||
$item->saveDepartment([
|
||||
'owner_userid' => $newUserid,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取所有子部门ID
|
||||
* @param int $departmentId
|
||||
* @return array
|
||||
*/
|
||||
public static function getAllSubDepartmentIds($departmentId)
|
||||
{
|
||||
$subIds = [];
|
||||
$directSubs = self::whereParentId($departmentId)->pluck('id')->toArray();
|
||||
|
||||
foreach ($directSubs as $subId) {
|
||||
$subIds[] = $subId;
|
||||
// 递归获取子部门的子部门
|
||||
$subSubIds = self::getAllSubDepartmentIds($subId);
|
||||
$subIds = array_merge($subIds, $subSubIds);
|
||||
}
|
||||
|
||||
return array_unique($subIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门基本信息(缓存时间1小时)
|
||||
* @param int|array $ids
|
||||
* @return \Illuminate\Support\Collection|static|null
|
||||
*/
|
||||
public static function getDepartmentsByIds($ids)
|
||||
{
|
||||
$ids = is_array($ids) ? $ids : [$ids];
|
||||
$departments = collect();
|
||||
$uncachedIds = [];
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$cacheKey = "department_info_{$id}";
|
||||
$department = Cache::get($cacheKey);
|
||||
if ($department) {
|
||||
$departments->push($department);
|
||||
} else {
|
||||
$uncachedIds[] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($uncachedIds)) {
|
||||
$dbDepartments = self::select(['id', 'name', 'parent_id', 'owner_userid'])->whereIn('id', $uncachedIds)->get();
|
||||
foreach ($dbDepartments as $department) {
|
||||
$cacheKey = "department_info_{$department->id}";
|
||||
Cache::put($cacheKey, $department, 60 * 60); // 1小时
|
||||
$departments->push($department);
|
||||
}
|
||||
}
|
||||
|
||||
// 保持返回顺序与传入ids一致
|
||||
$departments = $departments->keyBy('id');
|
||||
$result = collect();
|
||||
foreach ($ids as $id) {
|
||||
if ($departments->has($id)) {
|
||||
$result->push($departments->get($id));
|
||||
}
|
||||
}
|
||||
|
||||
return is_array($ids) ? $result : $result->first();
|
||||
}
|
||||
|
||||
}
|
||||
312
app/Models/UserDevice.php
Normal file
312
app/Models/UserDevice.php
Normal file
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Lock;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use DeviceDetector\DeviceDetector;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
* App\Models\UserDevice
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 会员ID
|
||||
* @property string|null $hash TOKEN MD5
|
||||
* @property string|null $detail 详细信息
|
||||
* @property string|null $expired_at 过期时间
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property-read int $is_current
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereDetail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereExpiredAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereHash($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice withTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserDevice withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserDevice extends AbstractModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'user_devices';
|
||||
|
||||
public static int $deviceLimit = 200; // 每个用户设备限制数量
|
||||
|
||||
protected $appends = [
|
||||
'is_current',
|
||||
];
|
||||
|
||||
public function getDetailAttribute($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
return Base::json2array($value);
|
||||
}
|
||||
|
||||
public function getIsCurrentAttribute(): int
|
||||
{
|
||||
return $this->hash === md5(Doo::userToken()) ? 1 : 0;
|
||||
}
|
||||
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
|
||||
/**
|
||||
* 缓存key
|
||||
* @param string $hash
|
||||
* @return string
|
||||
*/
|
||||
public static function ck(string $hash): string
|
||||
{
|
||||
return "user_devices:{$hash}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 UA 获取设备信息
|
||||
* @param string $ua
|
||||
* @return array
|
||||
*/
|
||||
private static function getDeviceInfo(string $ua): array
|
||||
{
|
||||
$result = [
|
||||
'ip' => Base::getIp(),
|
||||
'type' => '电脑',
|
||||
'os' => 'Unknown',
|
||||
'browser' => 'Unknown',
|
||||
'version' => '',
|
||||
|
||||
'app_name' => '', // 客户端名称
|
||||
'app_type' => '', // 客户端类型
|
||||
'app_version' => '', // 客户端版本
|
||||
];
|
||||
|
||||
if (empty($ua)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 使用 Device-Detector 解析 UA
|
||||
$dd = new DeviceDetector($ua);
|
||||
|
||||
// 解析 UA 字符串
|
||||
$dd->parse();
|
||||
|
||||
// 获取客户端信息(浏览器)
|
||||
$clientInfo = $dd->getClient();
|
||||
if (!empty($clientInfo)) {
|
||||
$result['browser'] = $clientInfo['name'] ?? 'Unknown';
|
||||
$result['version'] = $clientInfo['version'] ?? '';
|
||||
}
|
||||
|
||||
// 获取操作系统信息
|
||||
$osInfo = $dd->getOs();
|
||||
if (!empty($osInfo)) {
|
||||
$result['os'] = trim(($osInfo['name'] ?? '') . ' ' . ($osInfo['version'] ?? ''));
|
||||
if (empty($result['os'])) {
|
||||
$result['os'] = 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match("/android_kuaifan_eeui/i", $ua)) {
|
||||
// Android 客户端
|
||||
$result['app_type'] = 'Android';
|
||||
if ($dd->getBrandName() && $dd->getModel()) {
|
||||
// 厂商+型号
|
||||
$result['app_name'] = $dd->getBrandName() . ' ' . $dd->getModel();
|
||||
} elseif ($dd->getBrandName()) {
|
||||
// 仅厂商
|
||||
$result['app_name'] = $dd->getBrandName();
|
||||
} elseif ($dd->isTablet()) {
|
||||
// 平板
|
||||
$result['app_name'] = 'Tablet';
|
||||
} elseif ($dd->isPhablet()) {
|
||||
// 平板
|
||||
$result['app_name'] = 'Phablet';
|
||||
}
|
||||
$result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/');
|
||||
} elseif (preg_match("/ios_kuaifan_eeui/i", $ua)) {
|
||||
// iOS 客户端
|
||||
$result['app_type'] = 'iOS';
|
||||
if (preg_match("/(macintosh|ipad)/i", $ua)) {
|
||||
// iPad
|
||||
$result['app_name'] = 'iPad';
|
||||
} elseif (preg_match("/iphone/i", $ua)) {
|
||||
// iPhone
|
||||
$result['app_name'] = 'iPhone';
|
||||
}
|
||||
$result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/');
|
||||
} elseif (preg_match("/dootask/i", $ua)) {
|
||||
// DooTask 客户端
|
||||
$result['app_type'] = $osInfo['name'];
|
||||
$result['app_version'] = self::getAfterVersion($ua, 'dootask/');
|
||||
} else {
|
||||
// 其他客户端
|
||||
$result['app_type'] = 'Web';
|
||||
$result['app_version'] = Base::getClientVersion();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 ua 的 find 之后的内容获取版本号
|
||||
* @param string $ua
|
||||
* @param string $find
|
||||
* @return string
|
||||
*/
|
||||
private static function getAfterVersion(string $ua, string $find): string
|
||||
{
|
||||
$findPattern = preg_quote($find, '/');
|
||||
if (preg_match("/{$findPattern}(.*?)(?:\s|$)/i", $ua, $matches)) {
|
||||
$appInfo = $matches[1];
|
||||
|
||||
// 从内容中提取版本号(寻找符合x.x.x格式的部分)
|
||||
if (preg_match("/(\d+\.\d+(?:\.\d+)*)/", $appInfo, $versionMatches)) {
|
||||
return $versionMatches[1];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
/** ****************************************************************************** */
|
||||
|
||||
/**
|
||||
* 检查用户是否存在
|
||||
* @return string|null
|
||||
*/
|
||||
public static function check(): ?string
|
||||
{
|
||||
$token = Doo::userToken();
|
||||
$userid = Doo::userId();
|
||||
|
||||
$hash = md5($token);
|
||||
if (Cache::has(self::ck($hash))) {
|
||||
return $hash;
|
||||
}
|
||||
|
||||
$row = self::whereHash($hash)->first();
|
||||
if ($row) {
|
||||
// 判断是否过期
|
||||
if ($row->expired_at && Carbon::parse($row->expired_at)->isPast()) {
|
||||
self::forget($row);
|
||||
return null;
|
||||
}
|
||||
// 更新缓存
|
||||
self::record();
|
||||
return $hash;
|
||||
}
|
||||
// 没有记录
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录设备(添加、更新)
|
||||
* @param string|null $token
|
||||
* @return self|null
|
||||
*/
|
||||
public static function record(string $token = null): ?self
|
||||
{
|
||||
if (empty($token)) {
|
||||
$token = Doo::userToken();
|
||||
$userid = Doo::userId();
|
||||
$expiredAt = Doo::userExpiredAt();
|
||||
} else {
|
||||
$info = Doo::tokenDecode($token);
|
||||
$userid = $info['userid'] ?? 0;
|
||||
$expiredAt = $info['expired_at'];
|
||||
}
|
||||
$hash = md5($token);
|
||||
//
|
||||
return Lock::withLock("userDeviceRecord:{$hash}", function () use ($expiredAt, $userid, $hash, $token) {
|
||||
return AbstractModel::transaction(function () use ($expiredAt, $userid, $hash, $token) {
|
||||
$row = self::whereHash($hash)->first();
|
||||
if (empty($row)) {
|
||||
// 生成一个新的设备记录
|
||||
$row = self::createInstance([
|
||||
'userid' => $userid,
|
||||
'hash' => $hash,
|
||||
]);
|
||||
if (!$row->save()) {
|
||||
return null;
|
||||
}
|
||||
// 删除多余的设备记录
|
||||
$currentDeviceCount = self::whereUserid($userid)->count();
|
||||
if ($currentDeviceCount > self::$deviceLimit) {
|
||||
$rows = self::whereUserid($userid)->orderBy('id')->take($currentDeviceCount - self::$deviceLimit)->get();
|
||||
foreach ($rows as $row) {
|
||||
UserDevice::forget($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
$row->expired_at = $expiredAt;
|
||||
if (Request::hasHeader('version')) {
|
||||
$deviceInfo = array_merge(Base::json2array($row->detail), self::getDeviceInfo($_SERVER['HTTP_USER_AGENT'] ?? ''));
|
||||
$row->detail = Base::array2json($deviceInfo);
|
||||
}
|
||||
$row->save();
|
||||
|
||||
Cache::put(self::ck($hash), $row->userid, now()->addHour());
|
||||
return $row;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记设备(删除)
|
||||
* @param UserDevice|string|int|null $token
|
||||
* - UserDevice 表示指定的设备对象
|
||||
* - string 表示指定的 token
|
||||
* - int 表示指定的数据ID
|
||||
* - null 表示当前登录的设备
|
||||
* @return void
|
||||
*/
|
||||
public static function forget(UserDevice|string|int $token = null): void
|
||||
{
|
||||
if ($token instanceof UserDevice) {
|
||||
$hash = $token->hash;
|
||||
$token->delete();
|
||||
} elseif (Base::isNumber($token)) {
|
||||
$row = self::find(intval($token));
|
||||
if ($row) {
|
||||
$hash = $row->hash;
|
||||
$row->delete();
|
||||
}
|
||||
} else {
|
||||
if ($token === null) {
|
||||
$token = Doo::userToken();
|
||||
}
|
||||
if ($token) {
|
||||
$hash = md5($token);
|
||||
self::whereHash($hash)->delete();
|
||||
}
|
||||
}
|
||||
if (isset($hash)) {
|
||||
Cache::forget(self::ck($hash));
|
||||
UmengAlias::whereDeviceHash($hash)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
153
app/Models/UserEmailVerification.php
Normal file
153
app/Models/UserEmailVerification.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Timer;
|
||||
use Carbon\Carbon;
|
||||
use Guanguans\Notify\Factory;
|
||||
use Guanguans\Notify\Messages\EmailMessage;
|
||||
|
||||
/**
|
||||
* App\Models\UserEmailVerification
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 用户id
|
||||
* @property string|null $code 验证参数
|
||||
* @property string|null $email 电子邮箱
|
||||
* @property int|null $status 0-未验证,1-已验证
|
||||
* @property int|null $type 邮件类型:1-邮箱认证,2-修改邮箱
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|UserEmailVerification newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereCode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereEmail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserEmailVerification extends AbstractModel
|
||||
{
|
||||
|
||||
/**
|
||||
* 发验证邮箱
|
||||
* @param User $user
|
||||
* @param int $type
|
||||
* @param null $email
|
||||
*/
|
||||
public static function userEmailSend(User $user, $type = 1, $email = null)
|
||||
{
|
||||
$email = $type == 1 ? $user->email : $email;
|
||||
$res = self::whereEmail($email)->where('created_at', '>', Carbon::now()->subMinutes(30))->whereType($type)->first();
|
||||
if ($res && $type == 1) return;
|
||||
//删除
|
||||
self::whereUserid($email)->delete();
|
||||
$code = $type == 1 ? Base::generatePassword(64) : rand(100000, 999999);
|
||||
$row = self::createInstance([
|
||||
'userid' => $user->userid,
|
||||
'email' => $email,
|
||||
'code' => $code,
|
||||
'status' => 0,
|
||||
'type' => $type
|
||||
]);
|
||||
$row->save();
|
||||
$setting = Base::setting('emailSetting');
|
||||
$alias = Base::settingFind('system', 'system_alias', 'Task');
|
||||
try {
|
||||
if (!Base::isEmail($email)) {
|
||||
throw new \Exception("User email '{$email}' address error");
|
||||
}
|
||||
switch ($type) {
|
||||
case 2:
|
||||
$subject = Doo::translate($alias . "修改邮箱验证");
|
||||
$content = sprintf("<p>%s</p><p style='color: #0000DD;'><u>%s</u></p><p>%s</p>",
|
||||
Doo::translate($user->nickname . " 您好,您正在修改 " . $alias . " 的邮箱,验证码如下。请在30分钟内输入验证码"),
|
||||
$code,
|
||||
Doo::translate("如果不是本人操作,您的帐号可能存在风险,请及时修改密码!")
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
$subject = Doo::translate($alias . "注销帐号验证");
|
||||
$content = sprintf("<p>%s</p><p style='color: #0000DD;'><u>%s</u></p><p>%s</p>",
|
||||
Doo::translate($user->nickname . " 您好,您正在注销 " . $alias . " 的帐号,验证码如下。请在30分钟内输入验证码"),
|
||||
$code,
|
||||
Doo::translate("如果不是本人操作,您的帐号可能存在风险,请及时修改密码!")
|
||||
);
|
||||
break;
|
||||
default:
|
||||
$url = Base::fillUrl('single/valid/email') . '?code=' . $row->code;
|
||||
$subject = Doo::translate($alias . "绑定邮箱验证");
|
||||
$content = sprintf("<p>%s</p><p style='display: flex; justify-content: center;'>%s</p>",
|
||||
Doo::translate($user->nickname . " 您好,您正在绑定 " . $alias . " 的邮箱,请于30分钟之内点击以下链接完成验证:"),
|
||||
"<a href='{$url}' target='_blank'>{$url}</a>"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Factory::mailer()
|
||||
->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0")
|
||||
->setMessage(EmailMessage::create()
|
||||
->from($alias . " <{$setting['account']}>")
|
||||
->to($email)
|
||||
->subject($subject)
|
||||
->html($content))
|
||||
->send();
|
||||
} catch (\Throwable $e) {
|
||||
if (str_contains($e->getMessage(), "Timed Out")) {
|
||||
throw new ApiException("邮件发送超时,请检查邮箱配置是否正确");
|
||||
} elseif ($e->getCode() === 550) {
|
||||
throw new ApiException('邮件内容被拒绝,请检查邮箱是否开启接收功能');
|
||||
} else {
|
||||
throw new ApiException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
* @param $email
|
||||
* @param $code
|
||||
* @param int $type
|
||||
* @return bool
|
||||
*/
|
||||
public static function verify($email, $code, $type = 1)
|
||||
{
|
||||
if (!$code) {
|
||||
throw new ApiException('请输入验证码');
|
||||
}
|
||||
/** @var UserEmailVerification $emailVerify */
|
||||
$emailVerify = self::whereEmail($email)->whereType($type)->orderByDesc('id')->first();
|
||||
|
||||
if (empty($emailVerify) || $emailVerify->code != $code) {
|
||||
throw new ApiException('验证码错误');
|
||||
}
|
||||
|
||||
$oldTime = Carbon::parse($emailVerify->created_at)->timestamp;
|
||||
$time = Timer::Time();
|
||||
|
||||
// 30分钟失效
|
||||
if (abs($time - $oldTime) > 1800) {
|
||||
throw new ApiException('验证码已失效');
|
||||
}
|
||||
|
||||
self::whereEmail($email)->whereCode($code)->whereType($type)->update([
|
||||
'status' => 1
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
340
app/Models/UserFavorite.php
Normal file
340
app/Models/UserFavorite.php
Normal file
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\File;
|
||||
|
||||
/**
|
||||
* App\Models\UserFavorite
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 用户ID
|
||||
* @property string|null $favoritable_type 收藏类型(比如:task/project/file/message)
|
||||
* @property int|null $favoritable_id 收藏对象ID
|
||||
* @property string $remark 收藏备注
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Model|\Eloquent $favoritable
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereFavoritableId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereFavoritableType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereRemark($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserFavorite whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserFavorite extends AbstractModel
|
||||
{
|
||||
const TYPE_TASK = 'task';
|
||||
const TYPE_PROJECT = 'project';
|
||||
const TYPE_FILE = 'file';
|
||||
const TYPE_MESSAGE = 'message';
|
||||
|
||||
protected $fillable = [
|
||||
'userid',
|
||||
'favoritable_type',
|
||||
'favoritable_id',
|
||||
'remark',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联用户
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid', 'userid');
|
||||
}
|
||||
|
||||
/**
|
||||
* 多态关联
|
||||
*/
|
||||
public function favoritable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换收藏状态
|
||||
* @param int $userid 用户ID
|
||||
* @param string $type 收藏类型
|
||||
* @param int $id 收藏对象ID
|
||||
* @return array ['favorited' => bool, 'action' => 'added'|'removed']
|
||||
*/
|
||||
public static function toggleFavorite($userid, $type, $id)
|
||||
{
|
||||
$favorite = self::whereUserid($userid)
|
||||
->whereFavoritableType($type)
|
||||
->whereFavoritableId($id)
|
||||
->first();
|
||||
|
||||
if ($favorite) {
|
||||
// 取消收藏
|
||||
$favorite->delete();
|
||||
return ['favorited' => false, 'action' => 'removed', 'remark' => ''];
|
||||
}
|
||||
|
||||
// 添加收藏
|
||||
$favorite = self::create([
|
||||
'userid' => $userid,
|
||||
'favoritable_type' => $type,
|
||||
'favoritable_id' => $id,
|
||||
]);
|
||||
|
||||
return ['favorited' => true, 'action' => 'added', 'remark' => $favorite->remark ?? ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新收藏备注
|
||||
* @param int $userid
|
||||
* @param string $type
|
||||
* @param int $id
|
||||
* @param string $remark
|
||||
* @return static|null
|
||||
*/
|
||||
public static function updateRemark($userid, $type, $id, $remark)
|
||||
{
|
||||
$favorite = self::whereUserid($userid)
|
||||
->whereFavoritableType($type)
|
||||
->whereFavoritableId($id)
|
||||
->first();
|
||||
|
||||
if (!$favorite) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$favorite->remark = $remark;
|
||||
$favorite->save();
|
||||
|
||||
return $favorite;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已收藏
|
||||
* @param int $userid 用户ID
|
||||
* @param string $type 收藏类型
|
||||
* @param int $id 收藏对象ID
|
||||
* @return bool
|
||||
*/
|
||||
public static function isFavorited($userid, $type, $id)
|
||||
{
|
||||
return self::whereUserid($userid)
|
||||
->whereFavoritableType($type)
|
||||
->whereFavoritableId($id)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户收藏列表
|
||||
* @param int $userid 用户ID
|
||||
* @param string|null $type 收藏类型过滤
|
||||
* @param int $page 页码
|
||||
* @param int $pageSize 每页数量
|
||||
* @return array
|
||||
*/
|
||||
public static function getUserFavorites($userid, $type = null, $page = 1, $pageSize = 20)
|
||||
{
|
||||
$query = self::whereUserid($userid)->orderByDesc('created_at');
|
||||
|
||||
if ($type) {
|
||||
$query->whereFavoritableType($type);
|
||||
}
|
||||
|
||||
$favorites = $query->paginate($pageSize, ['*'], 'page', $page);
|
||||
|
||||
$data = [
|
||||
'tasks' => [],
|
||||
'projects' => [],
|
||||
'files' => [],
|
||||
'messages' => []
|
||||
];
|
||||
|
||||
// 分组收集ID
|
||||
$taskIds = [];
|
||||
$projectIds = [];
|
||||
$fileIds = [];
|
||||
$messageIds = [];
|
||||
|
||||
foreach ($favorites->items() as $favorite) {
|
||||
switch ($favorite->favoritable_type) {
|
||||
case self::TYPE_TASK:
|
||||
$taskIds[] = $favorite->favoritable_id;
|
||||
break;
|
||||
case self::TYPE_PROJECT:
|
||||
$projectIds[] = $favorite->favoritable_id;
|
||||
break;
|
||||
case self::TYPE_FILE:
|
||||
$fileIds[] = $favorite->favoritable_id;
|
||||
break;
|
||||
case self::TYPE_MESSAGE:
|
||||
$messageIds[] = $favorite->favoritable_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询具体数据
|
||||
if (!empty($taskIds)) {
|
||||
$tasks = ProjectTask::select([
|
||||
'project_tasks.id',
|
||||
'project_tasks.name',
|
||||
'project_tasks.project_id',
|
||||
'project_tasks.complete_at',
|
||||
'project_tasks.created_at',
|
||||
'project_tasks.flow_item_id',
|
||||
'project_tasks.flow_item_name',
|
||||
'projects.name as project_name'
|
||||
])
|
||||
->leftJoin('projects', 'project_tasks.project_id', '=', 'projects.id')
|
||||
->whereIn('project_tasks.id', $taskIds)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
foreach ($favorites->items() as $favorite) {
|
||||
if ($favorite->favoritable_type === self::TYPE_TASK && isset($tasks[$favorite->favoritable_id])) {
|
||||
$task = $tasks[$favorite->favoritable_id];
|
||||
|
||||
// 解析 flow_item_name 字段(格式:status|name|color)
|
||||
$flowItemParts = explode('|', $task->flow_item_name ?: '');
|
||||
$flowItemStatus = $flowItemParts[0] ?? '';
|
||||
$flowItemName = $flowItemParts[1] ?? $task->flow_item_name;
|
||||
$flowItemColor = $flowItemParts[2] ?? '';
|
||||
|
||||
$data['tasks'][] = [
|
||||
'id' => $task->id,
|
||||
'name' => $task->name,
|
||||
'project_id' => $task->project_id,
|
||||
'project_name' => $task->project_name,
|
||||
'complete_at' => $task->complete_at,
|
||||
'flow_item_id' => $task->flow_item_id,
|
||||
'flow_item_name' => $flowItemName,
|
||||
'flow_item_status' => $flowItemStatus,
|
||||
'flow_item_color' => $flowItemColor,
|
||||
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
|
||||
'remark' => $favorite->remark,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($projectIds)) {
|
||||
$projects = Project::select([
|
||||
'id', 'name', 'desc', 'archived_at', 'created_at'
|
||||
])->whereIn('id', $projectIds)->get()->keyBy('id');
|
||||
|
||||
foreach ($favorites->items() as $favorite) {
|
||||
if ($favorite->favoritable_type === self::TYPE_PROJECT && isset($projects[$favorite->favoritable_id])) {
|
||||
$project = $projects[$favorite->favoritable_id];
|
||||
$data['projects'][] = [
|
||||
'id' => $project->id,
|
||||
'name' => $project->name,
|
||||
'desc' => $project->desc,
|
||||
'archived_at' => $project->archived_at,
|
||||
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
|
||||
'remark' => $favorite->remark,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fileIds)) {
|
||||
$files = File::select([
|
||||
'id', 'name', 'ext', 'size', 'pid', 'created_at'
|
||||
])->whereIn('id', $fileIds)->get()->keyBy('id');
|
||||
|
||||
foreach ($favorites->items() as $favorite) {
|
||||
if ($favorite->favoritable_type === self::TYPE_FILE && isset($files[$favorite->favoritable_id])) {
|
||||
$file = $files[$favorite->favoritable_id];
|
||||
$fileData = File::handleImageUrl(array_merge(
|
||||
$file->only(['id', 'ext']),
|
||||
[
|
||||
'name' => $file->name,
|
||||
'size' => $file->size,
|
||||
'pid' => $file->pid,
|
||||
]
|
||||
));
|
||||
$data['files'][] = [
|
||||
'id' => $file->id,
|
||||
'name' => $file->name,
|
||||
'ext' => $file->ext,
|
||||
'size' => $file->size,
|
||||
'pid' => $file->pid,
|
||||
'image_url' => $fileData['image_url'] ?? null,
|
||||
'image_width' => $fileData['image_width'] ?? null,
|
||||
'image_height' => $fileData['image_height'] ?? null,
|
||||
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
|
||||
'remark' => $favorite->remark,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($messageIds)) {
|
||||
$messages = WebSocketDialogMsg::select([
|
||||
'id', 'dialog_id', 'userid', 'type', 'msg', 'created_at'
|
||||
])->whereIn('id', $messageIds)->get()->keyBy('id');
|
||||
|
||||
foreach ($favorites->items() as $favorite) {
|
||||
if ($favorite->favoritable_type === self::TYPE_MESSAGE && isset($messages[$favorite->favoritable_id])) {
|
||||
$message = $messages[$favorite->favoritable_id];
|
||||
|
||||
// 使用 previewMsg 获取消息预览文本
|
||||
$previewText = '';
|
||||
if ($message->msg && is_array($message->msg)) {
|
||||
$previewText = WebSocketDialogMsg::previewMsg($message);
|
||||
}
|
||||
|
||||
// 如果没有预览文本,使用消息类型作为标题
|
||||
if (empty($previewText)) {
|
||||
$previewText = '[' . ucfirst($message->type) . ']';
|
||||
}
|
||||
|
||||
$data['messages'][] = [
|
||||
'id' => $message->id,
|
||||
'name' => $previewText,
|
||||
'dialog_id' => $message->dialog_id,
|
||||
'userid' => $message->userid,
|
||||
'type' => $message->type,
|
||||
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
|
||||
'remark' => $favorite->remark,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'total' => $favorites->total(),
|
||||
'current_page' => $favorites->currentPage(),
|
||||
'per_page' => $favorites->perPage(),
|
||||
'last_page' => $favorites->lastPage(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理用户收藏
|
||||
* @param int $userid 用户ID
|
||||
* @param string|null $type 收藏类型,null表示全部类型
|
||||
* @return int 删除的记录数
|
||||
*/
|
||||
public static function cleanUserFavorites($userid, $type = null)
|
||||
{
|
||||
$query = self::whereUserid($userid);
|
||||
|
||||
if ($type) {
|
||||
$query->whereFavoritableType($type);
|
||||
}
|
||||
|
||||
return $query->delete();
|
||||
}
|
||||
}
|
||||
79
app/Models/UserRecentItem.php
Normal file
79
app/Models/UserRecentItem.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\UserRecentItem
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $userid 用户ID
|
||||
* @property string $target_type 目标类型(task/file/task_file/message_file 等)
|
||||
* @property int $target_id 目标ID
|
||||
* @property string $source_type 来源类型(project/filesystem/project_task/dialog 等)
|
||||
* @property int $source_id 来源ID
|
||||
* @property \Illuminate\Support\Carbon|null $browsed_at 浏览时间
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereBrowsedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereSourceId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereSourceType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereTargetId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereTargetType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserRecentItem whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserRecentItem extends AbstractModel
|
||||
{
|
||||
public const TYPE_TASK = 'task';
|
||||
public const TYPE_FILE = 'file';
|
||||
public const TYPE_TASK_FILE = 'task_file';
|
||||
public const TYPE_MESSAGE_FILE = 'message_file';
|
||||
|
||||
public const SOURCE_PROJECT = 'project';
|
||||
public const SOURCE_FILESYSTEM = 'filesystem';
|
||||
public const SOURCE_PROJECT_TASK = 'project_task';
|
||||
public const SOURCE_DIALOG = 'dialog';
|
||||
|
||||
protected $fillable = [
|
||||
'userid',
|
||||
'target_type',
|
||||
'target_id',
|
||||
'source_type',
|
||||
'source_id',
|
||||
'browsed_at',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'browsed_at',
|
||||
];
|
||||
|
||||
public static function record(int $userid, string $targetType, int $targetId, string $sourceType = '', int $sourceId = 0): self
|
||||
{
|
||||
return self::updateOrCreate(
|
||||
[
|
||||
'userid' => $userid,
|
||||
'target_type' => $targetType,
|
||||
'target_id' => $targetId,
|
||||
'source_type' => $sourceType,
|
||||
'source_id' => $sourceId,
|
||||
],
|
||||
[
|
||||
'browsed_at' => Carbon::now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
138
app/Models/UserTaskBrowse.php
Normal file
138
app/Models/UserTaskBrowse.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\UserTaskBrowse
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $userid 用户ID
|
||||
* @property int|null $task_id 任务ID
|
||||
* @property \Illuminate\Support\Carbon|null $browsed_at 浏览时间
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\ProjectTask|null $task
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereBrowsedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTaskBrowse whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserTaskBrowse extends AbstractModel
|
||||
{
|
||||
protected $fillable = [
|
||||
'userid',
|
||||
'task_id',
|
||||
'browsed_at',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'browsed_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联用户
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid', 'userid');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联任务
|
||||
*/
|
||||
public function task()
|
||||
{
|
||||
return $this->belongsTo(ProjectTask::class, 'task_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录用户浏览任务
|
||||
* @param int $userid 用户ID
|
||||
* @param int $task_id 任务ID
|
||||
* @return UserTaskBrowse
|
||||
*/
|
||||
public static function recordBrowse($userid, $task_id)
|
||||
{
|
||||
$record = self::updateOrCreate(
|
||||
[
|
||||
'userid' => $userid,
|
||||
'task_id' => $task_id,
|
||||
],
|
||||
[
|
||||
'browsed_at' => Carbon::now(),
|
||||
]
|
||||
);
|
||||
|
||||
UserRecentItem::record(
|
||||
$userid,
|
||||
UserRecentItem::TYPE_TASK,
|
||||
$task_id,
|
||||
UserRecentItem::SOURCE_PROJECT,
|
||||
0
|
||||
);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户浏览历史
|
||||
* @param int $userid 用户ID
|
||||
* @param int $limit 获取数量
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public static function getUserBrowseHistory($userid, $limit = 20)
|
||||
{
|
||||
return self::with(['task' => function ($query) {
|
||||
$query->select([
|
||||
'id', 'name', 'project_id', 'column_id', 'parent_id',
|
||||
'flow_item_id', 'flow_item_name',
|
||||
'complete_at', 'archived_at'
|
||||
]);
|
||||
}])
|
||||
->whereUserid($userid)
|
||||
->whereHas('task', function ($query) {
|
||||
// 只获取存在且未被删除的任务
|
||||
$query->whereNull('archived_at');
|
||||
})
|
||||
->orderByDesc('browsed_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理用户浏览历史
|
||||
* @param int $userid 用户ID
|
||||
* @param int $keepCount 保留数量,0表示全部删除
|
||||
* @return int 删除的记录数
|
||||
*/
|
||||
public static function cleanUserBrowseHistory($userid, $keepCount = 100)
|
||||
{
|
||||
if ($keepCount === 0) {
|
||||
return self::whereUserid($userid)->delete();
|
||||
}
|
||||
|
||||
$keepIds = self::whereUserid($userid)
|
||||
->orderByDesc('browsed_at')
|
||||
->limit($keepCount)
|
||||
->pluck('id');
|
||||
|
||||
return self::whereUserid($userid)
|
||||
->whereNotIn('id', $keepIds)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
107
app/Models/UserTransfer.php
Normal file
107
app/Models/UserTransfer.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
use Guanguans\Notify\Factory;
|
||||
use Guanguans\Notify\Messages\EmailMessage;
|
||||
|
||||
/**
|
||||
* App\Models\UserTransfer
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $original_userid 原作者
|
||||
* @property int|null $new_userid 交接人
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|UserTransfer newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereNewUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereOriginalUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UserTransfer extends AbstractModel
|
||||
{
|
||||
|
||||
/**
|
||||
* 开始移交
|
||||
* @return void
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
// 移交部门
|
||||
UserDepartment::transfer($this->original_userid, $this->new_userid);
|
||||
// 移交项目身份
|
||||
ProjectUser::transfer($this->original_userid, $this->new_userid);
|
||||
// 移交任务身份
|
||||
ProjectTaskUser::transfer($this->original_userid, $this->new_userid);
|
||||
// 移交文件
|
||||
File::transfer($this->original_userid, $this->new_userid);
|
||||
// 离职移出群组
|
||||
$this->exitDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出群组
|
||||
* @return void
|
||||
*/
|
||||
public function exitDialog()
|
||||
{
|
||||
$lastId = 0;
|
||||
$limit = 100;
|
||||
while (true) {
|
||||
$query = WebSocketDialog::select(['web_socket_dialogs.*'])
|
||||
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
|
||||
->where('web_socket_dialogs.type', 'group')
|
||||
->where('web_socket_dialogs.group_type', '!=', 'okr')
|
||||
->where('u.userid', $this->original_userid)
|
||||
->orderBy('web_socket_dialogs.id')
|
||||
->limit($limit);
|
||||
if ($lastId) {
|
||||
$query->where('web_socket_dialogs.id', '>', $lastId);
|
||||
}
|
||||
$list = $query->get();
|
||||
|
||||
// 没有数据了就退出
|
||||
if ($list->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 记录最后一条记录的ID
|
||||
$lastId = $list->last()->id;
|
||||
|
||||
// 离职员工退出群
|
||||
foreach ($list as $dialog) {
|
||||
$dialog->exitGroup($this->original_userid, 'remove', false, false);
|
||||
if ($dialog->owner_id === $this->original_userid) {
|
||||
// 如果是群主则把交接人设为群主
|
||||
$dialog->owner_id = $this->new_userid;
|
||||
if ($dialog->save()) {
|
||||
$dialog->joinGroup($this->new_userid, 0);
|
||||
$dialog->pushMsg("groupUpdate", [
|
||||
'id' => $dialog->id,
|
||||
'owner_id' => $dialog->owner_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果返回的数据少于限制数,说明已经是最后一批
|
||||
if ($list->count() < $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,15 @@ namespace App\Models;
|
||||
* @property int|null $userid
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocket newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocket newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocket query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocket whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocket whereFd($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocket whereId($value)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
64
app/Models/WebSocketDialogConfig.php
Normal file
64
app/Models/WebSocketDialogConfig.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogConfig
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $dialog_id 对话ID
|
||||
* @property int $userid 用户ID
|
||||
* @property string $type 配置类型
|
||||
* @property string|null $value 配置值
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\WebSocketDialog|null $dialog
|
||||
* @property-read \App\Models\User $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereValue($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class WebSocketDialogConfig extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 可以批量赋值的属性
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'dialog_id',
|
||||
'userid',
|
||||
'type',
|
||||
'value',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取关联的对话
|
||||
*/
|
||||
public function dialog()
|
||||
{
|
||||
return $this->belongsTo(WebSocketDialog::class, 'dialog_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关联的用户
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,23 +2,41 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogMsgRead
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property int|null $msg_id 消息ID
|
||||
* @property int|null $userid 发送会员ID
|
||||
* @property int|null $userid 接收会员ID
|
||||
* @property int|null $mention 是否提及(被@)
|
||||
* @property int|null $silence 是否免打扰:0否,1是
|
||||
* @property int|null $email 是否发了邮件
|
||||
* @property int|null $after 在阅读之后才添加的记录
|
||||
* @property string|null $read_at 阅读时间
|
||||
* @property int|null $dot 红点标记
|
||||
* @property \Illuminate\Support\Carbon|null $read_at 阅读时间
|
||||
* @property-read \App\Models\WebSocketDialogMsg|null $webSocketDialogMsg
|
||||
* @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|WebSocketDialogMsgRead newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereAfter($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereDot($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereEmail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereMention($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereMsgId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereReadAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereSilence($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
@@ -29,4 +47,78 @@ class WebSocketDialogMsgRead extends AbstractModel
|
||||
parent::__construct($attributes);
|
||||
$this->timestamps = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function webSocketDialogMsg(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(WebSocketDialogMsg::class, 'id', 'msg_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制标记成阅读
|
||||
* @param $dialogId
|
||||
* @param $userId
|
||||
* @return void
|
||||
*/
|
||||
public static function forceRead($dialogId, $userId)
|
||||
{
|
||||
self::whereDialogId($dialogId)
|
||||
->whereUserid($userId)
|
||||
->whereNull('read_at')
|
||||
->update(['read_at' => Carbon::now()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅标记成阅读
|
||||
* @param $list
|
||||
* @return void
|
||||
*/
|
||||
public static function onlyMarkRead($list)
|
||||
{
|
||||
if (empty($list)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$collection = collect($list);
|
||||
if ($collection->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
$ids = [];
|
||||
$msgCounts = [];
|
||||
|
||||
/** @var WebSocketDialogMsgRead $item */
|
||||
foreach ($collection as $item) {
|
||||
$ids[] = $item->id;
|
||||
if ($item->msg_id) {
|
||||
$msgCounts[$item->msg_id] = ($msgCounts[$item->msg_id] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($ids)) {
|
||||
DB::table((new self())->getTable())
|
||||
->whereIn('id', $ids)
|
||||
->whereNull('read_at')
|
||||
->update(['read_at' => $now]);
|
||||
}
|
||||
|
||||
if (!empty($msgCounts)) {
|
||||
$cases = [];
|
||||
$bindings = [];
|
||||
foreach ($msgCounts as $msgId => $num) {
|
||||
$cases[] = 'WHEN ? THEN ?';
|
||||
$bindings[] = $msgId;
|
||||
$bindings[] = $num;
|
||||
}
|
||||
$msgIds = array_keys($msgCounts);
|
||||
$bindings = array_merge($bindings, $msgIds);
|
||||
$placeholders = implode(',', array_fill(0, count($msgIds), '?'));
|
||||
$table = DB::getTablePrefix() . (new WebSocketDialogMsg())->getTable();
|
||||
$sql = "UPDATE {$table} SET `read` = `read` + CASE `id` " . implode(' ', $cases) . " END WHERE `deleted_at` IS NULL AND `id` IN ({$placeholders})";
|
||||
DB::update($sql, $bindings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
53
app/Models/WebSocketDialogMsgTodo.php
Normal file
53
app/Models/WebSocketDialogMsgTodo.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogMsgTodo
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property int|null $msg_id 消息ID
|
||||
* @property int|null $userid 接收会员ID
|
||||
* @property \Illuminate\Support\Carbon|null $done_at 完成时间
|
||||
* @property-read array|mixed $msg_data
|
||||
* @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|WebSocketDialogMsgTodo newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereDoneAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereMsgId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class WebSocketDialogMsgTodo extends AbstractModel
|
||||
{
|
||||
protected $appends = [
|
||||
'msg_data',
|
||||
];
|
||||
|
||||
function __construct(array $attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
$this->timestamps = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息详情
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function getMsgDataAttribute()
|
||||
{
|
||||
if (!isset($this->appendattrs['msgData'])) {
|
||||
$this->appendattrs['msgData'] = WebSocketDialogMsg::select(['id', 'type', 'msg'])->whereId($this->msg_id)->first()?->cancelAppend();
|
||||
}
|
||||
return $this->appendattrs['msgData'];
|
||||
}
|
||||
}
|
||||
36
app/Models/WebSocketDialogMsgTranslate.php
Normal file
36
app/Models/WebSocketDialogMsgTranslate.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogMsgTranslate
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property int|null $msg_id 消息ID
|
||||
* @property string|null $language 语言
|
||||
* @property string|null $content 翻译内容
|
||||
* @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|WebSocketDialogMsgTranslate newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereContent($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereLanguage($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereMsgId($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class WebSocketDialogMsgTranslate extends AbstractModel
|
||||
{
|
||||
function __construct(array $attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
$this->timestamps = false;
|
||||
}
|
||||
}
|
||||
86
app/Models/WebSocketDialogSession.php
Normal file
86
app/Models/WebSocketDialogSession.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Tasks\UpdateSessionTitleViaAiTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Cache;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogSession
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $dialog_id 对话ID
|
||||
* @property string $title 会话标题
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\WebSocketDialog|null $dialog
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereTitle($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class WebSocketDialogSession extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 可以批量赋值的属性
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'dialog_id',
|
||||
'userid',
|
||||
'title',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取关联的对话
|
||||
*/
|
||||
public function dialog()
|
||||
{
|
||||
return $this->belongsTo(WebSocketDialog::class, 'dialog_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $sessionId
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public static function updateTitle($sessionId, $dialogMsg)
|
||||
{
|
||||
if (!$sessionId) {
|
||||
return;
|
||||
}
|
||||
if ($dialogMsg->type != 'text') {
|
||||
return;
|
||||
}
|
||||
if ($dialogMsg->msg['text'] === '...') {
|
||||
return;
|
||||
}
|
||||
$cacheKey = 'dialog_session_title_' . $sessionId;
|
||||
if (Cache::has($cacheKey)) {
|
||||
return;
|
||||
}
|
||||
$title = $dialogMsg->key ?: WebSocketDialogMsg::previewTextMsg($dialogMsg->msg) ?: 'Untitled';
|
||||
$session = self::whereId($sessionId)->first();
|
||||
if (!$session) {
|
||||
return;
|
||||
}
|
||||
$session->title = $title;
|
||||
$session->save();
|
||||
Cache::forever($cacheKey, true);
|
||||
Task::deliver(new UpdateSessionTitleViaAiTask($session->id, $dialogMsg->msg['text']));
|
||||
}
|
||||
}
|
||||
@@ -8,19 +8,65 @@ 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是
|
||||
* @property int|null $silence 是否免打扰:0否,1是
|
||||
* @property int|null $hide 不显示会话:0否,1是
|
||||
* @property int|null $inviter 邀请人
|
||||
* @property int|null $important 是否不可移出(项目、任务、部门人员)
|
||||
* @property string|null $color 颜色
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|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|WebSocketDialogUser newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser newQuery()
|
||||
* @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)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereHide($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereImportant($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereInviter($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereLastAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereMarkUnread($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereSilence($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereTopAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class WebSocketDialogUser extends AbstractModel
|
||||
{
|
||||
protected $dateFormat = 'Y-m-d H:i:s.u';
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function webSocketDialog(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(WebSocketDialog::class, 'id', 'dialog_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对话最后消息时间
|
||||
* @return WebSocketDialogMsg|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|object|null
|
||||
*/
|
||||
public static function updateMsgLastAt($dialogId)
|
||||
{
|
||||
$lastMsg = WebSocketDialogMsg::whereDialogId($dialogId)->orderByDesc('id')->first();
|
||||
if ($lastMsg) {
|
||||
WebSocketDialogUser::whereDialogId($dialogId)->change(['last_at' => $lastMsg->created_at]);
|
||||
}
|
||||
return $lastMsg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,15 @@ namespace App\Models;
|
||||
* @property int|null $create_id 所属会员ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketTmpMsg newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketTmpMsg newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketTmpMsg query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketTmpMsg whereCreateId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketTmpMsg whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketTmpMsg whereId($value)
|
||||
|
||||
29
app/Models/clearHelper.php
Executable file
29
app/Models/clearHelper.php
Executable file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 清除模型class注释
|
||||
*/
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
// 判断是否通过cmd命令执行的
|
||||
if (function_exists('info')){
|
||||
echo "Success \n";
|
||||
return;
|
||||
}
|
||||
|
||||
$path = dirname(__FILE__). '/';
|
||||
$lists = scandir($path);
|
||||
//
|
||||
foreach ($lists AS $item) {
|
||||
$fillPath = $path . $item;
|
||||
if (!in_array($item, ['AbstractModel.php', 'clearHelper.php']) && str_ends_with($item, '.php')) {
|
||||
$content = file_get_contents($fillPath);
|
||||
preg_match("/\/\*\*([\s\S]*?)class\s*(.*?)\s*extends\s*AbstractModel/i", $content, $matchs);
|
||||
if ($matchs[0]) {
|
||||
$content = str_replace($matchs[0], 'class ' . $matchs[2] . ' extends AbstractModel', $content);
|
||||
file_put_contents($fillPath, $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
echo "Success \n";
|
||||
933
app/Module/AI.php
Normal file
933
app/Module/AI.php
Normal file
@@ -0,0 +1,933 @@
|
||||
<?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-5-nano",
|
||||
"reasoning_effort" => "minimal",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一名资深的专业翻译专家,专门从事项目任务管理系统的多语言本地化工作。
|
||||
|
||||
翻译任务:将提供的文本内容翻译为 {$targetLanguage}
|
||||
|
||||
专业要求:
|
||||
1. 术语一致性:确保项目管理、任务管理、团队协作等专业术语的准确翻译
|
||||
2. 上下文理解:根据项目管理场景选择最合适的表达方式
|
||||
3. 格式保持:严格保持原文的格式、结构、标点符号和排版
|
||||
4. 语言规范:使用目标语言的标准表达,符合该语言的语法和习惯
|
||||
5. 专业性:体现项目管理领域的专业水准和准确性
|
||||
6. 简洁性:避免冗余表达,保持语言简洁明了
|
||||
|
||||
注意事项:
|
||||
- 保留所有HTML标签、特殊符号、数字、日期格式
|
||||
- 对于专有名词(如软件名称、品牌名)保持原文
|
||||
- 确保翻译后的文本自然流畅,符合目标语言的表达习惯
|
||||
- 如遇到歧义表达,优先选择项目管理场景下的含义
|
||||
|
||||
请直接返回翻译结果,不要包含任何解释或标记。
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => "请将以下内容翻译为 {$targetLanguage}:\n\n{$text}"
|
||||
]
|
||||
],
|
||||
]);
|
||||
|
||||
$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-5-nano",
|
||||
"reasoning_effort" => "minimal",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一个专业的标题生成器,专门为项目任务管理系统的对话内容生成精准、简洁的标题。
|
||||
|
||||
要求:
|
||||
1. 标题要准确概括文本的核心内容和主要意图
|
||||
2. 标题长度控制在5-20个字符之间
|
||||
3. 语言简洁明了,避免冗余词汇
|
||||
4. 适合在项目管理场景中使用
|
||||
5. 不要包含引号或特殊符号
|
||||
6. 如果是技术讨论,突出技术要点
|
||||
7. 如果是项目管理内容,突出关键动作或目标
|
||||
8. 如果是需求讨论,突出需求的核心点
|
||||
|
||||
请直接返回标题,不要包含任何解释或其他内容。
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => "请为以下内容生成一个合适的标题:\n\n" . $text
|
||||
]
|
||||
],
|
||||
]);
|
||||
|
||||
$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 string $text 用户提供的提示词
|
||||
* @param array $context 上下文信息
|
||||
* @return array
|
||||
*/
|
||||
public static function generateTask($text, $context = [])
|
||||
{
|
||||
// 构建上下文提示信息
|
||||
$contextPrompt = self::buildTaskContextPrompt($context);
|
||||
|
||||
$post = json_encode([
|
||||
"model" => "gpt-5-nano",
|
||||
"reasoning_effort" => "minimal",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一个专业的任务管理专家,擅长将想法和需求转化为清晰、可执行的项目任务。
|
||||
|
||||
任务生成要求:
|
||||
1. 根据输入内容分析并生成合适的任务标题和详细描述
|
||||
2. 标题要简洁明了,准确概括任务核心目标,长度控制在8-30个字符
|
||||
3. 描述需覆盖任务背景、具体要求、交付标准、风险提示等关键信息
|
||||
4. 描述内容使用Markdown格式,合理组织标题、列表、加粗等结构
|
||||
5. 内容需适配项目管理系统,表述专业、逻辑清晰,并与用户输入语言保持一致
|
||||
6. 优先遵循用户在输入中给出的风格、长度或复杂度要求;默认情况下将详细描述控制在120-200字内,如用户要求简单或简短,则控制在80-120字内
|
||||
7. 当任务具有多个执行步骤、阶段或协作角色时,请拆解出 2-6 个关键子任务;如无必要,可返回空数组
|
||||
8. 子任务应聚焦单一可执行动作,名称控制在8-30个字符内,避免重复和含糊表述
|
||||
|
||||
返回格式要求:
|
||||
必须严格按照以下 JSON 结构返回,禁止输出额外文字或 Markdown 代码块标记;即使某项为空,也保留对应字段:
|
||||
{
|
||||
"title": "任务标题",
|
||||
"content": "任务的详细描述内容,使用Markdown格式,根据实际情况组织结构",
|
||||
"subtasks": [
|
||||
"子任务名称1",
|
||||
"子任务名称2"
|
||||
]
|
||||
}
|
||||
|
||||
内容格式建议(非强制):
|
||||
- 可以使用标题、列表、加粗等Markdown格式
|
||||
- 可以包含任务背景、具体要求、验收标准等部分
|
||||
- 根据任务性质灵活组织内容结构
|
||||
- 仅在确有必要时生成子任务,并确保每个子任务都是独立、可执行、便于追踪的动作
|
||||
- 若用户明确要求简洁或简单,保持描述紧凑,避免添加冗余段落或重复信息
|
||||
|
||||
上下文信息处理指南:
|
||||
- 如果已有标题和内容,优先考虑优化改进而非完全重写
|
||||
- 如果使用了任务模板,严格按照模板的结构和格式要求生成
|
||||
- 如果已设置负责人或时间计划,在任务描述中体现相关要求
|
||||
- 根据优先级等级调整任务的紧急程度和详细程度
|
||||
|
||||
注意事项:
|
||||
- 标题要体现任务的核心动作和目标
|
||||
- 描述要包含足够的细节让执行者理解任务
|
||||
- 如果涉及技术开发,要明确技术要求和实现方案
|
||||
- 如果涉及设计,要说明设计要求和期望效果
|
||||
- 如果涉及测试,要明确测试范围和验收标准
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => $contextPrompt . "\n\n请根据以上上下文并结合以下提示词生成一个完整的项目任务(包含标题和详细描述):\n\n" . $text
|
||||
]
|
||||
],
|
||||
]);
|
||||
|
||||
$ai = new self($post);
|
||||
$ai->setTimeout(60);
|
||||
|
||||
$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['title']) || !isset($parsedData['content'])) {
|
||||
return Base::retError("任务生成格式错误", $content);
|
||||
}
|
||||
|
||||
$title = trim($parsedData['title']);
|
||||
$markdownContent = trim($parsedData['content']);
|
||||
$rawSubtasks = $parsedData['subtasks'] ?? [];
|
||||
|
||||
if (empty($title) || empty($markdownContent)) {
|
||||
return Base::retError("生成的任务标题或内容为空", $parsedData);
|
||||
}
|
||||
|
||||
$subtasks = [];
|
||||
if (is_array($rawSubtasks)) {
|
||||
foreach ($rawSubtasks as $raw) {
|
||||
if (is_array($raw)) {
|
||||
$name = trim($raw['title'] ?? $raw['name'] ?? '');
|
||||
} else {
|
||||
$name = trim($raw);
|
||||
}
|
||||
|
||||
if (!empty($name)) {
|
||||
$subtasks[] = $name;
|
||||
}
|
||||
|
||||
if (count($subtasks) >= 8) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", [
|
||||
'title' => $title,
|
||||
'content' => Base::markdown2html($markdownContent), // 将 Markdown 转换为 HTML
|
||||
'subtasks' => $subtasks
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 openAI 生成项目名称与任务列表
|
||||
* @param string $text 用户提供的提示词
|
||||
* @param array $context 上下文信息
|
||||
* @return array
|
||||
*/
|
||||
public static function generateProject($text, $context = [])
|
||||
{
|
||||
$text = trim((string)$text);
|
||||
if ($text === '') {
|
||||
return Base::retError("项目提示词不能为空");
|
||||
}
|
||||
|
||||
$context['current_name'] = trim($context['current_name'] ?? '');
|
||||
$context['current_columns'] = self::normalizeProjectColumns($context['current_columns'] ?? []);
|
||||
|
||||
if (!empty($context['template_examples']) && is_array($context['template_examples'])) {
|
||||
$examples = [];
|
||||
foreach ($context['template_examples'] as $item) {
|
||||
$name = trim($item['name'] ?? '');
|
||||
$columns = self::normalizeProjectColumns($item['columns'] ?? []);
|
||||
if (empty($columns)) {
|
||||
continue;
|
||||
}
|
||||
$examples[] = [
|
||||
'name' => $name,
|
||||
'columns' => $columns,
|
||||
];
|
||||
if (count($examples) >= 6) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$context['template_examples'] = $examples;
|
||||
} else {
|
||||
$context['template_examples'] = [];
|
||||
}
|
||||
|
||||
$contextPrompt = self::buildProjectContextPrompt($context);
|
||||
|
||||
$post = json_encode([
|
||||
"model" => "gpt-5-nano",
|
||||
"reasoning_effort" => "minimal",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一名资深的项目规划顾问,帮助团队快速搭建符合需求的项目。
|
||||
|
||||
生成要求:
|
||||
1. 产出一个简洁、有辨识度的项目名称(不超过18个汉字或36个字符)
|
||||
2. 给出 3 - 8 个项目任务列表,用于看板列或阶段分组
|
||||
3. 任务列表名称保持 4 - 12 个字符,聚焦阶段或责任划分,避免冗长描述
|
||||
4. 结合用户描述的业务特征,必要时可包含里程碑或交付节点
|
||||
5. 尽量参考上下文提供的现有内容或模板,不要与之完全重复
|
||||
|
||||
输出格式:
|
||||
必须严格返回 JSON,禁止携带额外说明或 Markdown 代码块,结构如下:
|
||||
{
|
||||
"name": "项目名称",
|
||||
"columns": ["列表1", "列表2", "列表3"]
|
||||
}
|
||||
|
||||
校验标准:
|
||||
- 列表名称应当互不重复且语义明确
|
||||
- 若上下文包含已有名称或列表,请在此基础上迭代优化
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => ($contextPrompt ? $contextPrompt . "\n\n" : "") . "请根据以上信息,并结合以下提示词生成适合的项目名称和任务列表:\n\n" . $text
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$ai = new self($post);
|
||||
$ai->setTimeout(45);
|
||||
|
||||
$res = $ai->request();
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("项目生成失败", $res);
|
||||
}
|
||||
|
||||
$content = $res['data'];
|
||||
$content = preg_replace('/^\s*```json\s*/', '', $content);
|
||||
$content = preg_replace('/\s*```\s*$/', '', $content);
|
||||
|
||||
if (empty($content)) {
|
||||
return Base::retError("项目生成结果为空");
|
||||
}
|
||||
|
||||
$parsedData = Base::json2array($content);
|
||||
if (!$parsedData || !isset($parsedData['name'])) {
|
||||
return Base::retError("项目生成格式错误", $content);
|
||||
}
|
||||
|
||||
$name = trim($parsedData['name']);
|
||||
$columns = self::normalizeProjectColumns($parsedData['columns'] ?? []);
|
||||
|
||||
if ($name === '') {
|
||||
return Base::retError("生成的项目名称为空", $parsedData);
|
||||
}
|
||||
|
||||
if (empty($columns)) {
|
||||
$columns = $context['current_columns'];
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", [
|
||||
'name' => $name,
|
||||
'columns' => $columns,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 openAI 生成聊天消息
|
||||
* @param string $text 用户提供的提示词
|
||||
* @param array $context 上下文信息
|
||||
* @return array
|
||||
*/
|
||||
public static function generateMessage($text, $context = [])
|
||||
{
|
||||
$text = trim((string)$text);
|
||||
if ($text === '') {
|
||||
return Base::retError("消息提示词不能为空");
|
||||
}
|
||||
|
||||
$contextPrompt = self::buildMessageContextPrompt($context);
|
||||
|
||||
$post = json_encode([
|
||||
"model" => "gpt-5-nano",
|
||||
"reasoning_effort" => "minimal",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一名专业的沟通助手,协助用户编写得体、清晰且具行动指向的即时消息。
|
||||
|
||||
写作要求:
|
||||
1. 根据用户提供的需求与上下文生成完整消息,语气需符合业务沟通场景,保持真诚、礼貌且高效
|
||||
2. 默认使用简洁的短段落,可使用 Markdown 基础格式(加粗、列表、引用)增强结构,但不要输出代码块或 JSON
|
||||
3. 如果上下文包含引用信息或草稿,请在消息中自然呼应相关要点
|
||||
4. 如无特别说明,将消息长度控制在 60-180 字;若需更短或更长,遵循用户描述
|
||||
5. 如需提出行动或问题,请明确表达,避免含糊
|
||||
|
||||
输出规范:
|
||||
- 仅返回可直接发送的消息内容
|
||||
- 禁止在内容前后添加额外说明、标签或引导语
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => ($contextPrompt ? $contextPrompt . "\n\n" : "") . "请根据以上信息,并结合以下提示词生成一条待发送的消息:\n\n" . $text
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$ai = new self($post);
|
||||
$ai->setTimeout(45);
|
||||
|
||||
$res = $ai->request();
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("消息生成失败", $res);
|
||||
}
|
||||
|
||||
$content = trim($res['data']);
|
||||
$content = preg_replace('/^\s*```(?:markdown|md|text)?\s*/i', '', $content);
|
||||
$content = preg_replace('/\s*```\s*$/', '', $content);
|
||||
$content = trim($content);
|
||||
|
||||
if ($content === '') {
|
||||
return Base::retError("消息生成结果为空");
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", [
|
||||
'text' => $content,
|
||||
'html' => Base::markdown2html($content),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建任务生成的上下文提示信息
|
||||
* @param array $context 上下文信息
|
||||
* @return string
|
||||
*/
|
||||
private static function buildTaskContextPrompt($context)
|
||||
{
|
||||
$prompts = [];
|
||||
|
||||
// 当前任务信息
|
||||
if (!empty($context['current_title']) || !empty($context['current_content'])) {
|
||||
$prompts[] = "## 当前任务信息";
|
||||
if (!empty($context['current_title'])) {
|
||||
$prompts[] = "当前标题:" . $context['current_title'];
|
||||
}
|
||||
if (!empty($context['current_content'])) {
|
||||
$prompts[] = "当前内容:" . $context['current_content'];
|
||||
}
|
||||
$prompts[] = "请在此基础上优化改进,而不是完全重写。";
|
||||
}
|
||||
|
||||
// 任务模板信息
|
||||
if (!empty($context['template_name']) || !empty($context['template_content'])) {
|
||||
$prompts[] = "## 任务模板要求";
|
||||
if (!empty($context['template_name'])) {
|
||||
$prompts[] = "模板名称:" . $context['template_name'];
|
||||
}
|
||||
if (!empty($context['template_content'])) {
|
||||
$prompts[] = "模板内容结构:" . $context['template_content'];
|
||||
}
|
||||
$prompts[] = "请严格按照此模板的结构和格式要求生成内容。";
|
||||
}
|
||||
|
||||
// 项目状态信息
|
||||
$statusInfo = [];
|
||||
if (!empty($context['has_owner'])) {
|
||||
$statusInfo[] = "已设置负责人";
|
||||
}
|
||||
if (!empty($context['has_time_plan'])) {
|
||||
$statusInfo[] = "已设置计划时间";
|
||||
}
|
||||
if (!empty($context['priority_level'])) {
|
||||
$statusInfo[] = "优先级:" . $context['priority_level'];
|
||||
}
|
||||
|
||||
if (!empty($statusInfo)) {
|
||||
$prompts[] = "## 任务状态";
|
||||
$prompts[] = implode(",", $statusInfo);
|
||||
$prompts[] = "请在任务描述中体现相应的要求和约束。";
|
||||
}
|
||||
|
||||
return empty($prompts) ? "" : implode("\n", $prompts);
|
||||
}
|
||||
|
||||
private static function buildProjectContextPrompt($context)
|
||||
{
|
||||
$prompts = [];
|
||||
|
||||
if (!empty($context['current_name']) || !empty($context['current_columns'])) {
|
||||
$prompts[] = "## 当前项目草稿";
|
||||
if (!empty($context['current_name'])) {
|
||||
$prompts[] = "已有名称:" . $context['current_name'];
|
||||
}
|
||||
if (!empty($context['current_columns'])) {
|
||||
$prompts[] = "现有任务列表:" . implode("、", $context['current_columns']);
|
||||
}
|
||||
$prompts[] = "请在此基础上进行优化和补充。";
|
||||
}
|
||||
|
||||
if (!empty($context['template_examples'])) {
|
||||
$prompts[] = "## 常用模板示例";
|
||||
foreach ($context['template_examples'] as $example) {
|
||||
$line = '';
|
||||
if (!empty($example['name'])) {
|
||||
$line .= $example['name'] . ":";
|
||||
}
|
||||
$line .= implode("、", $example['columns']);
|
||||
$prompts[] = "- " . $line;
|
||||
}
|
||||
$prompts[] = "可以借鉴以上结构,但要结合用户需求生成更贴合的方案。";
|
||||
}
|
||||
|
||||
return empty($prompts) ? "" : implode("\n", $prompts);
|
||||
}
|
||||
|
||||
private static function buildMessageContextPrompt($context)
|
||||
{
|
||||
$prompts = [];
|
||||
|
||||
if (!empty($context['dialog_name']) || !empty($context['dialog_type']) || !empty($context['group_type'])) {
|
||||
$prompts[] = "## 会话信息";
|
||||
if (!empty($context['dialog_name'])) {
|
||||
$prompts[] = "名称:" . Base::cutStr($context['dialog_name'], 60);
|
||||
}
|
||||
if (!empty($context['dialog_type'])) {
|
||||
$typeMap = ['group' => '群聊', 'user' => '单聊'];
|
||||
$prompts[] = "类型:" . ($typeMap[$context['dialog_type']] ?? $context['dialog_type']);
|
||||
}
|
||||
if (!empty($context['group_type'])) {
|
||||
$prompts[] = "分类:" . Base::cutStr($context['group_type'], 60);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($context['members']) && is_array($context['members'])) {
|
||||
$members = array_slice(array_filter($context['members']), 0, 10);
|
||||
if (!empty($members)) {
|
||||
$prompts[] = "## 会话成员";
|
||||
$prompts[] = implode(",", array_map(fn($name) => Base::cutStr($name, 30), $members));
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($context['recent_messages']) && is_array($context['recent_messages'])) {
|
||||
$prompts[] = "## 最近消息";
|
||||
foreach ($context['recent_messages'] as $item) {
|
||||
$sender = Base::cutStr(trim($item['sender'] ?? ''), 40) ?: '成员';
|
||||
$summary = Base::cutStr(trim($item['summary'] ?? ''), 120);
|
||||
if ($summary !== '') {
|
||||
$prompts[] = "- {$sender}:{$summary}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($context['quote_summary'])) {
|
||||
$prompts[] = "## 引用消息";
|
||||
$quoteUser = Base::cutStr(trim($context['quote_user'] ?? ''), 40);
|
||||
$quoteText = Base::cutStr(trim($context['quote_summary']), 200);
|
||||
if ($quoteUser !== '') {
|
||||
$prompts[] = "{$quoteUser}:{$quoteText}";
|
||||
} else {
|
||||
$prompts[] = $quoteText;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($context['current_draft'])) {
|
||||
$prompts[] = "## 当前草稿";
|
||||
$prompts[] = Base::cutStr(trim($context['current_draft']), 200);
|
||||
}
|
||||
|
||||
return empty($prompts) ? "" : implode("\n", $prompts);
|
||||
}
|
||||
|
||||
private static function normalizeProjectColumns($columns)
|
||||
{
|
||||
if (is_string($columns)) {
|
||||
$columns = preg_split('/[\n\r,,;;|]/u', $columns);
|
||||
}
|
||||
|
||||
$normalized = [];
|
||||
if (is_array($columns)) {
|
||||
foreach ($columns as $item) {
|
||||
if (is_array($item)) {
|
||||
$item = $item['name'] ?? $item['title'] ?? reset($item);
|
||||
}
|
||||
$item = trim((string)$item);
|
||||
if ($item === '') {
|
||||
continue;
|
||||
}
|
||||
$item = mb_substr($item, 0, 30);
|
||||
if (!in_array($item, $normalized)) {
|
||||
$normalized[] = $item;
|
||||
}
|
||||
if (count($normalized) >= 8) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 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-5-nano",
|
||||
"reasoning_effort" => "minimal",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => <<<EOF
|
||||
你是一个专业的内容生成器。
|
||||
|
||||
要求:
|
||||
1. 笑话要幽默风趣,适合职场环境,内容积极正面
|
||||
2. 心灵鸡汤要励志温暖,适合职场人士阅读
|
||||
3. 每个笑话和鸡汤都要简洁明了,尽量不超过100字
|
||||
4. 必须严格按照以下JSON格式返回,不要markdown格式,不要包含任何其他内容:
|
||||
|
||||
{
|
||||
"jokes": [
|
||||
"笑话内容1",
|
||||
"笑话内容2",
|
||||
...
|
||||
],
|
||||
"soups": [
|
||||
"心灵鸡汤内容1",
|
||||
"心灵鸡汤内容2",
|
||||
...
|
||||
]
|
||||
}
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => "请生成20个职场笑话和20个心灵鸡汤"
|
||||
]
|
||||
],
|
||||
]);
|
||||
|
||||
$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']
|
||||
]);
|
||||
}
|
||||
}
|
||||
193
app/Module/AgoraIO/AccessToken.php
Normal file
193
app/Module/AgoraIO/AccessToken.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
/**
|
||||
* @Description :
|
||||
*
|
||||
* @Date : 2019-03-14 13:22
|
||||
* @Author : hmy940118@gmail.com
|
||||
*/
|
||||
|
||||
namespace App\Module\AgoraIO;
|
||||
|
||||
class AccessToken
|
||||
{
|
||||
|
||||
const Privileges = array(
|
||||
"kJoinChannel" => 1,
|
||||
"kPublishAudioStream" => 2,
|
||||
"kPublishVideoStream" => 3,
|
||||
"kPublishDataStream" => 4,
|
||||
"kPublishAudioCdn" => 5,
|
||||
"kPublishVideoCdn" => 6,
|
||||
"kRequestPublishAudioStream" => 7,
|
||||
"kRequestPublishVideoStream" => 8,
|
||||
"kRequestPublishDataStream" => 9,
|
||||
"kInvitePublishAudioStream" => 10,
|
||||
"kInvitePublishVideoStream" => 11,
|
||||
"kInvitePublishDataStream" => 12,
|
||||
"kAdministrateChannel" => 101
|
||||
);
|
||||
|
||||
public $appID, $appCertificate, $channelName, $uid;
|
||||
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* AccessToken constructor.
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->message = new Message();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $uid
|
||||
*/
|
||||
public function setUid($uid)
|
||||
{
|
||||
if ($uid === 0) {
|
||||
$this->uid = "";
|
||||
} else {
|
||||
$this->uid = $uid . '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @param $str
|
||||
* @return bool
|
||||
*/
|
||||
public function is_nonempty_string($name, $str)
|
||||
{
|
||||
if (is_string($str) && $str !== "") {
|
||||
return true;
|
||||
}
|
||||
echo $name . " check failed, should be a non-empty string";
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $appID
|
||||
* @param $appCertificate
|
||||
* @param $channelName
|
||||
* @param $uid
|
||||
* @return AccessToken|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function init($appID, $appCertificate, $channelName, $uid)
|
||||
{
|
||||
$accessToken = new AccessToken();
|
||||
if (!$accessToken->is_nonempty_string("appID", $appID) ||
|
||||
!$accessToken->is_nonempty_string("appCertificate", $appCertificate) ||
|
||||
!$accessToken->is_nonempty_string("channelName", $channelName)) {
|
||||
return null;
|
||||
}
|
||||
$accessToken->appID = $appID;
|
||||
$accessToken->appCertificate = $appCertificate;
|
||||
$accessToken->channelName = $channelName;
|
||||
$accessToken->setUid($uid);
|
||||
$accessToken->message = new Message();
|
||||
return $accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $token
|
||||
* @param $appCertificate
|
||||
* @param $channel
|
||||
* @param $uid
|
||||
* @return AccessToken|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function initWithToken($token, $appCertificate, $channel, $uid)
|
||||
{
|
||||
$accessToken = new AccessToken();
|
||||
if (!$accessToken->extract($token, $appCertificate, $channel, $uid)) {
|
||||
return null;
|
||||
}
|
||||
return $accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $key
|
||||
* @param $expireTimestamp
|
||||
* @return $this
|
||||
*/
|
||||
public function addPrivilege($key, $expireTimestamp)
|
||||
{
|
||||
$this->message->privileges[$key] = $expireTimestamp;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $token
|
||||
* @param $appCertificate
|
||||
* @param $channelName
|
||||
* @param $uid
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function extract($token, $appCertificate, $channelName, $uid)
|
||||
{
|
||||
$ver_len = 3;
|
||||
$appid_len = 32;
|
||||
$version = substr($token, 0, $ver_len);
|
||||
if ($version !== "006") {
|
||||
echo 'invalid version ' . $version;
|
||||
return false;
|
||||
}
|
||||
if (!$this->is_nonempty_string("token", $token) ||
|
||||
!$this->is_nonempty_string("appCertificate", $appCertificate) ||
|
||||
!$this->is_nonempty_string("channelName", $channelName)) {
|
||||
return false;
|
||||
}
|
||||
$appid = substr($token, $ver_len, $appid_len);
|
||||
$content = (base64_decode(substr($token, $ver_len + $appid_len, strlen($token) - ($ver_len + $appid_len))));
|
||||
$pos = 0;
|
||||
$len = unpack("v", $content . substr($pos, 2))[1];
|
||||
$pos += 2;
|
||||
$sig = substr($content, $pos, $len);
|
||||
$pos += $len;
|
||||
$crc_channel = unpack("V", substr($content, $pos, 4))[1];
|
||||
$pos += 4;
|
||||
$crc_uid = unpack("V", substr($content, $pos, 4))[1];
|
||||
$pos += 4;
|
||||
$msgLen = unpack("v", substr($content, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$msg = substr($content, $pos, $msgLen);
|
||||
$this->appID = $appid;
|
||||
$message = new Message();
|
||||
$message->unpackContent($msg);
|
||||
$this->message = $message;
|
||||
//non reversable values
|
||||
$this->appCertificate = $appCertificate;
|
||||
$this->channelName = $channelName;
|
||||
$this->setUid($uid);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$msg = $this->message->packContent();
|
||||
$val = array_merge(unpack("C*", $this->appID), unpack("C*", $this->channelName), unpack("C*", $this->uid), $msg);
|
||||
|
||||
$sig = hash_hmac('sha256', implode(array_map("chr", $val)), $this->appCertificate, true);
|
||||
$crc_channel_name = crc32($this->channelName) & 0xffffffff;
|
||||
$crc_uid = crc32($this->uid) & 0xffffffff;
|
||||
$content = array_merge(unpack("C*", $this->packString($sig)), unpack("C*", pack("V", $crc_channel_name)), unpack("C*", pack("V", $crc_uid)), unpack("C*", pack("v", count($msg))), $msg);
|
||||
$version = "006";
|
||||
$ret = $version . $this->appID . base64_encode(implode(array_map("chr", $content)));
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @return string
|
||||
*/
|
||||
public function packString($value)
|
||||
{
|
||||
return pack("v", strlen($value)) . $value;
|
||||
}
|
||||
}
|
||||
122
app/Module/AgoraIO/AgoraTokenGenerator.php
Normal file
122
app/Module/AgoraIO/AgoraTokenGenerator.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/**
|
||||
* @Description :
|
||||
*
|
||||
* @Date : 2019-03-14 13:20
|
||||
* @Author : hmy940118@gmail.com
|
||||
*/
|
||||
|
||||
namespace App\Module\AgoraIO;
|
||||
|
||||
|
||||
class AgoraTokenGenerator
|
||||
{
|
||||
const AttendeePrivileges = array(
|
||||
AccessToken::Privileges["kJoinChannel"] => 0,
|
||||
AccessToken::Privileges["kPublishAudioStream"] => 0,
|
||||
AccessToken::Privileges["kPublishVideoStream"] => 0,
|
||||
AccessToken::Privileges["kPublishDataStream"] => 0
|
||||
);
|
||||
|
||||
|
||||
const PublisherPrivileges = array(
|
||||
AccessToken::Privileges["kJoinChannel"] => 0,
|
||||
AccessToken::Privileges["kPublishAudioStream"] => 0,
|
||||
AccessToken::Privileges["kPublishVideoStream"] => 0,
|
||||
AccessToken::Privileges["kPublishDataStream"] => 0,
|
||||
AccessToken::Privileges["kPublishAudioCdn"] => 0,
|
||||
AccessToken::Privileges["kPublishVideoCdn"] => 0,
|
||||
AccessToken::Privileges["kInvitePublishAudioStream"] => 0,
|
||||
AccessToken::Privileges["kInvitePublishVideoStream"] => 0,
|
||||
AccessToken::Privileges["kInvitePublishDataStream"] => 0
|
||||
);
|
||||
|
||||
const SubscriberPrivileges = array(
|
||||
AccessToken::Privileges["kJoinChannel"] => 0,
|
||||
AccessToken::Privileges["kRequestPublishAudioStream"] => 0,
|
||||
AccessToken::Privileges["kRequestPublishVideoStream"] => 0,
|
||||
AccessToken::Privileges["kRequestPublishDataStream"] => 0
|
||||
);
|
||||
|
||||
const AdminPrivileges = array(
|
||||
AccessToken::Privileges["kJoinChannel"] => 0,
|
||||
AccessToken::Privileges["kPublishAudioStream"] => 0,
|
||||
AccessToken::Privileges["kPublishVideoStream"] => 0,
|
||||
AccessToken::Privileges["kPublishDataStream"] => 0,
|
||||
AccessToken::Privileges["kAdministrateChannel"] => 0
|
||||
);
|
||||
const Role = array(
|
||||
"kRoleAttendee" => 0, // for communication
|
||||
"kRolePublisher" => 1, // for live broadcast
|
||||
"kRoleSubscriber" => 2, // for live broadcast
|
||||
"kRoleAdmin" => 101
|
||||
);
|
||||
|
||||
const RolePrivileges = array(
|
||||
self::Role["kRoleAttendee"] => self::AttendeePrivileges,
|
||||
self::Role["kRolePublisher"] => self::PublisherPrivileges,
|
||||
self::Role["kRoleSubscriber"] => self::SubscriberPrivileges,
|
||||
self::Role["kRoleAdmin"] => self::AdminPrivileges
|
||||
);
|
||||
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* AgoraTokenGenerator constructor.
|
||||
* @param $appID
|
||||
* @param $appCertificate
|
||||
* @param $channelName
|
||||
* @param $uid
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct($appID, $appCertificate, $channelName, $uid){
|
||||
$this->token = new AccessToken();
|
||||
$this->token->appID = $appID;
|
||||
$this->token->appCertificate = $appCertificate;
|
||||
$this->token->channelName = $channelName;
|
||||
$this->token->setUid($uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $token
|
||||
* @param $appCertificate
|
||||
* @param $channel
|
||||
* @param $uid
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function initWithToken($token, $appCertificate, $channel, $uid){
|
||||
$this->token = AccessToken::initWithToken($token, $appCertificate, $channel, $uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $role
|
||||
*/
|
||||
public function initPrivilege($role){
|
||||
$p = self::RolePrivileges[$role];
|
||||
foreach($p as $key => $value){
|
||||
$this->setPrivilege($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $privilege
|
||||
* @param $expireTimestamp
|
||||
*/
|
||||
public function setPrivilege($privilege, $expireTimestamp){
|
||||
$this->token->addPrivilege($privilege, $expireTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $privilege
|
||||
*/
|
||||
public function removePrivilege($privilege){
|
||||
unset($this->token->message->privileges[$privilege]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function buildToken(){
|
||||
return $this->token->build();
|
||||
}
|
||||
}
|
||||
70
app/Module/AgoraIO/Message.php
Normal file
70
app/Module/AgoraIO/Message.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/**
|
||||
* @Description :
|
||||
*
|
||||
* @Date : 2019-03-14 13:27
|
||||
* @Author : hmy940118@gmail.com
|
||||
*/
|
||||
|
||||
namespace App\Module\AgoraIO;
|
||||
|
||||
class Message
|
||||
{
|
||||
public $salt;
|
||||
|
||||
public $ts;
|
||||
|
||||
public $privileges;
|
||||
|
||||
/**
|
||||
* Message constructor.
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->salt = rand(0, 100000);
|
||||
$date = new \DateTime("now", new \DateTimeZone('UTC'));
|
||||
$this->ts = $date->getTimestamp() + 168 * 3600; // 7天时间
|
||||
$this->privileges = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function packContent()
|
||||
{
|
||||
$buffer = unpack("C*", pack("V", $this->salt));
|
||||
$buffer = array_merge($buffer, unpack("C*", pack("V", $this->ts)));
|
||||
$buffer = array_merge($buffer, unpack("C*", pack("v", sizeof($this->privileges))));
|
||||
foreach ($this->privileges as $key => $value) {
|
||||
$buffer = array_merge($buffer, unpack("C*", pack("v", $key)));
|
||||
$buffer = array_merge($buffer, unpack("C*", pack("V", $value)));
|
||||
}
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $msg
|
||||
*/
|
||||
public function unpackContent($msg)
|
||||
{
|
||||
$pos = 0;
|
||||
$salt = unpack("V", substr($msg, $pos, 4))[1];
|
||||
$pos += 4;
|
||||
$ts = unpack("V", substr($msg, $pos, 4))[1];
|
||||
$pos += 4;
|
||||
$size = unpack("v", substr($msg, $pos, 2))[1];
|
||||
$pos += 2;
|
||||
$privileges = array();
|
||||
for ($i = 0; $i < $size; $i++) {
|
||||
$key = unpack("v", substr($msg, $pos, 2));
|
||||
$pos += 2;
|
||||
$value = unpack("V", substr($msg, $pos, 4));
|
||||
$pos += 4;
|
||||
$privileges[$key[1]] = $value[1];
|
||||
}
|
||||
$this->salt = $salt;
|
||||
$this->ts = $ts;
|
||||
$this->privileges = $privileges;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2161
app/Module/Base.php
2161
app/Module/Base.php
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user