mirror of
https://github.com/kuaifan/dootask.git
synced 2026-06-16 12:22:38 +00:00
Compare commits
2297 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20c3fa91fb | ||
|
|
c03867304e | ||
|
|
b595120d62 | ||
|
|
8e66f0bfb3 | ||
|
|
e9ea1adc5d | ||
|
|
2eee171a50 | ||
|
|
fd6a8a3650 | ||
|
|
84a90b7760 | ||
|
|
7335c59b68 | ||
|
|
035c9d9d3d | ||
|
|
36da18af79 | ||
|
|
363badbc97 | ||
|
|
9be6265220 | ||
|
|
be53e6c6ac | ||
|
|
4eab130313 | ||
|
|
c706c515ee | ||
|
|
8a576595ce | ||
|
|
8c809bbff1 | ||
|
|
08ed396444 | ||
|
|
f5eb84589f | ||
|
|
daca384822 | ||
|
|
0a6e944a9a | ||
|
|
e0d1b08e89 | ||
|
|
6b54b7b1c5 | ||
|
|
adc7fb0d07 | ||
|
|
f969c8145c | ||
|
|
20b5daba50 | ||
|
|
aa2e0acaba | ||
|
|
e57736bcc1 | ||
|
|
a8db8dde7b | ||
|
|
635f6e5d5a | ||
|
|
4875574c6e | ||
|
|
b1d5652bc7 | ||
|
|
025f45df0a | ||
|
|
981a5c9f0f | ||
|
|
88cfd40abe | ||
|
|
cdcf0ff5f3 | ||
|
|
42e355149c | ||
|
|
518364d70d | ||
|
|
f25340c0b3 | ||
|
|
24f607f442 | ||
|
|
6fbddbe77c | ||
|
|
21ba2665b9 | ||
|
|
0888f599a4 | ||
|
|
ef7293704b | ||
|
|
8cd4669b90 | ||
|
|
7f7a82b4b8 | ||
|
|
0863e5529a | ||
|
|
e0ad8ce6c1 | ||
|
|
9f4e5a8335 | ||
|
|
587db459bf | ||
|
|
5b87714acf | ||
|
|
bc54ac9462 | ||
|
|
7e5b31cfb2 | ||
|
|
d81b4ed273 | ||
|
|
0c1a913134 | ||
|
|
7dc641e69e | ||
|
|
18336c870e | ||
|
|
e43588c3b2 | ||
|
|
64649b514e | ||
|
|
24710289e1 | ||
|
|
2a3f05e06f | ||
|
|
0d31106b0f | ||
|
|
fbd1c829a1 | ||
|
|
82d2ca6360 | ||
|
|
717e520556 | ||
|
|
c8ddb511cf | ||
|
|
caf728de8d | ||
|
|
a7cd4d7fa8 | ||
|
|
ddc0046e24 | ||
|
|
1059630b9d | ||
|
|
e1c1fc030f | ||
|
|
09edb14d56 | ||
|
|
f27cef2d66 | ||
|
|
07a2e6df29 | ||
|
|
f521f0df65 | ||
|
|
a67fcd6f02 | ||
|
|
d17f404853 | ||
|
|
8def4addc4 | ||
|
|
0ecaf9740f | ||
|
|
bc75680ee9 | ||
|
|
6a71964592 | ||
|
|
00a2ea3d2f | ||
|
|
95e97333b4 | ||
|
|
9e65500748 | ||
|
|
a2acd6f6e4 | ||
|
|
ee96730268 | ||
|
|
f925f238dd | ||
|
|
39c6ca3e8c | ||
|
|
c798faa8db | ||
|
|
ed2f843815 | ||
|
|
984b98e4fc | ||
|
|
4b32472d64 | ||
|
|
fc171bc71f | ||
|
|
cc80fa83e0 | ||
|
|
782ba4a151 | ||
|
|
04708cedb6 | ||
|
|
4068966700 | ||
|
|
3ce8cf381a | ||
|
|
f78d3f3aff | ||
|
|
c60dff0950 | ||
|
|
f2d49ee104 | ||
|
|
a248d81230 | ||
|
|
1ac6bad2bb | ||
|
|
37de721df9 | ||
|
|
773eead827 | ||
|
|
c4dd04ccb6 | ||
|
|
2cdde37069 | ||
|
|
f68f759418 | ||
|
|
801d0b24ab | ||
|
|
29be29b9cf | ||
|
|
c253044f61 | ||
|
|
9acf7d2046 | ||
|
|
3911af7b51 | ||
|
|
6b722b7ed7 | ||
|
|
6a00b87f72 | ||
|
|
0a97039d75 | ||
|
|
cb56a01622 | ||
|
|
452af4bd2f | ||
|
|
75073d4320 | ||
|
|
d4d7a0d69f | ||
|
|
165ad03024 | ||
|
|
3603cf9889 | ||
|
|
027662ebab | ||
|
|
106465b932 | ||
|
|
eef4c6fbe5 | ||
|
|
916ae97ca7 | ||
|
|
841405505d | ||
|
|
22a653bb0f | ||
|
|
3482e4b1a8 | ||
|
|
9097369b0c | ||
|
|
95c6b53f10 | ||
|
|
f7d5040b02 | ||
|
|
26b7f83d35 | ||
|
|
07b99c6e75 | ||
|
|
cb5e7e2cc7 | ||
|
|
2180998e81 | ||
|
|
478876ddc1 | ||
|
|
ae021fd148 | ||
|
|
f36317b081 | ||
|
|
066a5a619c | ||
|
|
654793156d | ||
|
|
ba65378c6b | ||
|
|
cb6c50b071 | ||
|
|
2cb67fafe7 | ||
|
|
8eaba6f364 | ||
|
|
c4f0fb5a3d | ||
|
|
59ad79fa58 | ||
|
|
c65f0276bd | ||
|
|
f8b335a003 | ||
|
|
0ac4b546ba | ||
|
|
07a41ca0ac | ||
|
|
347465fc4d | ||
|
|
acb9cd317c | ||
|
|
b7213f8c47 | ||
|
|
a3caf5ebdf | ||
|
|
87dd07ef23 | ||
|
|
0cefb7eaff | ||
|
|
ff87de9f44 | ||
|
|
22de7de87c | ||
|
|
53dd9dca0f | ||
|
|
12d6bbea19 | ||
|
|
23b06327d6 | ||
|
|
6c22e373f7 | ||
|
|
4ebbb387ee | ||
|
|
9234fe3ed1 | ||
|
|
70be6619e9 | ||
|
|
c8c27e808f | ||
|
|
9cb8c92492 | ||
|
|
f4f9ee1d3d | ||
|
|
138336711f | ||
|
|
2163bb0bff | ||
|
|
bc460f0da8 | ||
|
|
ad66811f49 | ||
|
|
70ad8c394a | ||
|
|
32ffecb905 | ||
|
|
b794ba7a6b | ||
|
|
07360a8d2c | ||
|
|
fb7731ddcd | ||
|
|
13a25e3011 | ||
|
|
055cf53738 | ||
|
|
cb414b48f6 | ||
|
|
1c27719ac4 | ||
|
|
ec33327408 | ||
|
|
c2c27a684b | ||
|
|
224703a6d0 | ||
|
|
dd20711c04 | ||
|
|
3a2b7b1400 | ||
|
|
792989a504 | ||
|
|
c0183e62fb | ||
|
|
ce5bb5f187 | ||
|
|
a34b0c88d5 | ||
|
|
9c7ec58bb6 | ||
|
|
067a736b57 | ||
|
|
f8f08c9d0d | ||
|
|
4f2d382fd6 | ||
|
|
42e4ddbd17 | ||
|
|
3026cd698f | ||
|
|
47c53a18fa | ||
|
|
22926e19cd | ||
|
|
495b25e2b1 | ||
|
|
01908b7c48 | ||
|
|
b138dc580d | ||
|
|
78b14f4aad | ||
|
|
60387aa521 | ||
|
|
633826cb89 | ||
|
|
cf6d180fc5 | ||
|
|
0d85174250 | ||
|
|
925449c66a | ||
|
|
cd58b418af | ||
|
|
4cfc5e6024 | ||
|
|
7321ab06f0 | ||
|
|
790f5d4838 | ||
|
|
731dbc5507 | ||
|
|
3b1dce6d67 | ||
|
|
4929d44ce7 | ||
|
|
ce42c2a660 | ||
|
|
16d5ffd4f9 | ||
|
|
fc74e0d952 | ||
|
|
089f219280 | ||
|
|
9d62ec1ec1 | ||
|
|
5a4e51d1e0 | ||
|
|
f0982d7d9a | ||
|
|
1ac3a4cc96 | ||
|
|
7f9c42d3d8 | ||
|
|
4e99e398d6 | ||
|
|
395fc155ce | ||
|
|
6bdefc4f03 | ||
|
|
d4547cbe97 | ||
|
|
c9a0b7481a | ||
|
|
f496bc5fca | ||
|
|
4ba02b9dce | ||
|
|
f821e5ad28 | ||
|
|
425f7b6f79 | ||
|
|
61d7970b6a | ||
|
|
1aa9984535 | ||
|
|
8ab810c670 | ||
|
|
5cc3d60e15 | ||
|
|
42a2eb56c7 | ||
|
|
4b0f4e388c | ||
|
|
31045b3808 | ||
|
|
a95f22bf42 | ||
|
|
fa84f92577 | ||
|
|
90a5624877 | ||
|
|
f42250b8b7 | ||
|
|
b9809d207d | ||
|
|
0d8e10b60e | ||
|
|
501ff21e55 | ||
|
|
4759e28a56 | ||
|
|
bd7841ac05 | ||
|
|
ea0d27fdea | ||
|
|
610979f30b | ||
|
|
9a8304d595 | ||
|
|
e020a80020 | ||
|
|
7a21a2d800 | ||
|
|
ec0db3a76c | ||
|
|
67fc0781e5 | ||
|
|
79c2ba140c | ||
|
|
908171a977 | ||
|
|
a52dc14369 | ||
|
|
1e94ce501e | ||
|
|
7a5ef3a491 | ||
|
|
c08323e1ea | ||
|
|
fdf5ceeaab | ||
|
|
48ef4cfdef | ||
|
|
10c6177a9f | ||
|
|
0362c83e77 | ||
|
|
1af29837e2 | ||
|
|
986c4871df | ||
|
|
fe7a2a0e73 | ||
|
|
23faf28f7f | ||
|
|
a8d4f261a4 | ||
|
|
a336fd4a1a | ||
|
|
8759e6fd7e | ||
|
|
92d23014a7 | ||
|
|
7c3f33ea0d | ||
|
|
16a55de6f1 | ||
|
|
869ac7d316 | ||
|
|
55303689ea | ||
|
|
c69123ac92 | ||
|
|
7bce5f1c1f | ||
|
|
989660969c | ||
|
|
862acd0776 | ||
|
|
3b3ffd494f | ||
|
|
6cf8290565 | ||
|
|
230ebbcfb9 | ||
|
|
dc77f1cda1 | ||
|
|
1f791b528a | ||
|
|
1459d953ed | ||
|
|
719a36b275 | ||
|
|
0b7a3046fe | ||
|
|
203d107d68 | ||
|
|
17fd7f02a6 | ||
|
|
57ea4f2b6f | ||
|
|
df431eea46 | ||
|
|
ad9dd6330f | ||
|
|
df9d291f98 | ||
|
|
0cf7fc2ed2 | ||
|
|
e8f82baa99 | ||
|
|
353a05f344 | ||
|
|
d94ebfe04c | ||
|
|
52913abb4f | ||
|
|
d77406951d | ||
|
|
8c23192eeb | ||
|
|
078c9c198d | ||
|
|
6cfe2d226a | ||
|
|
fee1c12357 | ||
|
|
a6385b699e | ||
|
|
718ed8953f | ||
|
|
a1eea77b9e | ||
|
|
6eb08ac09b | ||
|
|
20fc2b073b | ||
|
|
8c4b9e8d12 | ||
|
|
8d187f5cfc | ||
|
|
db07a96e97 | ||
|
|
7acc9227ff | ||
|
|
c3a71e5b07 | ||
|
|
ac9e1e5e67 | ||
|
|
c668340661 | ||
|
|
ee9b6248bb | ||
|
|
01c7f7250b | ||
|
|
2abc5976f9 | ||
|
|
3e468c74e4 | ||
|
|
4ef78d2c81 | ||
|
|
4621222fa3 | ||
|
|
be860f9968 | ||
|
|
fe0b8aed20 | ||
|
|
f0e844c308 | ||
|
|
6a7cc95b23 | ||
|
|
7fd90b9ceb | ||
|
|
43577073e6 | ||
|
|
faeeb09a4a | ||
|
|
d88349b6f7 | ||
|
|
ff53e1fac3 | ||
|
|
cf4894b7c3 | ||
|
|
678dfd2d5c | ||
|
|
bf4a62ae04 | ||
|
|
7e6f3f92cf | ||
|
|
df382dafb4 | ||
|
|
10925d3a47 | ||
|
|
66252072c7 | ||
|
|
29918882bd | ||
|
|
4983fe8feb | ||
|
|
f65da118d7 | ||
|
|
a86bd9a05e | ||
|
|
f2719eb742 | ||
|
|
4f9ee1dfa9 | ||
|
|
e6ad1218bc | ||
|
|
dd2cd1df9a | ||
|
|
6dcbe8ba38 | ||
|
|
360d4dbbe2 | ||
|
|
2f32b53d19 | ||
|
|
6a3e3c3753 | ||
|
|
5ad08d8d36 | ||
|
|
b892d92614 | ||
|
|
b259f083d4 | ||
|
|
38aa9fe2fb | ||
|
|
863dd3a53e | ||
|
|
bea5058df8 | ||
|
|
31c157f58f | ||
|
|
8af6887daa | ||
|
|
eb9b7b4f86 | ||
|
|
cf78766a37 | ||
|
|
944824b552 | ||
|
|
477bb1ac8f | ||
|
|
29df864ecb | ||
|
|
bcf897b7e0 | ||
|
|
e63890c755 | ||
|
|
f3725215bd | ||
|
|
c43e305ea7 | ||
|
|
b9215e2410 | ||
|
|
19d79ab055 | ||
|
|
64d4492806 | ||
|
|
0790eae8c6 | ||
|
|
e10e2c27c1 | ||
|
|
d30b38d4b9 | ||
|
|
f6e4ed7c60 | ||
|
|
7a6bbfac75 | ||
|
|
425d6f9a06 | ||
|
|
58c760bb77 | ||
|
|
3ffdce5e7a | ||
|
|
8e518a044a | ||
|
|
a5adbf80a9 | ||
|
|
0b6c478b4f | ||
|
|
0434bde16f | ||
|
|
0deb3113b5 | ||
|
|
ecb52c76b9 | ||
|
|
69c66053b7 | ||
|
|
892ad395a7 | ||
|
|
e801c09c0f | ||
|
|
ad560a8555 | ||
|
|
e75aa5c2b9 | ||
|
|
e83fd7af1b | ||
|
|
eaec8ef994 | ||
|
|
3339e6b442 | ||
|
|
4c2425c758 | ||
|
|
80d1e6469e | ||
|
|
2fad6394ee | ||
|
|
4bfe33a37f | ||
|
|
130c8bf3b1 | ||
|
|
b9df277104 | ||
|
|
97e1f321ca | ||
|
|
4933930afd | ||
|
|
ab4640382d | ||
|
|
e4cfa4b405 | ||
|
|
789062e85e | ||
|
|
5370bee369 | ||
|
|
2f972488a1 | ||
|
|
6f7656802f | ||
|
|
7d98c5493e | ||
|
|
e0443aa336 | ||
|
|
39ff0d1516 | ||
|
|
1b9c0ee4b8 | ||
|
|
d48287f93a | ||
|
|
717e87cfa9 | ||
|
|
708b488af8 | ||
|
|
d60d3f374b | ||
|
|
8b87a2bc40 | ||
|
|
d0da517503 | ||
|
|
754036c472 | ||
|
|
720438fd91 | ||
|
|
ba76df1b00 | ||
|
|
44d85c2864 | ||
|
|
1c8b73a381 | ||
|
|
b445af932c | ||
|
|
5121739fe4 | ||
|
|
96106498d8 | ||
|
|
0116d92021 | ||
|
|
43746634a5 | ||
|
|
5183786fb0 | ||
|
|
5ba0eed721 | ||
|
|
7d08c735ef | ||
|
|
e3067b685c | ||
|
|
b219ca4c1c | ||
|
|
9e5d16ff16 | ||
|
|
da630458e1 | ||
|
|
ee2eceffb0 | ||
|
|
c8d22e7b5f | ||
|
|
342e8725bd | ||
|
|
3ced00de1f | ||
|
|
7fa075fa75 | ||
|
|
95ca496691 | ||
|
|
50b1d93f08 | ||
|
|
8958f2f234 | ||
|
|
00b4d6a748 | ||
|
|
f4de0d8276 | ||
|
|
cfa749f4f3 | ||
|
|
eeaff08673 | ||
|
|
0475e88dc2 | ||
|
|
e1f73a4639 | ||
|
|
e2296a6f64 | ||
|
|
1a6abf4e1b | ||
|
|
315851eb5f | ||
|
|
0b99b4a9a0 | ||
|
|
66002ff401 | ||
|
|
bdfc8bdd0c | ||
|
|
98e4668969 | ||
|
|
e8235dd0a2 | ||
|
|
123c74de46 | ||
|
|
c92b9bf0fb | ||
|
|
b4cbfd2ae9 | ||
|
|
dd7eee277e | ||
|
|
ab76185434 | ||
|
|
6d97bf1e88 | ||
|
|
49701fcd09 | ||
|
|
40f04d9860 | ||
|
|
d58dd25dbb | ||
|
|
9b2731607b | ||
|
|
a8d2d6f13f | ||
|
|
7c21782ab5 | ||
|
|
f59bdaf5e0 | ||
|
|
9419ddd174 | ||
|
|
0666a8f5c2 | ||
|
|
81c019105c | ||
|
|
6584259454 | ||
|
|
03d0f56095 | ||
|
|
6ffd169784 | ||
|
|
406f64a7c5 | ||
|
|
1353a2c4c9 | ||
|
|
fb88f3bd96 | ||
|
|
22b3598704 | ||
|
|
b62c580d5e | ||
|
|
6a63ceaecc | ||
|
|
591f9e61fb | ||
|
|
7011c81bcd | ||
|
|
3cf7055122 | ||
|
|
aba31eda83 | ||
|
|
1b30582dd9 | ||
|
|
0fb66358cc | ||
|
|
e226f444f7 | ||
|
|
95bf70f568 | ||
|
|
a6597b44c3 | ||
|
|
51c01c5445 | ||
|
|
161bf75a1d | ||
|
|
2f16e2c608 | ||
|
|
aea2e79b37 | ||
|
|
f433d13a2f | ||
|
|
e9abf6ed05 | ||
|
|
0c32b25ddf | ||
|
|
a03dec91c5 | ||
|
|
7c5a966944 | ||
|
|
652dc0953b | ||
|
|
03860a6dce | ||
|
|
c6bee25264 | ||
|
|
068de0fa9f | ||
|
|
4b45d5ca26 | ||
|
|
a268391e68 | ||
|
|
89bdd86f14 | ||
|
|
e533bd7e35 | ||
|
|
09ed978e80 | ||
|
|
4b106e1f41 | ||
|
|
feeeb26d94 | ||
|
|
bef0d2d992 | ||
|
|
6e6bd8a6be | ||
|
|
631fa0db4e | ||
|
|
65d30b7a30 | ||
|
|
5ba5f27ca7 | ||
|
|
acc437bf2d | ||
|
|
5fd2505a33 | ||
|
|
7f6abc331b | ||
|
|
c190aab8b9 | ||
|
|
0f71abdac3 | ||
|
|
8ddc507bd5 | ||
|
|
1c4bae2d91 | ||
|
|
73ca4b1ea5 | ||
|
|
18a922b5cd | ||
|
|
11b98978c1 | ||
|
|
379d3811a8 | ||
|
|
0401b8a6e6 | ||
|
|
6148b996d8 | ||
|
|
39781c9cd7 | ||
|
|
18758a1614 | ||
|
|
b044d8d90e | ||
|
|
02e56f87bc | ||
|
|
d9b9ee221b | ||
|
|
21ec9188ca | ||
|
|
4d768becf5 | ||
|
|
a27049386b | ||
|
|
b23e3d7359 | ||
|
|
7660164583 | ||
|
|
5e1f3c5564 | ||
|
|
197fa9c01c | ||
|
|
554e3d0c2f | ||
|
|
b800cde34d | ||
|
|
775fdd2be0 | ||
|
|
7908ae4258 | ||
|
|
bfbd8229a1 | ||
|
|
afbf8dedbf | ||
|
|
569912abef | ||
|
|
7c94f6bc9a | ||
|
|
b825b5b063 | ||
|
|
50098b5e70 | ||
|
|
e237b4db1c | ||
|
|
2a25cf3bbd | ||
|
|
02275bb417 | ||
|
|
788cae3efe | ||
|
|
0dec70c53a | ||
|
|
f534f012d2 | ||
|
|
bb83875c99 | ||
|
|
d048aa33f7 | ||
|
|
8f3e250073 | ||
|
|
63a792d169 | ||
|
|
eb3524a22d | ||
|
|
f657a24a1a | ||
|
|
a5228448d7 | ||
|
|
1ec4796f72 | ||
|
|
6964158cf6 | ||
|
|
4fc4dd1b16 | ||
|
|
3e851f0c3c | ||
|
|
b8befaa973 | ||
|
|
b05046af29 | ||
|
|
eecc6c9e53 | ||
|
|
d4e754d601 | ||
|
|
a8a54593e2 | ||
|
|
5bbffc4f5c | ||
|
|
0833018399 | ||
|
|
f6850fc795 | ||
|
|
c0b4674568 | ||
|
|
5a8996d90a | ||
|
|
548b30e5b3 | ||
|
|
80f9329004 | ||
|
|
f672280236 | ||
|
|
90a4a01de7 | ||
|
|
09cebb90fe | ||
|
|
70389aab3d | ||
|
|
d9132a722f | ||
|
|
ea7a4e46e0 | ||
|
|
07b91058af | ||
|
|
c27ace6a6a | ||
|
|
1c0a5b17ca | ||
|
|
9b12a829d2 | ||
|
|
0f41172468 | ||
|
|
8597705a77 | ||
|
|
3f733ce857 | ||
|
|
40f8ec77b8 | ||
|
|
0af967d6c9 | ||
|
|
f6d43c9f39 | ||
|
|
70b0538dd5 | ||
|
|
439262b930 | ||
|
|
968b2587ae | ||
|
|
15f471a032 | ||
|
|
5175157ba6 | ||
|
|
e51e8f7196 | ||
|
|
00b34fda42 | ||
|
|
b34fabab54 | ||
|
|
487c7e2824 | ||
|
|
46c79a8772 | ||
|
|
bfb4144e57 | ||
|
|
dc1bb72070 | ||
|
|
8e084d2362 | ||
|
|
d5a75f887d | ||
|
|
710609e98b | ||
|
|
b73ab76bfb | ||
|
|
27b64df870 | ||
|
|
eabb897f96 | ||
|
|
68c5e47bad | ||
|
|
2ae5af7019 | ||
|
|
860d1ca9b3 | ||
|
|
66a9d1f25e | ||
|
|
bbfeedcdb3 | ||
|
|
079e273edb | ||
|
|
393aab4c4b | ||
|
|
4f2bf7549c | ||
|
|
acdf23571c | ||
|
|
62ec634db3 | ||
|
|
c53e978106 | ||
|
|
a7fa757d0d | ||
|
|
5fb1bd4175 | ||
|
|
e792ab7b4d | ||
|
|
02544d29fd | ||
|
|
20acbd0331 | ||
|
|
115b4aacb8 | ||
|
|
8746caab06 | ||
|
|
625648c908 | ||
|
|
734b5f9534 | ||
|
|
a0579318bd | ||
|
|
a437e3cbd3 | ||
|
|
1b242dc04e | ||
|
|
a1a51914a2 | ||
|
|
f6cab9b5a9 | ||
|
|
a3649c04e2 | ||
|
|
a562bfdb08 | ||
|
|
8ffe64ad8e | ||
|
|
a116d06d61 | ||
|
|
c26f73a5a8 | ||
|
|
f5847a57c1 | ||
|
|
fe9d23a0ff | ||
|
|
cdc27004bf | ||
|
|
b914164a77 | ||
|
|
35e58f90bc | ||
|
|
16d360c582 | ||
|
|
4c075b4d11 | ||
|
|
8c9c1c5afa | ||
|
|
d093163cd4 | ||
|
|
9bd6fcefd3 | ||
|
|
5139947643 | ||
|
|
01ff10385a | ||
|
|
9969c3a7ac | ||
|
|
f7ed2ec3e3 | ||
|
|
fedeeb3076 | ||
|
|
8157c27529 | ||
|
|
0eba0c6a4b | ||
|
|
13fb9db52b | ||
|
|
f6818ba880 | ||
|
|
dbf3b3cc79 | ||
|
|
24534069da | ||
|
|
4cec0a7350 | ||
|
|
0b86fa7bee | ||
|
|
b406e22695 | ||
|
|
3fca783dd8 | ||
|
|
6de4865052 | ||
|
|
facc2fab24 | ||
|
|
ddc0931e90 | ||
|
|
d5d32038f5 | ||
|
|
a20edd9bec | ||
|
|
3da90337ef | ||
|
|
9633f7644e | ||
|
|
a19cf0e1c3 | ||
|
|
1a841c4b5d | ||
|
|
937e7ba154 | ||
|
|
4cc0c85a6c | ||
|
|
943941e0f6 | ||
|
|
b160021e67 | ||
|
|
1bcc035979 | ||
|
|
ef67dc144f | ||
|
|
cc96fcd6a0 | ||
|
|
d1f00b2d48 | ||
|
|
2fe28d2335 | ||
|
|
f9276f4d83 | ||
|
|
099004a080 | ||
|
|
cc1df8d7d0 | ||
|
|
686a2e4fff | ||
|
|
e98fe3eec5 | ||
|
|
fc34ff38d3 | ||
|
|
b6b44b3782 | ||
|
|
c906636776 | ||
|
|
db282d1a04 | ||
|
|
898656963d | ||
|
|
6426e0238a | ||
|
|
08e8faf3ff | ||
|
|
d21adf6004 | ||
|
|
c3ac7dd1ab | ||
|
|
f1cfba3ad8 | ||
|
|
1ceed3461c | ||
|
|
d25c26156d | ||
|
|
d75c22114c | ||
|
|
05a754f446 | ||
|
|
b5e6eff65d | ||
|
|
eaa8ae66db | ||
|
|
5e4a08538b | ||
|
|
b01a54437a | ||
|
|
5f0fc78f30 | ||
|
|
325dc5e2fe | ||
|
|
a15b29122e | ||
|
|
074ccc8aab | ||
|
|
3809046fbc | ||
|
|
83ceb3264f | ||
|
|
9055858d55 | ||
|
|
2a465b5f1d | ||
|
|
b4101f856a | ||
|
|
44baa743c0 | ||
|
|
46dd449b2f | ||
|
|
f21d45e697 | ||
|
|
1e0a19ea7a | ||
|
|
dcfa47291e | ||
|
|
bd32c9555e | ||
|
|
f8f612544e | ||
|
|
1c0271f55e | ||
|
|
a10bc74de1 | ||
|
|
958ef80602 | ||
|
|
124b63f325 | ||
|
|
40f5ba5004 | ||
|
|
32f30826b9 | ||
|
|
b4aa8b37ea | ||
|
|
8368bbec47 | ||
|
|
618e482507 | ||
|
|
43711a1a59 | ||
|
|
bbe071545d | ||
|
|
4710479b46 | ||
|
|
c28a375b5d | ||
|
|
750d3429e0 | ||
|
|
4c34fe9b85 | ||
|
|
34c56980d4 | ||
|
|
a5b8609df1 | ||
|
|
1fd7f0314a | ||
|
|
25e82d690e | ||
|
|
31879cb60b | ||
|
|
489e5b551c | ||
|
|
e29bd01f68 | ||
|
|
e5d9140aa0 | ||
|
|
09f5cca948 | ||
|
|
405e09fdf4 | ||
|
|
43624e9b7b | ||
|
|
29ed0ad05a | ||
|
|
0aafe79c65 | ||
|
|
802afd592c | ||
|
|
ac1644e32d | ||
|
|
5a1f130bec | ||
|
|
678868153a | ||
|
|
991f050dbb | ||
|
|
be04355685 | ||
|
|
762b6b3f3b | ||
|
|
71d9cbdce6 | ||
|
|
d995ef19b5 | ||
|
|
bf80e4b02b | ||
|
|
64a047cd7c | ||
|
|
566421003c | ||
|
|
198da7608d | ||
|
|
0a7edb219e | ||
|
|
65c20f2211 | ||
|
|
144767152a | ||
|
|
82be52be52 | ||
|
|
46c8caa627 | ||
|
|
0027e838a0 | ||
|
|
7e846e2a58 | ||
|
|
de7f55bb97 | ||
|
|
2ac9a59469 | ||
|
|
bdb4014b94 | ||
|
|
9509bd1510 | ||
|
|
3f11770baa | ||
|
|
259a040b7e | ||
|
|
ccb14630f7 | ||
|
|
4aa865a60f | ||
|
|
c7ea7b057c | ||
|
|
91dbbd46c0 | ||
|
|
e5146077eb | ||
|
|
76918bf973 | ||
|
|
b7d3e69f87 | ||
|
|
d7bccfd267 | ||
|
|
2a25917e41 | ||
|
|
d73239b274 | ||
|
|
9f6fffbe6b | ||
|
|
72f7ff3df5 | ||
|
|
2af1dba8dc | ||
|
|
ca353d747b | ||
|
|
1589d4df1c | ||
|
|
b3abe8af9c | ||
|
|
c178e36f9b | ||
|
|
d93092de99 | ||
|
|
790b05880a | ||
|
|
f34766ade0 | ||
|
|
0e1d5e802c | ||
|
|
db526dfcc8 | ||
|
|
c18db60e80 | ||
|
|
b579a6ade2 | ||
|
|
9d1d642734 | ||
|
|
261c051052 | ||
|
|
e499e2d0dc | ||
|
|
b860b6f389 | ||
|
|
05d5d5a967 | ||
|
|
74ba1cc723 | ||
|
|
f2042efdc2 | ||
|
|
6b7e7fa1e4 | ||
|
|
6677e6e74f | ||
|
|
c3994ddbea | ||
|
|
a981ff2f6c | ||
|
|
3e0ba398d4 | ||
|
|
aa4f7c8536 | ||
|
|
959f9454d8 | ||
|
|
6b72a309f5 | ||
|
|
c388fe373d | ||
|
|
270ddc6487 | ||
|
|
5ccaa8f106 | ||
|
|
1d92c2668d | ||
|
|
6e03a05e6d | ||
|
|
2905059947 | ||
|
|
1df927f771 | ||
|
|
bd8b6d0319 | ||
|
|
fef39b2720 | ||
|
|
7de433c5fc | ||
|
|
0a711f2656 | ||
|
|
04b6b8aa8a | ||
|
|
58f286efe4 | ||
|
|
a01b292eb3 | ||
|
|
18c608ad7e | ||
|
|
8d144f4e12 | ||
|
|
7f895bfbec | ||
|
|
b0c356fa9b | ||
|
|
79defdc3f3 | ||
|
|
b2d9568deb | ||
|
|
a130c049bf | ||
|
|
8904039515 | ||
|
|
7d28181b16 | ||
|
|
98e4c81b9b | ||
|
|
10f5af5f09 | ||
|
|
18ffad5de5 | ||
|
|
428db42140 | ||
|
|
6e5426764e | ||
|
|
f3cfcc650c | ||
|
|
fc88573c9d | ||
|
|
5cf1c3d14f | ||
|
|
79f15cc34d | ||
|
|
775cab1080 | ||
|
|
3e20e7d0ce | ||
|
|
54407e0a60 | ||
|
|
ef696391d8 | ||
|
|
0c34df290e | ||
|
|
04d31bd814 | ||
|
|
9888d9f59e | ||
|
|
3bb1bf0967 | ||
|
|
dfbcb1f45c | ||
|
|
12ecf4de40 | ||
|
|
7be1171004 | ||
|
|
2bb646d150 | ||
|
|
e7749b2dff | ||
|
|
434d8eabc8 | ||
|
|
0a14219112 | ||
|
|
5b811df8ee | ||
|
|
bc264109f3 | ||
|
|
9c29c1ca9b | ||
|
|
fe4f62ff8d | ||
|
|
35dfb9d1ff | ||
|
|
3809aca09d | ||
|
|
bcd5bb5009 | ||
|
|
0d7cc6a386 | ||
|
|
12265699b3 | ||
|
|
783c0356c7 | ||
|
|
7ef31bc0b5 | ||
|
|
467f2368dd | ||
|
|
2cfcb081a2 | ||
|
|
1829ac851d | ||
|
|
2e715004ae | ||
|
|
45ee092593 | ||
|
|
20e543f721 | ||
|
|
b2148eb656 | ||
|
|
efab4fb41b | ||
|
|
9ac3fd3615 | ||
|
|
14bc7a0f76 | ||
|
|
26727fea17 | ||
|
|
34f8d4c2a6 | ||
|
|
02708807bd | ||
|
|
176e5de531 | ||
|
|
b9180a4426 | ||
|
|
959b30c788 | ||
|
|
df79ef59ea | ||
|
|
4dc1f01cf0 | ||
|
|
cb17110562 | ||
|
|
4424e4f9be | ||
|
|
fb641ac960 | ||
|
|
a5faa378b0 | ||
|
|
66ea277a59 | ||
|
|
006bc6ceda | ||
|
|
aef3e869dc | ||
|
|
9c46d28871 | ||
|
|
1fc141050f | ||
|
|
1e45d199e2 | ||
|
|
3018f3653c | ||
|
|
1c5b856800 | ||
|
|
f53a5ea6c1 | ||
|
|
a608734be9 | ||
|
|
e52523f903 | ||
|
|
82778014b8 | ||
|
|
cb4b9a361f | ||
|
|
3a337940d1 | ||
|
|
9abdafb905 | ||
|
|
cd494b52a4 | ||
|
|
5581d1431b | ||
|
|
6539b14ecf | ||
|
|
45b30e4a33 | ||
|
|
ff48d543e7 | ||
|
|
6562b74130 | ||
|
|
23a68370b4 | ||
|
|
cc9f346d49 | ||
|
|
8c18865138 | ||
|
|
db3a17a2c8 | ||
|
|
5a0d1ac0c0 | ||
|
|
5414accc6c | ||
|
|
c415ace453 | ||
|
|
bf34beec20 | ||
|
|
f4d459af7f | ||
|
|
508cc2bd91 | ||
|
|
35b7e3a289 | ||
|
|
fc907d23a7 | ||
|
|
45e663fcf8 | ||
|
|
b00c6a9268 | ||
|
|
ad7b0cd834 | ||
|
|
eccb3e2825 | ||
|
|
f5a343f358 | ||
|
|
f8b65a5546 | ||
|
|
0f0b9c5551 | ||
|
|
41ab11e7b4 | ||
|
|
9949e7c8d4 | ||
|
|
9e36d84f19 | ||
|
|
ada526fa63 | ||
|
|
ca65eb907d | ||
|
|
7f2a0dd3e8 | ||
|
|
6e34409225 | ||
|
|
4af354e918 | ||
|
|
207d0caf2a | ||
|
|
f7487d22d5 | ||
|
|
3c10976aff | ||
|
|
ec8e144655 | ||
|
|
ae5ccfd775 | ||
|
|
9d9e22451d | ||
|
|
e68daf870f | ||
|
|
534e95f86c | ||
|
|
077713003f | ||
|
|
35fd8e62ac | ||
|
|
b7c2ddd59d | ||
|
|
f931567f56 | ||
|
|
8545e0692c | ||
|
|
19eb05269b | ||
|
|
92d757662a | ||
|
|
1b1406a4d9 | ||
|
|
03fc19f070 | ||
|
|
ffa09f1b29 | ||
|
|
ff9a1523fd | ||
|
|
8a7e5c0830 | ||
|
|
8e90ad69b1 | ||
|
|
52c389edd8 | ||
|
|
f7206c1603 | ||
|
|
86d9baa503 | ||
|
|
92c4565590 | ||
|
|
c51870ff79 | ||
|
|
182f061354 | ||
|
|
80507cab27 | ||
|
|
f801ae9b63 | ||
|
|
977173d987 | ||
|
|
cd0fcb903f | ||
|
|
7bae1d9537 | ||
|
|
b43cbb7afe | ||
|
|
72982387cc | ||
|
|
ff0245840a | ||
|
|
c55f64e209 | ||
|
|
a4cb5d1b14 | ||
|
|
13e1415355 | ||
|
|
7b49d66a8e | ||
|
|
63c6e12aca | ||
|
|
b64d4fd96f | ||
|
|
dda603c7d8 | ||
|
|
e22de5cba1 | ||
|
|
bdabfdcb3d | ||
|
|
00a8514245 | ||
|
|
94fd3197b3 | ||
|
|
7957353c3f | ||
|
|
b3b7589db3 | ||
|
|
5aed9ce29e | ||
|
|
924f0a9f7c | ||
|
|
7a7cd72db9 | ||
|
|
e9e9bab479 | ||
|
|
f258dcfca2 | ||
|
|
fe84f812e7 | ||
|
|
9eba376976 | ||
|
|
462705c4ed | ||
|
|
a2533ce7f9 | ||
|
|
dbf42c51a4 | ||
|
|
f61e7caf2b | ||
|
|
679c2070c1 | ||
|
|
92d46e1da3 | ||
|
|
7ab94205e4 | ||
|
|
ab616c5d32 | ||
|
|
8f2f68dffc | ||
|
|
18b7e17e95 | ||
|
|
cca2298d3a | ||
|
|
f3683bcc84 | ||
|
|
fa2959515e | ||
|
|
7ab5ddc408 | ||
|
|
f273858248 | ||
|
|
ca8f7374da | ||
|
|
ff1dce833a | ||
|
|
d3d5a7bade | ||
|
|
f5d6702472 | ||
|
|
3db687ad40 | ||
|
|
a5cb958398 | ||
|
|
9e522091c6 | ||
|
|
79f256976e | ||
|
|
b560c0bafd | ||
|
|
bd157d305e | ||
|
|
923016197a | ||
|
|
dcf96e2bf5 | ||
|
|
d4697cb203 | ||
|
|
6e6a50b46e | ||
|
|
b9830bc64a | ||
|
|
7c501cec45 | ||
|
|
add23934ca | ||
|
|
a8b798b00c | ||
|
|
b522b1de05 | ||
|
|
3660cbd450 | ||
|
|
50f8bb8721 | ||
|
|
e1a2d90382 | ||
|
|
d8872f215b | ||
|
|
484bc6ea39 | ||
|
|
7d1979f067 | ||
|
|
6927c0b30b | ||
|
|
aa74c5ccaf | ||
|
|
e3d0f571d2 | ||
|
|
d03dabdfdf | ||
|
|
fc339ae55f | ||
|
|
a0aa04fd8c | ||
|
|
6dc5ae1ae4 | ||
|
|
df02a6b50f | ||
|
|
9e4f733c28 | ||
|
|
1175b330f5 | ||
|
|
3cb9fff07f | ||
|
|
bfdb72dd0a | ||
|
|
5489462f90 | ||
|
|
94ac3c3922 | ||
|
|
bf75946e14 | ||
|
|
b2a70e0cce | ||
|
|
83780f9bcd | ||
|
|
bfb9795913 | ||
|
|
208598a6df | ||
|
|
6c79753051 | ||
|
|
095a238fff | ||
|
|
ebbde8afd3 | ||
|
|
bba5bb7411 | ||
|
|
9c155c6cf5 | ||
|
|
19da7a74df | ||
|
|
f5d76fd5ff | ||
|
|
77940c9430 | ||
|
|
54a42a14b6 | ||
|
|
52faf7884b | ||
|
|
841ed4e682 | ||
|
|
bc417b9eea | ||
|
|
da7dc477c8 | ||
|
|
6c519ebd61 | ||
|
|
88e859817b | ||
|
|
f5dd36260f | ||
|
|
a5325b84ae | ||
|
|
7095c9e71e | ||
|
|
fb4373c83a | ||
|
|
dd59a1aebb | ||
|
|
6f7edd0b40 | ||
|
|
397421010e | ||
|
|
8872b0519d | ||
|
|
83d3b3ffbf | ||
|
|
d1702bd62c | ||
|
|
928235eac8 | ||
|
|
3bdfaab158 | ||
|
|
d7902b4d08 | ||
|
|
3334abfb8f | ||
|
|
3e44e584c0 | ||
|
|
195a305fc3 | ||
|
|
cedffd17b3 | ||
|
|
59b29014d9 | ||
|
|
06db036e4a | ||
|
|
617e0837c9 | ||
|
|
7a275bd802 | ||
|
|
83f58eae68 | ||
|
|
19815fe27d | ||
|
|
0f75556bed | ||
|
|
dc0f925d24 | ||
|
|
c5948c4171 | ||
|
|
d144b06c1f | ||
|
|
92dfea677b | ||
|
|
7b5867e2c0 | ||
|
|
b643fe56d5 | ||
|
|
38fa72e9da | ||
|
|
82fddefc94 | ||
|
|
0c9c9cb90a | ||
|
|
38b50a8a84 | ||
|
|
0f250dbafd | ||
|
|
168650649f | ||
|
|
52babf82ae | ||
|
|
0c8517667f | ||
|
|
77d105cb9f | ||
|
|
8af33ea66a | ||
|
|
a57740e14e | ||
|
|
82230d70a5 | ||
|
|
15f3f9c0e5 | ||
|
|
7fc328492b | ||
|
|
81cedca590 | ||
|
|
df4e00e23f | ||
|
|
ad70f23a05 | ||
|
|
c93beb27fd | ||
|
|
41da2231ed | ||
|
|
9d9213fbdb | ||
|
|
50106d19e8 | ||
|
|
62d1e676bd | ||
|
|
0b7d49785c | ||
|
|
40736c4a05 | ||
|
|
1f0ab02702 | ||
|
|
21aa4f7b2b | ||
|
|
43d0a85061 | ||
|
|
8bdd31ae67 | ||
|
|
b78f93979d | ||
|
|
8d6b4a1d2e | ||
|
|
7630c83ae0 | ||
|
|
c7c47aff5a | ||
|
|
72e475cb84 | ||
|
|
f750a6aec2 | ||
|
|
0dde37e1f1 | ||
|
|
a2066fc137 | ||
|
|
f652f35c3a | ||
|
|
562697da27 | ||
|
|
d23d77ff90 | ||
|
|
119443cc88 | ||
|
|
27ae831799 | ||
|
|
b482947207 | ||
|
|
6ebc89695a | ||
|
|
a65dfec7a8 | ||
|
|
a0cd79e587 | ||
|
|
8fe1e2fee4 | ||
|
|
3cc9f7bc40 | ||
|
|
8d3d5025ed | ||
|
|
a49c0aea47 | ||
|
|
d366cf9885 | ||
|
|
be53afe6b4 | ||
|
|
cdd980112d | ||
|
|
bca284969d | ||
|
|
dd899a3e13 | ||
|
|
d6ca66aa2f | ||
|
|
20ba671cd3 | ||
|
|
672795ac49 | ||
|
|
9716d7fe43 | ||
|
|
193ad8d902 | ||
|
|
a87f903c50 | ||
|
|
82f154a229 | ||
|
|
bee36801ab | ||
|
|
37f379c890 | ||
|
|
88b995ca9c | ||
|
|
919289c5ca | ||
|
|
0535b56766 | ||
|
|
6afd413b87 | ||
|
|
4818409329 | ||
|
|
919b652a06 | ||
|
|
15d3ec9d81 | ||
|
|
e0be6e429e | ||
|
|
8d24be914d | ||
|
|
8bbe9c97e9 | ||
|
|
ccbd904a3f | ||
|
|
4ed3db7e41 | ||
|
|
65ced28004 | ||
|
|
4c282962b3 | ||
|
|
c64c436b9f | ||
|
|
378e270f41 | ||
|
|
7217bd7d1a | ||
|
|
ff2461d89d | ||
|
|
0eb3430c14 | ||
|
|
da7c1e40e3 | ||
|
|
8c9e928ddc | ||
|
|
477aef7db6 | ||
|
|
cc97d9f1ea | ||
|
|
ee6cf05a92 | ||
|
|
575db58476 | ||
|
|
986a2f8cbb | ||
|
|
0b1da914cd | ||
|
|
04acd7c56d | ||
|
|
4430d85242 | ||
|
|
55ade32589 | ||
|
|
0ffbaaaeaa | ||
|
|
62b40ddb84 | ||
|
|
2d5ce87605 | ||
|
|
7ca0bc5960 | ||
|
|
021c09e426 | ||
|
|
75db81f2f9 | ||
|
|
f162617765 | ||
|
|
b7d10a4c58 | ||
|
|
79ca1aea02 | ||
|
|
957201804c | ||
|
|
cf5e126eaa | ||
|
|
69fc0a118b | ||
|
|
4dacc26567 | ||
|
|
7de1ed7d45 | ||
|
|
ab47f01625 | ||
|
|
13c4fa4f1f | ||
|
|
173631f115 | ||
|
|
8462e9c097 | ||
|
|
3c9447e1b6 | ||
|
|
38eaf2eb02 | ||
|
|
c8364ed17b | ||
|
|
ba52738904 | ||
|
|
4061ae4275 | ||
|
|
82afb5b150 | ||
|
|
e1203f0c8d | ||
|
|
e6f6b3fee2 | ||
|
|
e5efcd3d26 | ||
|
|
bf45587c80 | ||
|
|
29a0d22938 | ||
|
|
635cc04c50 | ||
|
|
bc5343652b | ||
|
|
03f140fe3b | ||
|
|
3e4a119f61 | ||
|
|
3b7bcbc14a | ||
|
|
3c49e96e02 | ||
|
|
5be209ab59 | ||
|
|
56ea048ab3 | ||
|
|
9fc0bd0439 | ||
|
|
1c2798cbf4 | ||
|
|
9d8af2eaab | ||
|
|
bba1e0d12f | ||
|
|
c060e60e4a | ||
|
|
1c504bd899 | ||
|
|
b617648bd8 | ||
|
|
e849c7a34f | ||
|
|
f6dd1ce98e | ||
|
|
9c78db8d45 | ||
|
|
5154348cf9 | ||
|
|
4521cea3b4 | ||
|
|
0ff1ac7743 | ||
|
|
277a751ed4 | ||
|
|
96be2a86ca | ||
|
|
f28bff569a | ||
|
|
e34aa77a54 | ||
|
|
e53b65496f | ||
|
|
f6ee630615 | ||
|
|
ec2e1e3152 | ||
|
|
6cffe9baed | ||
|
|
b63df27409 | ||
|
|
617c466ac0 | ||
|
|
ed8e443f3a | ||
|
|
58cb49b125 | ||
|
|
7dd5baa9ec | ||
|
|
bbf9107560 | ||
|
|
be527355ee | ||
|
|
c866500120 | ||
|
|
3e2a40aaa0 | ||
|
|
eef9fa56c6 | ||
|
|
945d84dbc4 | ||
|
|
d353d33107 | ||
|
|
f54bad5d79 | ||
|
|
b605c70e91 | ||
|
|
1752e88c42 | ||
|
|
e2718a39a0 | ||
|
|
25298ac69e | ||
|
|
cf9f389f75 | ||
|
|
567c75830a | ||
|
|
7b1d352c95 | ||
|
|
4fa54381a6 | ||
|
|
9c91f7cf83 | ||
|
|
edd5cd1ca1 | ||
|
|
f2ec6ad05e | ||
|
|
a04ef4ac38 | ||
|
|
43b3d1d379 | ||
|
|
b65fdeacc2 | ||
|
|
622fe1e5d9 | ||
|
|
a6c7c0c7ad | ||
|
|
e5c8748b75 | ||
|
|
f096d71cc1 | ||
|
|
d73a152a36 | ||
|
|
f4e6fd060e | ||
|
|
c78ca1de5d | ||
|
|
2b219c7256 | ||
|
|
6ffa651742 | ||
|
|
cb3b22a4bf | ||
|
|
145bfdb0e9 | ||
|
|
8c7b0c502d | ||
|
|
684bf12a5c | ||
|
|
aaa75aff14 | ||
|
|
f03600bd65 | ||
|
|
1c4c4fe3fb | ||
|
|
5e46b2cd1a | ||
|
|
027db7c0ec | ||
|
|
5bb17ddc6b | ||
|
|
e8edd74bc3 | ||
|
|
ed064a825a | ||
|
|
32c232a0b5 | ||
|
|
c2fd747c45 | ||
|
|
9148853f2c | ||
|
|
23d0f50a3d | ||
|
|
36cdf87bfe | ||
|
|
cfd2e1fd7b | ||
|
|
3cafac99ff | ||
|
|
1dd4e8da71 | ||
|
|
543015a36e | ||
|
|
2efdfc4b1f | ||
|
|
7234d9307e | ||
|
|
769ce1ce7c | ||
|
|
62c1d5783e | ||
|
|
a6bd4a2ffe | ||
|
|
f1a9077b7e | ||
|
|
2c3e80bd8f | ||
|
|
e52d066fb0 | ||
|
|
5279d57018 | ||
|
|
25e5eb4427 | ||
|
|
b01d5ce8c4 | ||
|
|
ff41f5c041 | ||
|
|
dd0770a93f | ||
|
|
9a3e76fff3 | ||
|
|
7c867578ee | ||
|
|
d543c27000 | ||
|
|
a8be330baa | ||
|
|
c128c58110 | ||
|
|
e32a3887cd | ||
|
|
94932c7486 | ||
|
|
a1920745fb | ||
|
|
51e8f9555e | ||
|
|
213ab8418b | ||
|
|
707f1dd6cb | ||
|
|
125ce036cd | ||
|
|
172c562a71 | ||
|
|
80bbe6711c | ||
|
|
3f56c64086 | ||
|
|
e6167119e0 | ||
|
|
368fae5f32 | ||
|
|
6ae46cf7bb | ||
|
|
e97806c85b | ||
|
|
f31e88bed1 | ||
|
|
6bd20038f9 | ||
|
|
30cfb1200d | ||
|
|
154e0039d1 | ||
|
|
a8f3b02ee7 | ||
|
|
b3e83e13bc | ||
|
|
d0a0e77c44 | ||
|
|
a14896307f | ||
|
|
976b300277 | ||
|
|
ccbd873204 | ||
|
|
9c1482f9e9 | ||
|
|
5a7f4efa91 | ||
|
|
f78c4a1fb0 | ||
|
|
db6500369f | ||
|
|
9e4beaa317 | ||
|
|
afd021737a | ||
|
|
3982ed56f7 | ||
|
|
df4a01a7f9 | ||
|
|
a6fac96ec1 | ||
|
|
8ed9186ff4 | ||
|
|
821df75d4b | ||
|
|
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 | ||
|
|
59aa854470 |
119
.claude/skills/dootask-backup/SKILL.md
Normal file
119
.claude/skills/dootask-backup/SKILL.md
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
description: 备份 DooTask 数据:数据库(必须)+ public/uploads(排除 tmp,可选)+ docker/appstore/config(可选)。汇总到临时目录并附 README 说明,打包到 backup/ 按日期命名。只读取源数据、绝不删改,失败即停。
|
||||||
|
---
|
||||||
|
|
||||||
|
# DooTask 数据备份
|
||||||
|
|
||||||
|
**刚性技能**——前置检查 → 选可选项 → 确认 → 执行 → 报告。只读取源数据生成归档,**绝不删除或修改任何源数据/既有备份**。任何一步失败立即停止。
|
||||||
|
|
||||||
|
## 备份范围
|
||||||
|
|
||||||
|
| 项 | 来源 | 是否必须 | 说明 |
|
||||||
|
|----|------|---------|------|
|
||||||
|
| 数据库 | `./cmd mysql backup` 产出的 `.sql.gz` | **必须** | 脚本内部用 mysqldump 导出当前库 |
|
||||||
|
| 上传文件 | `public/uploads`(**排除 `public/uploads/tmp`**) | 可选 | 头像/聊天/任务/文件等真实上传数据;`tmp` 是临时目录,可重建,不备份 |
|
||||||
|
| 应用配置 | `docker/appstore/config` | 可选 | 应用市场各应用的配置;含 **root 属主子目录**,收集时可能需 sudo |
|
||||||
|
|
||||||
|
> `docker/appstore/apps` **不在备份范围**——可从应用市场重新安装,无需备份。
|
||||||
|
|
||||||
|
## 前置检查(全部通过才能继续)
|
||||||
|
|
||||||
|
1. **工作目录**:在项目根(存在 `cmd`、`docker-compose.yml`)
|
||||||
|
2. **数据库容器**:`mariadb` 容器在跑(DB 备份依赖它;不在则提示用户先 `./cmd up` 起服务)
|
||||||
|
3. **磁盘空间**:确认 `backup/` 所在盘空间足够(数据库 dump 可能较大)
|
||||||
|
4. **选可选项**:询问用户本次是否包含 `public/uploads` 和 `docker/appstore/config`(**默认两个都含**)
|
||||||
|
|
||||||
|
检查通过、可选项确定后,汇报本次将备份哪些项,**向用户确认一次**再执行。
|
||||||
|
|
||||||
|
## 执行
|
||||||
|
|
||||||
|
用一个统一时间戳贯穿全程:`TS=$(date +%Y%m%d_%H%M%S)`,临时目录 `WORK="tmp/dootask-backup-${TS}"`。
|
||||||
|
|
||||||
|
### 1) 建临时工作目录
|
||||||
|
```shell
|
||||||
|
mkdir -p "$WORK"
|
||||||
|
```
|
||||||
|
(`tmp/` 已被 gitignore,安全)
|
||||||
|
|
||||||
|
### 2) 数据库(必须)
|
||||||
|
```shell
|
||||||
|
./cmd mysql backup
|
||||||
|
```
|
||||||
|
脚本会把 dump 写到 `docker/mysql/backup/<库名>_<时间戳>.sql.gz` 并打印「备份文件:...」。**取该次产出的最新 dump** 复制进工作目录(不用关心它原始落在哪):
|
||||||
|
```shell
|
||||||
|
DB_FILE=$(ls -t docker/mysql/backup/*.sql.gz | head -1)
|
||||||
|
cp "$DB_FILE" "$WORK/"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3) public/uploads(可选,排除 tmp)
|
||||||
|
```shell
|
||||||
|
rsync -a --exclude='tmp' public/uploads/ "$WORK/uploads/"
|
||||||
|
```
|
||||||
|
> 无 rsync 时用 tar 管道:`mkdir -p "$WORK/uploads" && tar cf - --exclude='./tmp' -C public/uploads . | tar xf - -C "$WORK/uploads"`
|
||||||
|
|
||||||
|
### 4) docker/appstore/config(可选)
|
||||||
|
```shell
|
||||||
|
cp -a docker/appstore/config "$WORK/appstore-config"
|
||||||
|
```
|
||||||
|
> 含 root 属主子目录,若报 `permission denied`:改用 `sudo cp -a ...`,随后把整个工作目录属主归还当前用户,保证后续打包/清理不受阻:
|
||||||
|
> ```shell
|
||||||
|
> sudo chown -R "$(id -u):$(id -g)" "$WORK"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### 5) 写 README.md(备份说明)
|
||||||
|
在 `$WORK/README.md` 写明本次备份信息,便于日后识别与还原。模板:
|
||||||
|
```markdown
|
||||||
|
# DooTask 备份 — <TS>
|
||||||
|
|
||||||
|
- 备份时间:<人类可读时间>
|
||||||
|
- DooTask 版本:<取自 package.json 的 version>
|
||||||
|
- 包含内容:
|
||||||
|
- 数据库:<DB dump 文件名>(来源 mysqldump 当前库)
|
||||||
|
- 上传文件:uploads/(来源 public/uploads,已排除 tmp) ← 未选则写「未包含」
|
||||||
|
- 应用配置:appstore-config/(来源 docker/appstore/config) ← 未选则写「未包含」
|
||||||
|
- 各项大小:<du -sh 列出工作目录内各项>
|
||||||
|
|
||||||
|
## 还原提示
|
||||||
|
- 数据库:`gunzip < <db>.sql.gz | mysql -u<user> -p<pass> <库名>`,或用 `./cmd mysql recovery` 选对应文件还原。
|
||||||
|
- 上传文件:将 uploads/ 内容覆盖回项目 public/uploads/。
|
||||||
|
- 应用配置:将 appstore-config/ 覆盖回 docker/appstore/config/。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6) 打包到 backup/,清理临时目录
|
||||||
|
```shell
|
||||||
|
mkdir -p backup
|
||||||
|
tar czf "backup/dootask_backup_${TS}.tar.gz" -C tmp "dootask-backup-${TS}"
|
||||||
|
rm -rf "$WORK"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 报告
|
||||||
|
|
||||||
|
向用户报告:
|
||||||
|
- 最终归档路径:`backup/dootask_backup_<TS>.tar.gz`
|
||||||
|
- 归档大小(`ls -lh`)
|
||||||
|
- 实际包含了哪些项(数据库 + 视选择含/不含 uploads、appstore-config)
|
||||||
|
|
||||||
|
## 失败处理
|
||||||
|
|
||||||
|
- 任何步骤失败立即停止,原样报告错误
|
||||||
|
- **不要**自动重试、不要静默跳过某一项(可选项是否包含由前置确认决定,不在执行中临时变更)
|
||||||
|
- DB 备份失败(如 mariadb 未运行)→ 停止,提示用户起服务后重试
|
||||||
|
- 打包前若工作目录有 root 属主残留导致 tar/rm 失败 → `sudo chown` 归还属主后继续,不要删源数据
|
||||||
|
|
||||||
|
## 禁止项
|
||||||
|
|
||||||
|
| 错误做法 | 正确做法 |
|
||||||
|
|---------|---------|
|
||||||
|
| 为"省空间"删除源数据或既有备份 | 只读取源数据生成归档,源数据一律不动 |
|
||||||
|
| 备份 `public/uploads/tmp` | 排除 tmp(临时、可重建) |
|
||||||
|
| 把 `docker/appstore/apps` 也打进去 | 不在范围,可从应用市场重装 |
|
||||||
|
| 遇 config 的 root 子目录就跳过该项 | `sudo` 收集后 chown 归还,完整备份 |
|
||||||
|
| 不写 README 直接打包 | 每个归档自带 README,便于日后识别还原 |
|
||||||
|
| 把归档写进 git | 归档放 `backup/`(已 gitignore),不提交 |
|
||||||
|
|
||||||
|
## Red Flags —— 出现这些念头立即停下
|
||||||
|
|
||||||
|
- "源数据太大,删点旧的再备份" → 不,备份只读不删
|
||||||
|
- "config 有 root 目录,跳过算了" → 不,sudo 收集后归还属主
|
||||||
|
- "apps 也一起备了更全" → 不,apps 不在范围
|
||||||
|
- "tmp 里临时文件顺手也备了" → 不,明确排除 `public/uploads/tmp`
|
||||||
76
.claude/skills/dootask-fix-permission/SKILL.md
Normal file
76
.claude/skills/dootask-fix-permission/SKILL.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
name: dootask-fix-permission
|
||||||
|
description: 修复 DooTask 可写目录(bootstrap/cache、docker、public、storage)的属主/权限:chown 回当前用户 + 目录 chmod 775,对齐 install 的赋权逻辑,赋权不删数据。
|
||||||
|
---
|
||||||
|
|
||||||
|
# DooTask 目录权限修复
|
||||||
|
|
||||||
|
容器内进程常以 **root** 写入挂载目录(`storage`、`public/uploads`、`bootstrap/cache` 等),导致宿主机当前用户对这些文件**没有写权限**,进而触发:
|
||||||
|
|
||||||
|
- `./cmd install` 报「目录【xxx】权限不足」/ 目录权限检测失败
|
||||||
|
- `./cmd build`(vite)报 `EACCES: permission denied, copyfile`(复制 `public/uploads/...` 时)
|
||||||
|
- Laravel 运行时写 `storage`/`bootstrap/cache` 失败
|
||||||
|
|
||||||
|
本技能**对齐 `./cmd install` 的目录赋权逻辑**:对四个可写目录做 `chmod 775`(目录)+ `chown` 回当前用户。
|
||||||
|
|
||||||
|
## 适用目录
|
||||||
|
|
||||||
|
与 install 一致的四个:
|
||||||
|
|
||||||
|
```
|
||||||
|
bootstrap/cache
|
||||||
|
docker
|
||||||
|
public # 含 public/uploads(真实上传数据)
|
||||||
|
storage
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心原则:赋权,不删数据
|
||||||
|
|
||||||
|
`public/uploads` 含真实上传文件(头像、附件等)。**永远优先 `chown` 改属主,不要删数据。** 即便用户说"清理一下",也只允许清临时目录 `public/uploads/tmp`,**切勿**删 uploads 下其他内容。
|
||||||
|
|
||||||
|
## 前置检查
|
||||||
|
|
||||||
|
1. **工作目录**:在项目根(存在 `cmd` 且这四个目录在)
|
||||||
|
2. **sudo**:改属主需 root(当前文件多为 root 属主)。本机一般可免密 sudo;不行则经 docker 以 root 改权限
|
||||||
|
3. 确认要修的范围:默认四个目录全修;若用户只想解 build 报错,也可只针对 `public`(含 `public/uploads`)
|
||||||
|
|
||||||
|
检查通过后汇报将执行的命令,**向用户确认一次**再执行。
|
||||||
|
|
||||||
|
## 执行
|
||||||
|
|
||||||
|
确认后执行(属主修回当前用户,目录权限 775):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 1) 属主修回当前用户(递归)
|
||||||
|
sudo chown -R "$(id -u):$(id -g)" bootstrap/cache docker public storage
|
||||||
|
|
||||||
|
# 2) 目录权限 775(仅目录,对齐 install 的 `find -type d -exec chmod 775`)
|
||||||
|
find bootstrap/cache docker public storage -type d -exec chmod 775 {} \;
|
||||||
|
```
|
||||||
|
|
||||||
|
> 只想解 build 的 uploads 报错时,可只对 `public`:
|
||||||
|
> ```shell
|
||||||
|
> sudo chown -R "$(id -u):$(id -g)" public/uploads
|
||||||
|
> ```
|
||||||
|
|
||||||
|
执行后报告:改了哪些目录、属主/权限现状(可 `ls -ld` 抽查),并提示用户可重试之前失败的 install/build/update。
|
||||||
|
|
||||||
|
## 失败处理
|
||||||
|
|
||||||
|
- `chown` 报权限不足 → 当前用户无 sudo 权限,提示用户用有 root 权限的账户,或经 docker 以 root 执行;不要静默跳过
|
||||||
|
- 任何步骤失败立即停止报告,不自动重试
|
||||||
|
|
||||||
|
## 禁止项
|
||||||
|
|
||||||
|
| 错误做法 | 正确做法 |
|
||||||
|
|---------|---------|
|
||||||
|
| build 报 uploads EACCES 就 `rm` 删文件 | `chown` 修属主,保留数据 |
|
||||||
|
| 删整个 `public/uploads` 清场 | 最多清 `public/uploads/tmp`,别碰真实上传数据 |
|
||||||
|
| 对文件无差别 `chmod 777` | 目录 `chmod 775` + `chown` 回当前用户即可 |
|
||||||
|
| 不加 sudo 直接 chown root 文件 | 改属主需 root |
|
||||||
|
|
||||||
|
## Red Flags —— 出现这些念头立即停下
|
||||||
|
|
||||||
|
- "uploads 复制失败,删掉再 build" → 不,`chown` 赋权,不丢数据
|
||||||
|
- "777 一把梭最省事" → 不,按 install 的 775(目录)+ chown
|
||||||
|
- "权限不够就跳过这个目录" → 不,报告交用户处理 sudo
|
||||||
74
.claude/skills/dootask-install/SKILL.md
Normal file
74
.claude/skills/dootask-install/SKILL.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: dootask-install
|
||||||
|
description: 首次部署 DooTask:前置检查后执行 `sudo ./cmd install`(建库 + migrate --seed 的重操作),刚性流程、单次确认、失败即停。
|
||||||
|
---
|
||||||
|
|
||||||
|
# DooTask 安装流程
|
||||||
|
|
||||||
|
**刚性技能**——前置检查 → 向用户确认一次 → 执行 → 报告结果。任何一步失败立即停止。
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
**违反字面规则 = 违反流程精神。** 不要擅自增加、省略、合并步骤,不要为"省事"绕过 sudo 或确认。
|
||||||
|
|
||||||
|
`./cmd install` 已把整套安装封装为单条命令(赋权→起容器→`composer install`→`key:generate`→`migrate --seed`→`up -d`)。本技能的职责是**安装前把关、选对参数、执行前确认、已知失败处理**,而不是把脚本逻辑拆开重做。
|
||||||
|
|
||||||
|
## 前置检查(全部通过才能继续)
|
||||||
|
|
||||||
|
执行前依次确认:
|
||||||
|
|
||||||
|
1. **工作目录**:必须在项目根(存在 `cmd`、`docker-compose.yml`、`.env.docker`)
|
||||||
|
2. **Docker**:`docker` 与 `docker-compose`/`docker compose`(v2+) 可用且 daemon 在跑(脚本 `check_docker` 也会查,但提前确认能更早报错)
|
||||||
|
3. **Node.js ≥ 20**(脚本 `check_node` 会查)
|
||||||
|
4. **APP_ID 不冲突**:若 `.env` 已有 `APP_ID` 且被其他实例占用,脚本 `check_instance` 会报错——此时**停止**,提示用户先清空 `.env` 里的 `APP_ID` 和 `APP_IPPR` 再装
|
||||||
|
5. **sudo**:`./cmd install` 需 root(`check_sudo`),用 `sudo ./cmd install` 执行
|
||||||
|
|
||||||
|
⚠️ **这是重操作**:会创建数据库并执行 `migrate --seed`(灌入种子数据)。在已有数据的环境上重装前务必和用户确认,避免覆盖。
|
||||||
|
|
||||||
|
检查通过后汇报结果,**向用户确认一次**再执行。
|
||||||
|
|
||||||
|
## 参数选择
|
||||||
|
|
||||||
|
| 参数 | 作用 | 何时用 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `--port <端口>` | 指定 HTTP 端口(脚本会做端口占用检测) | 用户要自定义端口,或默认端口被占 |
|
||||||
|
| `--relock` | 删除 `node_modules`/`package-lock.json`/`vendor`/`composer.lock` 后重装 | **谨慎**:仅在依赖锁损坏、用户明确要求重建锁时用,会拖慢安装 |
|
||||||
|
|
||||||
|
不确定时不要自作主张加参数,按需询问用户。
|
||||||
|
|
||||||
|
## 执行
|
||||||
|
|
||||||
|
确认后执行(按用户选择带上参数):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo ./cmd install
|
||||||
|
# 或: sudo ./cmd install --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
成功后脚本会输出访问地址并调用 `repassword.sh`。执行完向用户报告:访问地址(`http://127.0.0.1:<APP_PORT>`)、以及数据库密码提示。
|
||||||
|
|
||||||
|
## 失败处理
|
||||||
|
|
||||||
|
- 任何步骤失败立即停止,原样报告错误信息
|
||||||
|
- **不要**自动重试,**不要**自动跳过
|
||||||
|
- 常见失败与对应处理:
|
||||||
|
- `APP_ID(xxx)已被其他实例使用` → 停止,让用户清空 `.env` 的 `APP_ID`/`APP_IPPR` 再装
|
||||||
|
- `端口 xxx 已被占用` → 停止,让用户换 `--port`
|
||||||
|
- `目录【xxx】权限不足` / 目录权限检测失败 → 这是目录属主/权限问题,引导用户用 **dootask-fix-permission** 技能修复后重装
|
||||||
|
- `安装依赖失败`(composer)→ 报告,交用户决定(常因网络/镜像源)
|
||||||
|
|
||||||
|
## 禁止项
|
||||||
|
|
||||||
|
| 错误做法 | 正确做法 |
|
||||||
|
|---------|---------|
|
||||||
|
| 不加 sudo 直接 `./cmd install` | 用 `sudo ./cmd install`(脚本强制 root) |
|
||||||
|
| 失败后"我再试一次"或自动跳过 | 立即停止,交还用户 |
|
||||||
|
| 在已有数据环境上不问就重装 | 先确认会 `migrate --seed`,可能影响现有数据 |
|
||||||
|
| 遇权限报错自己乱 `chmod`/`chown` | 走 dootask-fix-permission 技能统一处理 |
|
||||||
|
| 不问就加 `--relock` | 默认不加;仅用户明确要求或锁损坏时用 |
|
||||||
|
|
||||||
|
## Red Flags —— 出现这些念头立即停下
|
||||||
|
|
||||||
|
- "端口/权限报错了我顺手帮 TA 改一下别的" → 停下,只处理本次报的问题,按指引走对应技能
|
||||||
|
- "种子数据应该没事,直接重装" → 不,先确认是否会覆盖现有数据
|
||||||
|
- "sudo 麻烦,先试试不加" → 不,install 必须 root
|
||||||
204
.claude/skills/dootask-release/SKILL.md
Normal file
204
.claude/skills/dootask-release/SKILL.md
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
---
|
||||||
|
name: dootask-release
|
||||||
|
description: 从 `pro` 分支发布 DooTask 前端新版本:翻译 → 版本号/更新日志 → 构建 → 提交推送,刚性顺序、每步确认、失败即停。
|
||||||
|
---
|
||||||
|
|
||||||
|
# DooTask 发布流程
|
||||||
|
|
||||||
|
**刚性技能**——严格按顺序执行,每步向用户确认,任何一步失败立即停止。
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
按固定顺序执行,不增删、合并或重排步骤。翻译(Step 1)和更新日志(Step 2)由你直接产出;脚本只做确定性机械工作(算版本号、检测差异、字节级生成语言文件)。
|
||||||
|
|
||||||
|
## 前置检查(全部通过才能继续)
|
||||||
|
|
||||||
|
执行任何发布步骤前,依次检查:
|
||||||
|
|
||||||
|
1. **分支**:必须是 `pro`,否则停止,提示用户切换
|
||||||
|
2. **工作区**:`git status` 必须干净(无未提交变更、无未跟踪文件),否则**停止**并交由用户处理
|
||||||
|
3. **Node.js**:`node --version` 必须 ≥ 20
|
||||||
|
4. **PHP**:`php --version` 必须可用(Step 1 的脚本依赖本地 php,无需容器)。若 host 无 php,停止并提示用户
|
||||||
|
|
||||||
|
检查通过后汇报结果,用户确认后再开始执行。
|
||||||
|
|
||||||
|
## 发布步骤
|
||||||
|
|
||||||
|
**每步执行前**向用户确认;**每步执行后**报告结果。
|
||||||
|
|
||||||
|
开始前先把这份清单复制到你的回复里,逐项勾选、跟踪进度:
|
||||||
|
|
||||||
|
```
|
||||||
|
发布进度:
|
||||||
|
- [ ] 前置检查(分支 pro / 工作区干净 / node≥20 / php 可用)
|
||||||
|
- [ ] Step 1 翻译(diff → 翻译 → apply → generate)
|
||||||
|
- [ ] Step 2 版本号 + CHANGELOG
|
||||||
|
- [ ] Step 3 构建(./cmd prod)
|
||||||
|
- [ ] 汇总变更 → 用户确认 → commit + push
|
||||||
|
- [ ] 确认 GitHub Actions Publish 工作流 success
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 1: 翻译
|
||||||
|
|
||||||
|
多语言数据流:`language/original-{web,api}.txt`(原文/简体中文)→ 经翻译写入 `language/translate.json`(含 9 种语言)→ 生成 `public/language/{web,api}/*`。
|
||||||
|
|
||||||
|
**1.1 检测差异**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
php .claude/skills/dootask-release/scripts/language.php diff
|
||||||
|
```
|
||||||
|
|
||||||
|
输出 JSON:
|
||||||
|
- `regexErrorCount > 0`:translate.json **已有条目**的占位符与某语言值不一致 → **停止**,报告 `regexErrors`,交用户修复(这是历史数据问题,不要自行猜测修改)
|
||||||
|
- `redundantCount > 0`:translate.json 里有、但原文已删除的条目 → 仅作提示(apply 时会自动剔除,不致命)
|
||||||
|
- `needsCount == 0`:无新文案 → **跳到 1.4 直接生成**
|
||||||
|
- `needsCount > 0`:`needs` 数组即待翻译清单,每项 `key` 已转成占位符形式(如 `(%T1)`)→ 进入 1.2
|
||||||
|
|
||||||
|
**1.2 翻译**
|
||||||
|
|
||||||
|
对 `needs` 里的每个 `key`,翻成 8 种语言(`zh` 留空、`key` 原样保留):`zh-CHT` `en` `ko` `ja` `de` `fr` `id` `ru`。
|
||||||
|
|
||||||
|
要求:贴合「项目任务管理系统」语境;占位符 `(%T1)`/`(%M1)` 等原样保留、不可增删改,位置可随目标语言语序调整:
|
||||||
|
|
||||||
|
| 原文 | 翻成英语 |
|
||||||
|
|---|---|
|
||||||
|
| (%T1)的周报[(%T2)][(%T3)月第(%T4)周] | Weekly report of (%T1) [(%T2)] [Week (%T4) of month (%T3)] |
|
||||||
|
| (%T1)提交的「(%M2)」待你审批 | '(%M2)' submitted by (%T1) is waiting for your approval |
|
||||||
|
|
||||||
|
把结果写成一个 JSON 数组文件(建议放 `/tmp/dootask-release-translated.json`,避免污染工作区),每个元素含全部 10 个字段,顺序为:
|
||||||
|
`key, zh, zh-CHT, en, ko, ja, de, fr, id, ru`(`zh` 写 `""`)。
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"key":"...(%T1)...","zh":"","zh-CHT":"...","en":"...","ko":"...","ja":"...","de":"...","fr":"...","id":"...","ru":"..."}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**1.3 合并进 translate.json**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
php .claude/skills/dootask-release/scripts/language.php apply /tmp/dootask-release-translated.json
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会校验字段完整性与占位符完整性、追加新条目、剔除冗余项,并按项目原生格式写回 `translate.json`。任一条不合格会报错停止,按提示修正翻译后重试。
|
||||||
|
|
||||||
|
**1.4 生成前端/后端语言文件**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
php .claude/skills/dootask-release/scripts/language.php generate
|
||||||
|
```
|
||||||
|
|
||||||
|
由 `translate.json` 字节级重新生成 `public/language/web/*.js` 与 `public/language/api/*.json`(排序/转义与项目原生工具完全一致,正常情况下 diff 只包含本次新增条目)。
|
||||||
|
|
||||||
|
**1.5 报告**:用 `git status --short language public/language` 汇总本步改动,向用户报告新增了多少条翻译。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: 版本号 + 更新日志
|
||||||
|
|
||||||
|
**2.1 计算并写入版本号**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
node .claude/skills/dootask-release/scripts/version_bump.js
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本据 git 历史算出新 `version` 与 `codeVerson` 并写入 `package.json`,输出 JSON 含:`version`、`prevVersion`、`changelogRange`(如 `<上次release提交>..HEAD`,用于下一步圈定本次更新范围)。
|
||||||
|
|
||||||
|
**2.2 撰写 CHANGELOG**
|
||||||
|
|
||||||
|
读取本次区间的提交:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git log <changelogRange> --stat
|
||||||
|
```
|
||||||
|
|
||||||
|
`--stat` 会带上每个提交的完整描述正文 + 改动文件清单;光看标题不够时用 `git show <hash>` 看具体代码改动。
|
||||||
|
|
||||||
|
按 `CHANGELOG.md` 现有格式,在文件顶部 `# Changelog` 说明段之后、紧挨上一个 `## [...]` 之前,插入新版本区段:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [<version>]
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
撰写要求(对齐项目历史风格):
|
||||||
|
- 小节标题用**英文 Title Case**:`Features` / `Bug Fixes` / `Performance` / `Documentation` / `Security` / `Miscellaneous`,**不要译成中文**;**没有内容的小节整段省略**。
|
||||||
|
- 条目正文用**通俗友好的简体中文**,面向**普通用户**描述更新带来的直接好处,**避免技术术语**(如 refactor、merge branch、commit lint、bump deps 等)。
|
||||||
|
- 过滤掉对用户无意义的提交(纯构建/依赖/CI/合并提交、本技能自身的脚手架改动等)。
|
||||||
|
- 仅凭提交标题无法判断是否对用户有价值时,结合提交的完整描述正文和实际代码改动(`git show <hash>`)再决定,不要只看一行就下结论。
|
||||||
|
- 合并相似项;每个小节内**按用户价值与影响范围排序,重要的在前**。
|
||||||
|
|
||||||
|
**2.3 报告**:展示新版本号与你写的 changelog 区段,请用户过目。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: 构建前端
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./cmd prod
|
||||||
|
```
|
||||||
|
|
||||||
|
构建前端生产版本。用 `./cmd prod`,不要换成裸跑 vite(它还负责 node 检查、清 `public/js/build`、debug 切换)。
|
||||||
|
|
||||||
|
> **已知失败**:build 报 `public/uploads/...` 的 `EACCES: permission denied, copyfile`,是 vite 复制 `public/` 时撞到 root 属主的运行时上传文件(不限于 `tmp`,`avatar` 等都可能)。补救是赋权、不是删数据——把 uploads 属主改回当前用户后重试:
|
||||||
|
> ```shell
|
||||||
|
> sudo chown -R "$(id -u):$(id -g)" public/uploads
|
||||||
|
> ```
|
||||||
|
> `public/uploads` 是真实上传数据,**不要删**;即便要清也只清 `public/uploads/tmp`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最终:提交并推送
|
||||||
|
|
||||||
|
所有步骤完成后:
|
||||||
|
|
||||||
|
1. 通过 `git diff` + `git status` 汇总所有变更,向用户报告摘要
|
||||||
|
2. **询问用户是否提交并推送**
|
||||||
|
3. 用户明确确认后才执行 `git add`、`git commit`、`git push`
|
||||||
|
4. 未确认一律不执行
|
||||||
|
|
||||||
|
提交规范:
|
||||||
|
- 提交信息使用 `release: v<新版本号>`(与历史一致,参见 `git log --oneline | grep '^release:'`)
|
||||||
|
- **只 add 本次发布相关改动**,按文件名/目录显式添加(例如 `git add package.json CHANGELOG.md language/translate.json public/language public/js`),不要用 `git add -A` / `git add .`,以免卷入未跟踪的本地实验文件
|
||||||
|
- 不打 git tag(现行发布流程不使用 tag)
|
||||||
|
- 确认前先核对:`/tmp/dootask-release-translated.json` 等临时文件不在仓库内,工作区不应残留发布无关的未跟踪文件
|
||||||
|
|
||||||
|
## push 之后:确认发布工作流(CI 才是真正出包)
|
||||||
|
|
||||||
|
push 到 `pro` 只是触发器,真正的构建/出包由 GitHub Actions 完成——**push 成功 ≠ 发布完成**:
|
||||||
|
|
||||||
|
- **Publish**(`.github/workflows/publish.yml`,push→pro 触发)跑完才算出包;成功后会自动触发 **Sync to Gitee**(镜像同步)。
|
||||||
|
- push 完成后**主动确认** Publish 工作流 `conclusion=success`。优先用 `gh`(未装可临时装;公开仓库也可用 GitHub REST API 免鉴权读取 runs):
|
||||||
|
```shell
|
||||||
|
gh run list --workflow=publish.yml -R kuaifan/dootask -L 1
|
||||||
|
gh run view <run-id> -R kuaifan/dootask --json status,conclusion,url
|
||||||
|
```
|
||||||
|
- 工作流仍在跑时,挂后台轮询、结束即通知用户,**不要在前台死等**。
|
||||||
|
|
||||||
|
### iOS 发布(询问后决定)
|
||||||
|
|
||||||
|
`ios-publish.yml` 是**独立的手动工作流**(`workflow_dispatch`),不随 push 触发。Publish 成功后,用 options 或 AskUserQuestion 形式提问是否同时发布 iOS(选项:发布 iOS / 不发布):
|
||||||
|
|
||||||
|
- 选「发布 iOS」才执行:
|
||||||
|
```shell
|
||||||
|
gh workflow run ios-publish.yml --ref pro -R kuaifan/dootask
|
||||||
|
```
|
||||||
|
需 `gh` 已登录且 token 含 `workflow` 权限;触发后可挂后台轮询结果。
|
||||||
|
- 选「不发布」则结束。
|
||||||
|
|
||||||
|
## 失败处理
|
||||||
|
|
||||||
|
任何步骤失败立即停止、报告错误信息,交用户决定;不要自动重试或跳过。
|
||||||
239
.claude/skills/dootask-release/scripts/language.php
Normal file
239
.claude/skills/dootask-release/scripts/language.php
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
<?php
|
||||||
|
// DooTask 发布——翻译流水线(纯本地 php,host 直接跑,不进容器、不调 OpenAI、不需 autoload)。
|
||||||
|
// 逐行对齐 language/translate.php 的检测/保存/生成逻辑,唯独把"调用外部模型翻译"那一段抽走,
|
||||||
|
// 翻译改在技能流程内完成。用 php 而非 node 的唯一原因:array_multisort + json_encode
|
||||||
|
// 的逐字节产物必须与项目原生工具一致,否则每次发版都会产生大面积排序/转义噪声 diff(已验证 host php 可字节级复现)。
|
||||||
|
//
|
||||||
|
// 子命令:
|
||||||
|
// language.php diff
|
||||||
|
// —— 输出 JSON:needs(待翻译,key 已转成 (%T1)/(%M1) 形式) / redundants(冗余,提示) / regexErrors(占位符错乱,致命)
|
||||||
|
// language.php apply <translated.json>
|
||||||
|
// —— 把新翻译合并进 translate.json(追加 + 剔除冗余),不生成 public 文件
|
||||||
|
// language.php generate
|
||||||
|
// —— 由 translate.json 重新生成 public/language/{web,api}/*
|
||||||
|
//
|
||||||
|
// 项目根相对脚本自身定位(脚本固定在 <root>/.claude/skills/dootask-release/scripts/),与调用时的 cwd 无关。
|
||||||
|
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||||
|
|
||||||
|
$ROOT = dirname(__DIR__, 4);
|
||||||
|
$LANG_DIR = $ROOT . '/language';
|
||||||
|
$LANG_FIELDS = ['key', 'zh', 'zh-CHT', 'en', 'ko', 'ja', 'de', 'fr', 'id', 'ru'];
|
||||||
|
|
||||||
|
if (!is_dir($LANG_DIR)) {
|
||||||
|
fwrite(STDERR, "未找到 language 目录($LANG_DIR)。\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
chdir($LANG_DIR);
|
||||||
|
|
||||||
|
$cmd = $argv[1] ?? '';
|
||||||
|
|
||||||
|
// ---- 公共:读取 original-*.txt ----
|
||||||
|
function read_generateds(): array
|
||||||
|
{
|
||||||
|
$originals = [];
|
||||||
|
$generateds = [];
|
||||||
|
foreach (['web', 'api'] as $type) {
|
||||||
|
$content = file_exists("original-{$type}.txt") ? file_get_contents("original-{$type}.txt") : "";
|
||||||
|
$array = array_values(array_filter(array_unique(explode("\n", $content))));
|
||||||
|
$generateds[$type] = $array;
|
||||||
|
$originals = array_merge($originals, $array);
|
||||||
|
}
|
||||||
|
return [$originals, $generateds];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 公共:构建 translations 映射(normalizedKey -> obj),并收集冗余/占位符错乱 ----
|
||||||
|
function build_translations(array $originals): array
|
||||||
|
{
|
||||||
|
$translations = [];
|
||||||
|
$redundants = [];
|
||||||
|
$regrror = [];
|
||||||
|
if (!file_exists("translate.json")) {
|
||||||
|
fwrite(STDERR, "translate.json not exists\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
$tmps = json_decode(file_get_contents("translate.json"), true);
|
||||||
|
foreach ($tmps as $obj) {
|
||||||
|
if (!isset($obj['key'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$currentKey = $obj['key'];
|
||||||
|
$originalKey = preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $currentKey);
|
||||||
|
if (!in_array($originalKey, $originals)) {
|
||||||
|
$redundants[$originalKey] = $obj;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$translations[$originalKey] = $obj;
|
||||||
|
if (preg_match_all('/\(%[TM]\d+\)/', $currentKey, $matches)) {
|
||||||
|
foreach ($matches[0] as $match) {
|
||||||
|
foreach ($obj as $k => $v) {
|
||||||
|
if (empty($v)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!str_contains($v, $match)) {
|
||||||
|
$regrror[$originalKey] = ['key' => $currentKey, 'field' => $k, 'value' => $v, 'match' => $match];
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [$translations, $redundants, $regrror];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 公共:由 translate.json + originals 重新生成 public 文件 ----
|
||||||
|
function generate(array $generateds, array $translations): void
|
||||||
|
{
|
||||||
|
foreach ($generateds as $type => $array) {
|
||||||
|
$datas = [];
|
||||||
|
foreach ($array as $text) {
|
||||||
|
$text = trim($text);
|
||||||
|
if (isset($translations[$text])) {
|
||||||
|
$datas[] = $translations[$text];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$inOrder = [];
|
||||||
|
foreach ($datas as $index => $item) {
|
||||||
|
if (preg_match('/\(%[TM]\d+\)/', $item['key'])) {
|
||||||
|
$inOrder[$index] = strlen($item['key']);
|
||||||
|
} else {
|
||||||
|
$inOrder[$index] = strlen($item['key']) + 10000000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
array_multisort($inOrder, SORT_DESC, $datas);
|
||||||
|
$results = [];
|
||||||
|
foreach ($datas as $items) {
|
||||||
|
foreach ($items as $kk => $item) {
|
||||||
|
$results[$kk][] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($type === 'api') {
|
||||||
|
if (!is_dir("../public/language/api")) {
|
||||||
|
mkdir("../public/language/api", 0777, true);
|
||||||
|
}
|
||||||
|
foreach ($results as $kk => $item) {
|
||||||
|
file_put_contents("../public/language/api/$kk.json", json_encode($item, JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
} elseif ($type === 'web') {
|
||||||
|
if (!is_dir("../public/language/web")) {
|
||||||
|
mkdir("../public/language/web", 0777, true);
|
||||||
|
}
|
||||||
|
foreach ($results as $kk => $item) {
|
||||||
|
file_put_contents("../public/language/web/$kk.js", "if(typeof window.LANGUAGE_DATA===\"undefined\")window.LANGUAGE_DATA={};window.LANGUAGE_DATA[\"{$kk}\"]=" . json_encode($item, JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "[$type] total: " . count($results['key']) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cmd === 'diff') {
|
||||||
|
[$originals, $generateds] = read_generateds();
|
||||||
|
[$translations, $redundants, $regrror] = build_translations($originals);
|
||||||
|
|
||||||
|
// 需要翻译的数据(对齐 translate.php 150-169:占位符按单一计数器编号)
|
||||||
|
$needs = [];
|
||||||
|
foreach ($originals as $text) {
|
||||||
|
$key = trim($text);
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isset($translations[$key])) {
|
||||||
|
$needs[$key] = $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$needsOut = [];
|
||||||
|
foreach ($needs as $key) {
|
||||||
|
$c = 1;
|
||||||
|
$converted = preg_replace_callback('/\((\*+)\)/', function ($m) use (&$c) {
|
||||||
|
$label = strlen($m[1]) > 1 ? "M" : "T";
|
||||||
|
return "(%" . $label . $c++ . ")";
|
||||||
|
}, $key);
|
||||||
|
$needsOut[] = ['key' => $converted];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'needsCount' => count($needsOut),
|
||||||
|
'redundantCount' => count($redundants),
|
||||||
|
'regexErrorCount' => count($regrror),
|
||||||
|
'needs' => $needsOut,
|
||||||
|
'redundants' => array_keys($redundants),
|
||||||
|
'regexErrors' => array_values($regrror),
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
||||||
|
|
||||||
|
if (count($regrror) > 0) {
|
||||||
|
exit(2); // 已有数据占位符错乱,需先修复
|
||||||
|
}
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cmd === 'apply') {
|
||||||
|
$file = $argv[2] ?? '';
|
||||||
|
if ($file === '' || !file_exists($file)) {
|
||||||
|
fwrite(STDERR, "用法:apply <translated.json>(文件不存在)\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
[$originals, $generateds] = read_generateds();
|
||||||
|
[$translations, $redundants, $regrror] = build_translations($originals);
|
||||||
|
if (count($regrror) > 0) {
|
||||||
|
fwrite(STDERR, "translate.json 已有条目占位符错乱,请先修复再发版。\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$incoming = json_decode(file_get_contents($file), true);
|
||||||
|
if (!is_array($incoming)) {
|
||||||
|
fwrite(STDERR, "translated.json 必须是数组\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
$added = 0;
|
||||||
|
foreach ($incoming as $raw) {
|
||||||
|
foreach ($GLOBALS['LANG_FIELDS'] as $f) {
|
||||||
|
if (!array_key_exists($f, $raw)) {
|
||||||
|
fwrite(STDERR, "新翻译缺字段 \"$f\":" . json_encode($raw, JSON_UNESCAPED_UNICODE) . "\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 占位符完整性:key 里每个 (%T1)/(%M1) 必须出现在每个非空语言值里
|
||||||
|
if (preg_match_all('/\(%[TM]\d+\)/', $raw['key'], $m)) {
|
||||||
|
foreach ($m[0] as $match) {
|
||||||
|
foreach ($GLOBALS['LANG_FIELDS'] as $f) {
|
||||||
|
if ($f === 'key' || $f === 'zh') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (empty($raw[$f])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!str_contains($raw[$f], $match)) {
|
||||||
|
fwrite(STDERR, "占位符 $match 在字段 \"$f\" 缺失:{$raw['key']}\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 规范化:固定字段顺序 + zh 置空
|
||||||
|
$item = [];
|
||||||
|
foreach ($GLOBALS['LANG_FIELDS'] as $f) {
|
||||||
|
$item[$f] = $f === 'zh' ? '' : $raw[$f];
|
||||||
|
}
|
||||||
|
$originalKey = preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $item['key']);
|
||||||
|
$translations[$originalKey] = $item;
|
||||||
|
$added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// array_values:现有条目(去冗余)在前,新条目追加在后
|
||||||
|
file_put_contents("translate.json", json_encode(array_values($translations), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||||
|
echo json_encode([
|
||||||
|
'added' => $added,
|
||||||
|
'total' => count($translations),
|
||||||
|
'droppedRedundant' => count($redundants),
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cmd === 'generate') {
|
||||||
|
[$originals, $generateds] = read_generateds();
|
||||||
|
[$translations] = build_translations($originals);
|
||||||
|
generate($generateds, $translations);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fwrite(STDERR, "未知子命令:'$cmd'。可用:diff | apply <file> | generate\n");
|
||||||
|
exit(1);
|
||||||
47
.claude/skills/dootask-release/scripts/version_bump.js
vendored
Normal file
47
.claude/skills/dootask-release/scripts/version_bump.js
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// 计算并写入新版本号到 package.json(version + codeVerson),算法对齐 bin/version.js。
|
||||||
|
// 不生成 CHANGELOG(在技能流程内撰写),只输出版本号与 changelog 的提交区间。
|
||||||
|
//
|
||||||
|
// 项目根相对脚本自身定位(脚本固定在 <root>/.claude/skills/dootask-release/scripts/),与调用时的 cwd 无关。
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '../../../..');
|
||||||
|
const pkgFile = path.join(ROOT, 'package.json');
|
||||||
|
const verOffset = 6394; // 版本号偏移量(与 bin/version.js 一致)
|
||||||
|
const codeOffset = 35; // 代码版本号偏移量
|
||||||
|
|
||||||
|
function git(cmd) {
|
||||||
|
return execSync(cmd, { cwd: ROOT, maxBuffer: 1024 * 1024 * 10 }).toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const verCount = parseInt(git('git rev-list --count HEAD'), 10);
|
||||||
|
const codeCount = parseInt(git("git tag --merged pro -l 'v*' | wc -l"), 10);
|
||||||
|
const num = verOffset + verCount;
|
||||||
|
if (Number.isNaN(num)) {
|
||||||
|
console.error(`版本计算失败:rev-list count=${verCount}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const version = `${Math.floor(num / 10000)}.${Math.floor((num % 10000) / 100)}.${Math.floor(num % 100)}`;
|
||||||
|
const codeVersion = codeOffset + codeCount;
|
||||||
|
|
||||||
|
let pkg = fs.readFileSync(pkgFile, 'utf8');
|
||||||
|
const prevVersion = (pkg.match(/"version":\s*"(.*?)"/) || [])[1] || '';
|
||||||
|
pkg = pkg.replace(/"version":\s*"(.*?)"/, `"version": "${version}"`);
|
||||||
|
pkg = pkg.replace(/"codeVerson":(.*?)(,|$)/, `"codeVerson": ${codeVersion}$2`);
|
||||||
|
fs.writeFileSync(pkgFile, pkg, 'utf8');
|
||||||
|
|
||||||
|
// 上一个 release 提交作为 changelog 区间下界
|
||||||
|
let prevReleaseCommit = '';
|
||||||
|
try {
|
||||||
|
prevReleaseCommit = git("git log --grep='^release: v' -n 1 --pretty=format:%H");
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
version,
|
||||||
|
codeVersion,
|
||||||
|
prevVersion,
|
||||||
|
prevReleaseCommit,
|
||||||
|
changelogRange: prevReleaseCommit ? `${prevReleaseCommit}..HEAD` : '(未找到上一个 release 提交,需人工确定区间)',
|
||||||
|
}, null, 2));
|
||||||
83
.claude/skills/dootask-update/SKILL.md
Normal file
83
.claude/skills/dootask-update/SKILL.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
name: dootask-update
|
||||||
|
description: 更新已部署的 DooTask:前置检查后执行 `sudo ./cmd update`(拉代码 + composer + 迁移 + 重启),本地有改动时停下交用户决定,不自动强制、失败即停。
|
||||||
|
---
|
||||||
|
|
||||||
|
# DooTask 更新流程
|
||||||
|
|
||||||
|
**刚性技能**——前置检查 → 向用户确认一次 → 执行 → 报告结果。任何一步失败立即停止。
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
**违反字面规则 = 违反流程精神。** 不要擅自加步骤、绕过 sudo/确认,**尤其不要替用户决定强制更新**(会丢本地改动)。
|
||||||
|
|
||||||
|
`./cmd update` 已封装整套更新(检测本地改动→`git fetch`→必要时备份库→`git pull/reset`→`composer install`→`migrate`→重启 php+nginx→写 `UPDATE_TIME`)。本技能职责是**更新前把关、选对参数、处理本地改动这一关键岔路、执行前确认**。
|
||||||
|
|
||||||
|
## 前置检查(全部通过才能继续)
|
||||||
|
|
||||||
|
1. **已安装**:必须存在 `vendor/autoload.php`(脚本会查,没装则报"请先执行安装命令"——此时引导用户走 dootask-install)
|
||||||
|
2. **工作目录**:在项目根
|
||||||
|
3. **当前分支 / 目标分支**:默认更新当前分支;用户要切分支用 `--branch <分支>`。若用户没说,确认是否就更新当前分支
|
||||||
|
4. **本地改动**(关键):`git status` 看是否有未提交改动
|
||||||
|
5. **sudo**:`sudo ./cmd update` 需 root
|
||||||
|
|
||||||
|
检查通过后汇报结果,**向用户确认一次**再执行。
|
||||||
|
|
||||||
|
## 关键岔路:本地有改动
|
||||||
|
|
||||||
|
脚本检测到本地改动时会询问是否强制更新。**强制更新 = `git reset --hard origin/<分支>`,会丢弃所有本地改动。**
|
||||||
|
|
||||||
|
- 发现本地有改动 → **停下**,把改动清单报告用户,让**用户决定**:先提交/暂存改动,还是确认强制更新
|
||||||
|
- **不要**替用户选 `--force`
|
||||||
|
- 只有用户明确说"丢掉改动强制更新"时,才带 `--force`
|
||||||
|
|
||||||
|
## 参数选择
|
||||||
|
|
||||||
|
| 参数 | 作用 | 何时用 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `--branch <分支>` | 切到指定分支再更新 | 用户要换分支(如切 `dev`/`pro`) |
|
||||||
|
| `--force` | 强制更新:`git checkout -f` + `git reset --hard` | **危险**:仅用户明确接受"丢弃本地改动"后 |
|
||||||
|
| `--local` | 本地更新模式:只备份库 + `migrate` + 重启,不拉远程代码 | 代码已就位(如手动改过/CI 拉过),只需迁移+重启 |
|
||||||
|
|
||||||
|
## 数据库
|
||||||
|
|
||||||
|
- 远程模式下,脚本检测到 `database/` 目录有迁移变动会**自动备份数据库**再继续——这是脚本内置的,无需手动。
|
||||||
|
- 但若是大版本升级或用户在意数据,执行前提醒用户:本次可能含库迁移,已有自动备份兜底;如需可先 `./cmd mysql backup` 额外备份。
|
||||||
|
|
||||||
|
## 执行
|
||||||
|
|
||||||
|
确认(含本地改动决策)后执行:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo ./cmd update
|
||||||
|
# 切分支: sudo ./cmd update --branch pro
|
||||||
|
# 强制(丢改动,用户确认后): sudo ./cmd update --force
|
||||||
|
# 本地模式: sudo ./cmd update --local
|
||||||
|
```
|
||||||
|
|
||||||
|
成功后报告:更新到的分支、是否做了库备份/迁移、服务是否重启完成。
|
||||||
|
|
||||||
|
## 失败处理
|
||||||
|
|
||||||
|
- 任何步骤失败立即停止,原样报告错误
|
||||||
|
- **不要**自动重试、不要自动跳过、不要因为 `git pull` 失败就自己改成 `--force`
|
||||||
|
- 常见失败:
|
||||||
|
- `请先执行安装命令` → 走 dootask-install
|
||||||
|
- `代码拉取失败,可能存在冲突` → 报告,让用户决定是否 `--force`(丢改动)或先处理冲突
|
||||||
|
- 重启服务失败 → 脚本会尝试 `down` 后重起;若仍失败,报告交用户
|
||||||
|
|
||||||
|
## 禁止项
|
||||||
|
|
||||||
|
| 错误做法 | 正确做法 |
|
||||||
|
|---------|---------|
|
||||||
|
| 检测到本地改动就自动 `--force` | 停下,报告改动,交用户决定 |
|
||||||
|
| `git pull` 失败就自动改用 `--force` | 报告冲突,交用户 |
|
||||||
|
| 不加 sudo | `sudo ./cmd update` |
|
||||||
|
| 未装就更新 | 先走 dootask-install |
|
||||||
|
| 失败后自动重试/跳过 | 立即停止 |
|
||||||
|
|
||||||
|
## Red Flags —— 出现这些念头立即停下
|
||||||
|
|
||||||
|
- "有点本地改动,强制更新一下就好了" → 不,`--force` 会丢改动,必须用户拍板
|
||||||
|
- "拉取冲突了,我 reset 一下" → 不,交用户决定
|
||||||
|
- "已经装过了吧,直接更新" → 先确认 `vendor/autoload.php` 在
|
||||||
@ -10,13 +10,14 @@ APP_URL=http://localhost
|
|||||||
APP_ID=
|
APP_ID=
|
||||||
APP_IPPR=
|
APP_IPPR=
|
||||||
APP_PORT=2222
|
APP_PORT=2222
|
||||||
|
APP_SSL_PORT=
|
||||||
APP_DEV_PORT=
|
APP_DEV_PORT=
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stack
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST="${APP_IPPR}.5"
|
DB_HOST=mariadb
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=dootask
|
DB_DATABASE=dootask
|
||||||
DB_USERNAME=dootask
|
DB_USERNAME=dootask
|
||||||
@ -33,7 +34,7 @@ SESSION_LIFETIME=120
|
|||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
REDIS_HOST="${APP_IPPR}.4"
|
REDIS_HOST=redis
|
||||||
REDIS_PASSWORD=null
|
REDIS_PASSWORD=null
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
@ -56,9 +57,6 @@ PUSHER_APP_KEY=
|
|||||||
PUSHER_APP_SECRET=
|
PUSHER_APP_SECRET=
|
||||||
PUSHER_APP_CLUSTER=mt1
|
PUSHER_APP_CLUSTER=mt1
|
||||||
|
|
||||||
JUKE_KEY_JOKE=
|
|
||||||
JUKE_KEY_SOUP=
|
|
||||||
|
|
||||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||||
|
|
||||||
|
|||||||
432
.github/workflows/ios-publish.yml
vendored
Normal file
432
.github/workflows/ios-publish.yml
vendored
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
name: "iOS Publish"
|
||||||
|
|
||||||
|
# Required GitHub Secrets:
|
||||||
|
#
|
||||||
|
# IOS_CERTIFICATE_BASE64 - Apple distribution certificate (.p12) encoded in base64
|
||||||
|
# IOS_CERTIFICATE_PASSWORD - Password for the .p12 certificate
|
||||||
|
# IOS_PROVISION_PROFILE_BASE64 - App Store provisioning profile (.mobileprovision) encoded in base64
|
||||||
|
# IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 - Share extension App Store provisioning profile (.mobileprovision) encoded in base64
|
||||||
|
# ASC_API_KEY_P8_BASE64 - App Store Connect API key (.p8) encoded in base64
|
||||||
|
# ASC_API_KEY_ID - App Store Connect API Key ID
|
||||||
|
# ASC_ISSUER_ID - App Store Connect Issuer ID
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ios-publish-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prepare-assets:
|
||||||
|
name: Prepare iOS Assets
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
outputs:
|
||||||
|
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
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Install electron dependencies
|
||||||
|
run: |
|
||||||
|
pushd electron
|
||||||
|
npm install
|
||||||
|
popd
|
||||||
|
|
||||||
|
- name: Init mobile submodule
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update --remote "resources/mobile"
|
||||||
|
|
||||||
|
- name: Build app assets
|
||||||
|
run: ./cmd appbuild publish
|
||||||
|
|
||||||
|
- name: Upload iOS platform artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ios-platform
|
||||||
|
path: resources/mobile/platforms/ios/
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
build-ios:
|
||||||
|
name: Build & Submit iOS
|
||||||
|
needs: prepare-assets
|
||||||
|
runs-on: macos-26
|
||||||
|
timeout-minutes: 60
|
||||||
|
environment: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Init mobile submodule
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update --remote "resources/mobile"
|
||||||
|
|
||||||
|
- name: Download prepared assets
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ios-platform
|
||||||
|
path: resources/mobile/platforms/ios/
|
||||||
|
|
||||||
|
- name: Select Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1
|
||||||
|
with:
|
||||||
|
xcode-version: latest-stable
|
||||||
|
|
||||||
|
- name: Install CocoaPods
|
||||||
|
run: |
|
||||||
|
if [ -f "resources/mobile/platforms/ios/eeuiApp/Podfile" ]; then
|
||||||
|
cd resources/mobile/platforms/ios/eeuiApp
|
||||||
|
pod install
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Import signing certificate
|
||||||
|
env:
|
||||||
|
IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
|
||||||
|
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# Create temporary keychain
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
KEYCHAIN_PASSWORD=$(openssl rand -hex 20)
|
||||||
|
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
# Import certificate
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
|
||||||
|
echo "$IOS_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH"
|
||||||
|
security import "$CERTIFICATE_PATH" \
|
||||||
|
-P "$IOS_CERTIFICATE_PASSWORD" \
|
||||||
|
-A \
|
||||||
|
-t cert \
|
||||||
|
-f pkcs12 \
|
||||||
|
-k "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
security list-keychain -d user -s "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
- name: Import provisioning profile
|
||||||
|
env:
|
||||||
|
IOS_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}
|
||||||
|
IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_PROFILE_PATH=$RUNNER_TEMP/app.mobileprovision
|
||||||
|
SHARE_PROFILE_PATH=$RUNNER_TEMP/share-extension.mobileprovision
|
||||||
|
APP_PROFILE_PLIST=$RUNNER_TEMP/app-profile.plist
|
||||||
|
SHARE_PROFILE_PLIST=$RUNNER_TEMP/share-extension-profile.plist
|
||||||
|
|
||||||
|
echo "$IOS_PROVISION_PROFILE_BASE64" | base64 --decode > "$APP_PROFILE_PATH"
|
||||||
|
echo "$IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64" | base64 --decode > "$SHARE_PROFILE_PATH"
|
||||||
|
|
||||||
|
security cms -D -i "$APP_PROFILE_PATH" > "$APP_PROFILE_PLIST"
|
||||||
|
security cms -D -i "$SHARE_PROFILE_PATH" > "$SHARE_PROFILE_PLIST"
|
||||||
|
|
||||||
|
APP_PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print :Name" "$APP_PROFILE_PLIST")
|
||||||
|
SHARE_PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print :Name" "$SHARE_PROFILE_PLIST")
|
||||||
|
IOS_TEAM_ID=$(/usr/libexec/PlistBuddy -c "Print :TeamIdentifier:0" "$APP_PROFILE_PLIST")
|
||||||
|
APP_PROFILE_APP_ID=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:application-identifier" "$APP_PROFILE_PLIST")
|
||||||
|
SHARE_PROFILE_APP_ID=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:application-identifier" "$SHARE_PROFILE_PLIST")
|
||||||
|
|
||||||
|
if [ "$APP_PROFILE_APP_ID" != "$IOS_TEAM_ID.com.dootask.task" ]; then
|
||||||
|
echo "Expected app profile for $IOS_TEAM_ID.com.dootask.task, got $APP_PROFILE_APP_ID"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SHARE_PROFILE_APP_ID" != "$IOS_TEAM_ID.com.dootask.task.shareExtension" ]; then
|
||||||
|
echo "Expected share extension profile for $IOS_TEAM_ID.com.dootask.task.shareExtension, got $SHARE_PROFILE_APP_ID"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! /usr/libexec/PlistBuddy -c "Print :Entitlements:aps-environment" "$APP_PROFILE_PLIST" >/dev/null; then
|
||||||
|
echo "The DooTask app profile must include Push Notifications."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! /usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.security.application-groups" "$APP_PROFILE_PLIST" | grep -q "group.im.dootask"; then
|
||||||
|
echo "The DooTask app profile must include App Group group.im.dootask."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! /usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.security.application-groups" "$SHARE_PROFILE_PLIST" | grep -q "group.im.dootask"; then
|
||||||
|
echo "The share extension profile must include App Group group.im.dootask."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
|
cp "$APP_PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
|
||||||
|
cp "$SHARE_PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
|
||||||
|
|
||||||
|
echo "APP_PROFILE_NAME=$APP_PROFILE_NAME" >> $GITHUB_ENV
|
||||||
|
echo "SHARE_PROFILE_NAME=$SHARE_PROFILE_NAME" >> $GITHUB_ENV
|
||||||
|
echo "IOS_TEAM_ID=$IOS_TEAM_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Configure manual signing
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ruby <<'RUBY'
|
||||||
|
require 'xcodeproj'
|
||||||
|
|
||||||
|
project_path = 'resources/mobile/platforms/ios/eeuiApp/eeuiApp.xcodeproj'
|
||||||
|
project = Xcodeproj::Project.open(project_path)
|
||||||
|
|
||||||
|
{
|
||||||
|
'DooTask' => ENV.fetch('APP_PROFILE_NAME'),
|
||||||
|
'ShareExtension' => ENV.fetch('SHARE_PROFILE_NAME')
|
||||||
|
}.each do |target_name, profile_name|
|
||||||
|
target = project.targets.find { |item| item.name == target_name }
|
||||||
|
abort "Target #{target_name} not found in #{project_path}" unless target
|
||||||
|
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
next unless config.name == 'Release'
|
||||||
|
|
||||||
|
config.build_settings['CODE_SIGN_STYLE'] = 'Manual'
|
||||||
|
config.build_settings['DEVELOPMENT_TEAM'] = ENV.fetch('IOS_TEAM_ID')
|
||||||
|
config.build_settings['CODE_SIGN_IDENTITY'] = 'Apple Distribution'
|
||||||
|
config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = profile_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
project.save
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
- name: Resolve iOS build number
|
||||||
|
env:
|
||||||
|
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
|
||||||
|
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
||||||
|
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ruby <<'RUBY'
|
||||||
|
require 'base64'
|
||||||
|
require 'json'
|
||||||
|
require 'net/http'
|
||||||
|
require 'openssl'
|
||||||
|
require 'uri'
|
||||||
|
|
||||||
|
BUNDLE_ID = 'com.dootask.task'
|
||||||
|
VERSION_CONFIG_PATH = 'resources/mobile/platforms/ios/eeuiApp/Config/Version.xcconfig'
|
||||||
|
|
||||||
|
def base64url(value)
|
||||||
|
Base64.urlsafe_encode64(value).delete('=')
|
||||||
|
end
|
||||||
|
|
||||||
|
def jwt_es256_signature(private_key, unsigned)
|
||||||
|
der_signature = private_key.sign('SHA256', unsigned)
|
||||||
|
sequence = OpenSSL::ASN1.decode(der_signature)
|
||||||
|
|
||||||
|
sequence.value.map { |integer|
|
||||||
|
integer.value.to_s(2).rjust(32, "\0")[-32, 32]
|
||||||
|
}.join
|
||||||
|
end
|
||||||
|
|
||||||
|
def asc_token
|
||||||
|
key_id = ENV.fetch('ASC_API_KEY_ID')
|
||||||
|
issuer_id = ENV.fetch('ASC_ISSUER_ID')
|
||||||
|
private_key = OpenSSL::PKey.read(Base64.decode64(ENV.fetch('ASC_API_KEY_P8_BASE64')))
|
||||||
|
now = Time.now.to_i
|
||||||
|
|
||||||
|
header = { alg: 'ES256', kid: key_id, typ: 'JWT' }
|
||||||
|
payload = {
|
||||||
|
iss: issuer_id,
|
||||||
|
iat: now,
|
||||||
|
exp: now + 20 * 60,
|
||||||
|
aud: 'appstoreconnect-v1'
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned = "#{base64url(header.to_json)}.#{base64url(payload.to_json)}"
|
||||||
|
signature = jwt_es256_signature(private_key, unsigned)
|
||||||
|
"#{unsigned}.#{base64url(signature)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def asc_get(path, params, token)
|
||||||
|
uri = URI::HTTPS.build(
|
||||||
|
host: 'api.appstoreconnect.apple.com',
|
||||||
|
path: path,
|
||||||
|
query: URI.encode_www_form(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
request_uri = uri
|
||||||
|
loop do
|
||||||
|
response = Net::HTTP.start(request_uri.host, request_uri.port, use_ssl: true) do |http|
|
||||||
|
request = Net::HTTP::Get.new(request_uri)
|
||||||
|
request['Authorization'] = "Bearer #{token}"
|
||||||
|
http.request(request)
|
||||||
|
end
|
||||||
|
|
||||||
|
unless response.is_a?(Net::HTTPSuccess)
|
||||||
|
abort "App Store Connect API request failed: #{response.code} #{response.body}"
|
||||||
|
end
|
||||||
|
|
||||||
|
parsed = JSON.parse(response.body)
|
||||||
|
yield parsed
|
||||||
|
|
||||||
|
next_link = parsed.dig('links', 'next')
|
||||||
|
break unless next_link
|
||||||
|
|
||||||
|
request_uri = URI(next_link)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
token = asc_token
|
||||||
|
app_id = nil
|
||||||
|
|
||||||
|
asc_get('/v1/apps', { 'filter[bundleId]' => BUNDLE_ID, 'limit' => 1 }, token) do |page|
|
||||||
|
app_id = page.fetch('data').first&.fetch('id')
|
||||||
|
end
|
||||||
|
|
||||||
|
abort "App Store Connect app not found for bundle id #{BUNDLE_ID}" unless app_id
|
||||||
|
|
||||||
|
existing_versions = []
|
||||||
|
asc_get('/v1/builds', {
|
||||||
|
'filter[app]' => app_id,
|
||||||
|
'fields[builds]' => 'version',
|
||||||
|
'limit' => 200
|
||||||
|
}, token) do |page|
|
||||||
|
existing_versions.concat(
|
||||||
|
page.fetch('data').map { |build| build.dig('attributes', 'version').to_s }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
max_build_number = existing_versions
|
||||||
|
.select { |version| version.match?(/\A\d+\z/) }
|
||||||
|
.map(&:to_i)
|
||||||
|
.max || 0
|
||||||
|
|
||||||
|
next_build_number = max_build_number + 1
|
||||||
|
config_content = File.exist?(VERSION_CONFIG_PATH) ? File.read(VERSION_CONFIG_PATH) : ''
|
||||||
|
|
||||||
|
if config_content.match?(/^VERSION_CODE\s*=/)
|
||||||
|
config_content = config_content.gsub(/^VERSION_CODE\s*=.*$/, "VERSION_CODE = #{next_build_number}")
|
||||||
|
else
|
||||||
|
config_content = "#{config_content.rstrip}\nVERSION_CODE = #{next_build_number}\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.write(VERSION_CONFIG_PATH, config_content)
|
||||||
|
File.open(ENV.fetch('GITHUB_ENV'), 'a') { |file| file.puts "IOS_BUILD_NUMBER=#{next_build_number}" }
|
||||||
|
|
||||||
|
puts "Latest App Store Connect build number: #{max_build_number}"
|
||||||
|
puts "Resolved iOS build number: #{next_build_number}"
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
- name: Build archive
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd resources/mobile/platforms/ios/eeuiApp
|
||||||
|
xcodebuild archive \
|
||||||
|
-workspace eeuiApp.xcworkspace \
|
||||||
|
-scheme eeuiApp \
|
||||||
|
-configuration Release \
|
||||||
|
-destination "generic/platform=iOS" \
|
||||||
|
-archivePath $RUNNER_TEMP/eeuiApp.xcarchive \
|
||||||
|
-allowProvisioningUpdates \
|
||||||
|
DEVELOPMENT_TEAM=$IOS_TEAM_ID \
|
||||||
|
CODE_SIGN_IDENTITY="Apple Distribution" \
|
||||||
|
CODE_SIGN_STYLE=Manual \
|
||||||
|
| xcpretty
|
||||||
|
|
||||||
|
if [ ! -d "$RUNNER_TEMP/eeuiApp.xcarchive" ]; then
|
||||||
|
echo "Archive was not created at $RUNNER_TEMP/eeuiApp.xcarchive"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Export IPA
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd resources/mobile/platforms/ios/eeuiApp
|
||||||
|
|
||||||
|
# Generate ExportOptions.plist
|
||||||
|
cat > $RUNNER_TEMP/ExportOptions.plist << PLIST
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>method</key>
|
||||||
|
<string>app-store</string>
|
||||||
|
<key>signingStyle</key>
|
||||||
|
<string>manual</string>
|
||||||
|
<key>teamID</key>
|
||||||
|
<string>${IOS_TEAM_ID}</string>
|
||||||
|
<key>provisioningProfiles</key>
|
||||||
|
<dict>
|
||||||
|
<key>com.dootask.task</key>
|
||||||
|
<string>${APP_PROFILE_NAME}</string>
|
||||||
|
<key>com.dootask.task.shareExtension</key>
|
||||||
|
<string>${SHARE_PROFILE_NAME}</string>
|
||||||
|
</dict>
|
||||||
|
<key>uploadBitcode</key>
|
||||||
|
<false/>
|
||||||
|
<key>uploadSymbols</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
PLIST
|
||||||
|
|
||||||
|
xcodebuild -exportArchive \
|
||||||
|
-archivePath $RUNNER_TEMP/eeuiApp.xcarchive \
|
||||||
|
-exportOptionsPlist $RUNNER_TEMP/ExportOptions.plist \
|
||||||
|
-exportPath $RUNNER_TEMP/ipa-output \
|
||||||
|
-allowProvisioningUpdates \
|
||||||
|
| xcpretty
|
||||||
|
|
||||||
|
- name: Submit to App Store Connect
|
||||||
|
env:
|
||||||
|
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
|
||||||
|
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
||||||
|
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Prepare API key
|
||||||
|
mkdir -p ~/private_keys
|
||||||
|
echo "$ASC_API_KEY_P8_BASE64" | base64 --decode > ~/private_keys/AuthKey_${ASC_API_KEY_ID}.p8
|
||||||
|
|
||||||
|
# Find and upload IPA
|
||||||
|
IPA_PATH=$(find $RUNNER_TEMP/ipa-output -name "*.ipa" | head -1)
|
||||||
|
if [ -z "$IPA_PATH" ]; then
|
||||||
|
echo "No IPA file found in $RUNNER_TEMP/ipa-output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Uploading: $IPA_PATH"
|
||||||
|
|
||||||
|
xcrun altool --upload-app \
|
||||||
|
-f "$IPA_PATH" \
|
||||||
|
--type ios \
|
||||||
|
--apiKey "$ASC_API_KEY_ID" \
|
||||||
|
--apiIssuer "$ASC_ISSUER_ID"
|
||||||
|
|
||||||
|
- name: Clean up
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db 2>/dev/null || true
|
||||||
|
rm -f $RUNNER_TEMP/certificate.p12
|
||||||
|
rm -f $RUNNER_TEMP/app.mobileprovision
|
||||||
|
rm -f $RUNNER_TEMP/share-extension.mobileprovision
|
||||||
|
rm -f $RUNNER_TEMP/app-profile.plist
|
||||||
|
rm -f $RUNNER_TEMP/share-extension-profile.plist
|
||||||
|
rm -rf ~/private_keys
|
||||||
33
.github/workflows/publish-desktop.yml
vendored
33
.github/workflows/publish-desktop.yml
vendored
@ -1,33 +0,0 @@
|
|||||||
name: Publish Desktop
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: macos-latest
|
|
||||||
environment: build
|
|
||||||
|
|
||||||
if: startsWith(github.event.ref, 'refs/tags/v')
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Use Node.js 16.x
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
APPLEID: ${{ secrets.APPLEID }}
|
|
||||||
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
DP_KEY: ${{ secrets.DP_KEY }}
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
GH_REPOSITORY: ${{ github.repository }}
|
|
||||||
run: ./cmd electron all
|
|
||||||
|
|
||||||
286
.github/workflows/publish.yml
vendored
Normal file
286
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
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: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const version = '${{ needs.check-version.outputs.version }}';
|
||||||
|
|
||||||
|
// 从 CHANGELOG.md 提取当前版本段落
|
||||||
|
let changelog = '';
|
||||||
|
const changelogPath = 'CHANGELOG.md';
|
||||||
|
if (fs.existsSync(changelogPath)) {
|
||||||
|
const content = fs.readFileSync(changelogPath, 'utf-8');
|
||||||
|
const regex = new RegExp(`## \\[${version.replace(/\./g, '\\.')}\\][\\s\\S]*?(?=\\n## \\[|$)`);
|
||||||
|
const match = content.match(regex);
|
||||||
|
if (match) {
|
||||||
|
changelog = match[0].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 release
|
||||||
|
const { data } = await github.rest.repos.createRelease({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
tag_name: `v${version}`,
|
||||||
|
name: 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:
|
||||||
|
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||||
|
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||||
|
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
|
||||||
|
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 }}
|
||||||
|
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||||
|
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||||
|
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
./cmd electron mac
|
||||||
|
|
||||||
|
# Windows 构建步骤
|
||||||
|
- name: (Windows) Build Client
|
||||||
|
if: matrix.build_type == 'windows'
|
||||||
|
env:
|
||||||
|
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||||
|
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||||
|
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
|
||||||
|
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: Upload Changelog & Publish to Website
|
||||||
|
env:
|
||||||
|
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||||
|
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||||
|
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
|
||||||
|
run: |
|
||||||
|
pushd electron || exit
|
||||||
|
npm install
|
||||||
|
popd || exit
|
||||||
|
node ./electron/build.js upload-changelog
|
||||||
|
node ./electron/build.js release
|
||||||
45
.github/workflows/sync-gitee.yml
vendored
Normal file
45
.github/workflows/sync-gitee.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
name: "Sync to Gitee"
|
||||||
|
|
||||||
|
# Required GitHub Secrets:
|
||||||
|
#
|
||||||
|
# GITEE_SSH_PRIVATE_KEY - SSH private key with push access to gitee.com/aipaw/dootask
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Publish"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
name: Push to Gitee
|
||||||
|
if: github.event.workflow_run.conclusion == 'success'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup SSH key
|
||||||
|
env:
|
||||||
|
GITEE_SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "$GITEE_SSH_PRIVATE_KEY" > ~/.ssh/gitee_key
|
||||||
|
chmod 600 ~/.ssh/gitee_key
|
||||||
|
cat >> ~/.ssh/config << EOF
|
||||||
|
Host gitee.com
|
||||||
|
HostName gitee.com
|
||||||
|
IdentityFile ~/.ssh/gitee_key
|
||||||
|
StrictHostKeyChecking no
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Push to Gitee
|
||||||
|
run: |
|
||||||
|
git remote add gitee git@gitee.com:aipaw/dootask.git
|
||||||
|
git push gitee pro
|
||||||
|
|
||||||
|
- name: Clean up
|
||||||
|
if: always()
|
||||||
|
run: rm -rf ~/.ssh/gitee_key
|
||||||
57
.gitignore
vendored
57
.gitignore
vendored
@ -1,28 +1,67 @@
|
|||||||
|
# Dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/vendor
|
||||||
|
|
||||||
|
# Build and temporary files
|
||||||
|
/build
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/tmp
|
/public/tmp
|
||||||
|
/tmp
|
||||||
|
/backup
|
||||||
|
|
||||||
|
# Uploads and user-generated content
|
||||||
|
/public/summary
|
||||||
/public/uploads/*
|
/public/uploads/*
|
||||||
/public/.well-known
|
/public/.well-known
|
||||||
/public/.user.ini
|
/public/.user.ini
|
||||||
/storage/*.key
|
|
||||||
|
# Storage and configuration
|
||||||
/config/LICENSE
|
/config/LICENSE
|
||||||
/vendor
|
/storage/*.key
|
||||||
/build
|
|
||||||
/tmp
|
# Environment and configuration
|
||||||
._*
|
|
||||||
.env
|
.env
|
||||||
|
vars.yaml
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
|
.cursor/*
|
||||||
|
!.cursor/rules/
|
||||||
|
!.cursor/rules/**
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
.windsurfrules
|
||||||
|
|
||||||
|
# Development tools
|
||||||
.vagrant
|
.vagrant
|
||||||
.phpunit.result.cache
|
|
||||||
Homestead.json
|
Homestead.json
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
|
|
||||||
|
# Development file
|
||||||
|
/index.html
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.phpunit.result.cache
|
||||||
|
test.*
|
||||||
|
|
||||||
|
# Logs and debug files
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
test.*
|
|
||||||
|
# Lock files
|
||||||
|
dootask.lock
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
|
# Laravel/Swoole specific
|
||||||
laravels-timer-process.pid
|
laravels-timer-process.pid
|
||||||
.DS_Store
|
|
||||||
vars.yaml
|
|
||||||
laravels.conf
|
laravels.conf
|
||||||
laravels.pid
|
laravels.pid
|
||||||
|
|
||||||
|
# System files
|
||||||
|
._*
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README_LOCAL.md
|
||||||
|
|
||||||
|
# playwright
|
||||||
|
.playwright-mcp/
|
||||||
|
|||||||
13
.gitpod.yml
13
.gitpod.yml
@ -1,13 +0,0 @@
|
|||||||
# This configuration file was automatically generated by Gitpod.
|
|
||||||
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
|
|
||||||
# and commit this file to your remote git repository to share the goodness with others.
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- init: sudo ./cmd install
|
|
||||||
command: ./cmd dev
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- port: 2222
|
|
||||||
visibility: public
|
|
||||||
- port: 22222
|
|
||||||
visibility: public
|
|
||||||
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
|
||||||
3789
CHANGELOG.md
3789
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
62
CLAUDE.md
Normal file
62
CLAUDE.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
## 项目概述
|
||||||
|
|
||||||
|
Laravel 8 (LaravelS/Swoole) + Vue 2 (Vite) + Electron。开源任务/项目管理系统。
|
||||||
|
|
||||||
|
## 开发命令
|
||||||
|
|
||||||
|
所有命令通过 `./cmd` 脚本执行(不要直接运行 `php artisan` 等):
|
||||||
|
|
||||||
|
- `./cmd artisan ...` / `./cmd composer ...` / `./cmd php ...` — PHP 相关命令
|
||||||
|
|
||||||
|
### AI 不要主动执行的命令
|
||||||
|
|
||||||
|
以下命令仅由用户人工触发,AI 不要主动跑——包括"任务完成后 sanity check"、"看下能不能编译"等场景:
|
||||||
|
|
||||||
|
- `./cmd dev` — 用户已自行运行 dev server,改完会自己 reload;AI 再跑会争抢进程
|
||||||
|
- `./cmd prod` / `./cmd build` — 发版才用,走 `/release` 流程
|
||||||
|
|
||||||
|
前端代码改动只做 Edit/Write,不要为了"验证"启动 dev server。用户明确说"跑一下 / 出包"时除外。
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
### LaravelS/Swoole
|
||||||
|
|
||||||
|
- **避免在静态属性、单例、全局变量中存储请求级状态**——请求间共享进程,会导致数据串联和内存泄漏
|
||||||
|
- 构造函数、服务提供者、`boot()` 方法不会在每个请求重新执行
|
||||||
|
- 配置/路由变更需要 `./cmd php restart` 或容器重启才能生效
|
||||||
|
- 长生命周期逻辑(WebSocket、定时器)应复用现有模式,避免阻塞协程/事件循环
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
|
||||||
|
- **非 REST 路由**:API 控制器(继承 `InvokeController`)在 `routes/web.php` 按资源注册路由,URL 段映射为控制器方法(如 `api/project/lists` → `lists()`,带 action 则用双下划线:`api/project/invite/join` → `invite__join()`)
|
||||||
|
- 路由最多两段:方法名最多一个双下划线(`method__action`),不支持 `method__action__xxx`(无对应路由,访问 404)
|
||||||
|
- **响应格式**:统一使用 `Base::retSuccess($msg, $data)` / `Base::retError($msg)`,返回 `{"ret": 1, "msg": "...", "data": {...}}`——不要用 `response()->json()`
|
||||||
|
- 业务异常通过 `App\Exceptions\ApiException` 抛出,不要用通用 Exception
|
||||||
|
- 模型继承 `AbstractModel`,使用 `Model::createInstance($params)` 创建——不要用 `new Model()` 或 `Model::create()`
|
||||||
|
- 认证使用 `Doo::userId()`——不要用 `auth()->user()`
|
||||||
|
- 参数校验在控制器方法中手动进行——不要创建 FormRequest 类
|
||||||
|
- 异步任务使用 Swoole Task(`app/Tasks/`)——不要用 Laravel Queue
|
||||||
|
- `app/Module/` 存放跨控制器/跨模型的业务逻辑(非标准 Laravel 目录)
|
||||||
|
- 所有表结构变更必须通过 Laravel migration,禁止直接改库
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
|
||||||
|
- API 调用使用 `store.dispatch("call", params)`,不要在组件中直接 axios/fetch
|
||||||
|
- `$A.modalXXX`、`$A.messageXXX`、`$A.noticeXXX` 内部自动处理 `$L` 翻译,调用方不要额外包 `$L`。仅当传入 `language: false` 时由调用方自行处理翻译
|
||||||
|
|
||||||
|
### 国际化
|
||||||
|
|
||||||
|
- 新增用户可见文本须追加原文(简体中文)到:前端 `language/original-web.txt`,后端 `language/original-api.txt`(去重)
|
||||||
|
- 前端翻译用 `$L("文本")`,动态值用 `(*)` 占位:`$L('共(*)条', n)`——禁止拼接翻译
|
||||||
|
|
||||||
|
## Playwright 测试
|
||||||
|
|
||||||
|
- Playwright 测试结果放在 `tests/playwright-results/`,包含测试环境、测试用例、结果截图等信息
|
||||||
|
|
||||||
|
## 交互规范
|
||||||
|
|
||||||
|
- **提问时附带建议**:当需要向用户提问或请求澄清时,应同时提供具体的建议选项或推荐方案,帮助用户快速决策,而非仅抛出开放式问题
|
||||||
|
|
||||||
|
## 语言偏好
|
||||||
|
|
||||||
|
- 回复一律使用简体中文,除非用户明确要求其他语言
|
||||||
146
README.md
146
README.md
@ -1,146 +1,150 @@
|
|||||||
# Install (Docker)
|
# DooTask - Open Source Task Management System
|
||||||
|
|
||||||
English | **[中文文档](./README_CN.md)**
|
English | **[中文文档](./README_CN.md)**
|
||||||
|
|
||||||
- [Screenshot Preview](README_PREVIEW.md)
|
- [Screenshot Preview](./README_PREVIEW.md)
|
||||||
- [Demo site](http://www.dootask.com/)
|
- [Demo Site](http://www.dootask.com/)
|
||||||
|
|
||||||
**QQ Group**
|
**QQ Group**
|
||||||
|
|
||||||
Group No.: `546574618`
|
- Group Number: `546574618`
|
||||||
|
|
||||||
## Setup
|
## 📍 Migration from 0.x to 1.x
|
||||||
|
|
||||||
- `Docker v20.10+` & `Docker Compose v2.0+` must be installed
|
- Please ensure to back up your data before upgrading!
|
||||||
- System: `Centos/Debian/Ubuntu/macOS`
|
- If the upgrade fails, try running `./cmd update` multiple times.
|
||||||
- Hardware suggestion: 2 cores and above 4G memory
|
- If you encounter "Container xxx not found" during upgrade, run `./cmd reup` and then execute `./cmd update`.
|
||||||
|
- If you see a 502 error after upgrading, run `./cmd reup` to restart the services.
|
||||||
|
- If you encounter "Application 'xxx' not installed" after upgrading, log in with the admin account and install the relevant applications from the App Store.
|
||||||
|
|
||||||
### Deployment (Pro Edition)
|
## Installation Requirements
|
||||||
|
|
||||||
|
- Required: `Docker v20.10+` and `Docker Compose v2.0+`
|
||||||
|
- Supported Systems: `CentOS/Debian/Ubuntu/macOS` and other Linux/Unix systems
|
||||||
|
- Hardware Recommendation: 2+ cores, 4GB+ memory
|
||||||
|
- Database: MariaDB (provided by the default Docker Compose `mariadb` service)
|
||||||
|
- Special Note: Windows users can install Linux environment using WSL2 before installing DooTask.
|
||||||
|
|
||||||
|
### Deploy Project
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1、Clone the repository
|
# 1、Clone the project to your local machine or server
|
||||||
|
|
||||||
# Clone projects on github
|
# Clone project from GitHub
|
||||||
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
|
git clone --depth=1 https://github.com/kuaifan/dootask.git
|
||||||
# Or you can use gitee
|
# Or you can use Gitee
|
||||||
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
|
git clone --depth=1 https://gitee.com/aipaw/dootask.git
|
||||||
|
|
||||||
# 2、Enter directory
|
# 2、Enter directory
|
||||||
cd dootask
|
cd dootask
|
||||||
|
|
||||||
# 3、Installation(Custom port installation: ./cmd install --port 2222)
|
# 3、One-click installation (Custom port installation: ./cmd install --port 80)
|
||||||
./cmd install
|
./cmd install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Reset password
|
### Reset Password
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Reset default account password
|
# Reset default administrator password
|
||||||
./cmd repassword
|
./cmd repassword
|
||||||
```
|
```
|
||||||
|
|
||||||
### Change port
|
### Change Port
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./cmd port 2222
|
# This method only changes HTTP port. For HTTPS port, please read SSL configuration below
|
||||||
|
./cmd port 80
|
||||||
```
|
```
|
||||||
|
|
||||||
### Change App Url
|
### Stop Service
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# This URL only affects the email reply.
|
./cmd down
|
||||||
./cmd url {Your domain url}
|
|
||||||
|
|
||||||
# example:
|
|
||||||
./cmd url https://domain.com
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Stop server
|
### Start Service
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./cmd stop
|
./cmd up
|
||||||
|
|
||||||
# P.S: Once application is set up, whenever you want to start the server (if it is stopped) run below command
|
|
||||||
./cmd start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development compilation
|
### Development & Build
|
||||||
|
|
||||||
|
Please ensure you have installed `NodeJs 20+`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development mode, Mac OS only
|
# Development mode
|
||||||
./cmd dev
|
./cmd dev
|
||||||
|
|
||||||
# Production projects, macOS only
|
# Build project (This is for web client. For desktop apps, refer to ".github/workflows/publish.yml")
|
||||||
./cmd prod
|
./cmd prod
|
||||||
```
|
```
|
||||||
|
|
||||||
### Shortcuts for running command
|
### SSL Configuration
|
||||||
|
|
||||||
```bash
|
#### Method 1: Automatic Configuration
|
||||||
# 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
|
|
||||||
|
|
||||||
```bash
|
```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-Host $http_host;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
# 2、Running commands in a project
|
# 2、Run command (To cancel Nginx proxy configuration: ./cmd https close)
|
||||||
./cmd https
|
./cmd https agent
|
||||||
```
|
```
|
||||||
|
|
||||||
## Upgrade
|
## Upgrade & Update
|
||||||
|
|
||||||
**Note: Please back up your data before upgrading!**
|
**Note: Please backup your data before upgrading!**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Method 1: Running commands in a project
|
|
||||||
./cmd update
|
./cmd update
|
||||||
|
|
||||||
# Or method 2: use this method if method 1 fails
|
|
||||||
git pull
|
|
||||||
./cmd mysql backup
|
|
||||||
./cmd uninstall
|
|
||||||
./cmd install
|
|
||||||
./cmd mysql recovery
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* Please try again if the upgrade fails across a large version.
|
* Please retry if upgrade fails across major versions.
|
||||||
* If 502 after the upgrade please run `./cmd restart` restart the service.
|
* If you encounter 502 errors after upgrade, run `./cmd reup` to restart services.
|
||||||
|
|
||||||
## Transfer
|
## Project Migration
|
||||||
|
|
||||||
Follow these steps to complete the project migration after the new project is installed:
|
After installing the new project, follow these steps to complete migration:
|
||||||
|
|
||||||
1. Backup original database
|
1、Backup the MariaDB database
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run command under old project
|
# Run command in the old project
|
||||||
./cmd mysql backup
|
./cmd mysql backup
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Copy `database backup file` and `public/uploads` directory to the new project.
|
> `./cmd mysql` is the CLI subcommand name; backups run against the MariaDB container.
|
||||||
|
|
||||||
3. Restore database to new project
|
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
|
```bash
|
||||||
# Run command under new project
|
# Run command in the new project
|
||||||
./cmd mysql recovery
|
./cmd mysql recovery
|
||||||
```
|
```
|
||||||
|
|
||||||
## Uninstall
|
## Uninstall Project
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Running commands in a project
|
|
||||||
./cmd uninstall
|
./cmd uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### More Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cmd help
|
||||||
|
```
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
# 客户端说明
|
|
||||||
|
|
||||||
## 1、App客户端
|
|
||||||
|
|
||||||
#### 1.1、说明
|
|
||||||
目录 `resources/mobile`,使用`eeui.app`框架,遵从eeui的开发文档进行打包开发app
|
|
||||||
|
|
||||||
#### 1.2、编译App
|
|
||||||
1. 在项目目录执行 `./cmd appbuild` 编译
|
|
||||||
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用
|
|
||||||
|
|
||||||
|
|
||||||
## 2、PC/Mac客户端
|
|
||||||
|
|
||||||
#### 2.1、说明
|
|
||||||
目录 `electron`,使用`electron`框架,遵从electron的开发文档进行打包客户端
|
|
||||||
|
|
||||||
#### 2.2、编译客户端
|
|
||||||
在项目目录执行 `./cmd electron` 根据提示编译
|
|
||||||
|
|
||||||
|
|
||||||
105
README_CN.md
105
README_CN.md
@ -1,34 +1,44 @@
|
|||||||
# Install (Docker)
|
# DooTask - 开源任务管理系统
|
||||||
|
|
||||||
**[English](./README.md)** | 中文文档
|
**[English](./README.md)** | 中文文档
|
||||||
|
|
||||||
- [截图预览](README_PREVIEW.md)
|
- [截图预览](./README_PREVIEW.md)
|
||||||
- [演示站点](http://www.dootask.com/)
|
- [演示站点](http://www.dootask.com/)
|
||||||
|
|
||||||
**QQ交流群**
|
**QQ交流群**
|
||||||
|
|
||||||
- QQ群号: `546574618`
|
- QQ群号: `546574618`
|
||||||
|
|
||||||
|
## 📍 0.x 迁移到 1.x
|
||||||
|
|
||||||
|
- 升级时请务必备份好数据!
|
||||||
|
- 如果升级失败请尝试执行 `./cmd update` 重试几次。
|
||||||
|
- 如果升级中出现 `没有找到 xxx 容器` 的提示,请运行 `./cmd reup` 后再执行 `./cmd update`。
|
||||||
|
- 如果升级后出现502错误请运行 `./cmd reup` 重启服务即可。
|
||||||
|
- 如果升级后出现 `应用「xxx」未安装` 的提示,请使用管理员账号进入应用商店安装相关应用。
|
||||||
|
|
||||||
## 安装程序
|
## 安装程序
|
||||||
|
|
||||||
- 必须安装:`Docker v20.10+` 和 `Docker Compose v2.0+`
|
- 必须安装:`Docker v20.10+` 和 `Docker Compose v2.0+`
|
||||||
- 支持环境:`Centos/Debian/Ubuntu/macOS`
|
- 支持环境:`Centos/Debian/Ubuntu/macOS` 等 linux/unix 系统
|
||||||
- 硬件建议:2核4G以上
|
- 硬件建议:2核4G以上
|
||||||
|
- 数据库:MariaDB(默认 Docker Compose 中的 `mariadb` 服务)
|
||||||
|
- 特别说明:Windows 可以使用 WSL2 安装 Linux 环境后再安装 DooTask。
|
||||||
|
|
||||||
### 部署项目(Pro版)
|
### 部署项目
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1、克隆项目到您的本地或服务器
|
# 1、克隆项目到您的本地或服务器
|
||||||
|
|
||||||
# 通过github克隆项目
|
# 通过github克隆项目
|
||||||
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
|
git clone --depth=1 https://github.com/kuaifan/dootask.git
|
||||||
# 或者你也可以使用gitee
|
# 或者你也可以使用gitee
|
||||||
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
|
git clone --depth=1 https://gitee.com/aipaw/dootask.git
|
||||||
|
|
||||||
# 2、进入目录
|
# 2、进入目录
|
||||||
cd dootask
|
cd dootask
|
||||||
|
|
||||||
# 3、一键安装项目(自定义端口安装 ./cmd install --port 2222)
|
# 3、一键安装项目(自定义端口安装,如:./cmd install --port 80)
|
||||||
./cmd install
|
./cmd install
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -42,54 +52,44 @@ cd dootask
|
|||||||
### 更换端口
|
### 更换端口
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./cmd port 2222
|
# 此方法仅更换http端口,更换https端口请阅读下面SSL配置
|
||||||
```
|
./cmd port 80
|
||||||
|
|
||||||
### 更换URL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 此地址仅影响邮件回复功能
|
|
||||||
./cmd url {域名地址}
|
|
||||||
|
|
||||||
# 例如:
|
|
||||||
./cmd url https://domain.com
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 停止服务
|
### 停止服务
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./cmd stop
|
./cmd down
|
||||||
|
```
|
||||||
|
|
||||||
# 一旦应用程序被设置,无论何时你想要启动服务器(如果它被停止)运行以下命令
|
### 启动服务
|
||||||
./cmd start
|
|
||||||
|
```bash
|
||||||
|
./cmd up
|
||||||
```
|
```
|
||||||
|
|
||||||
### 开发编译
|
### 开发编译
|
||||||
|
|
||||||
|
请确保你已经安装了 `NodeJs 20+`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 开发模式,仅限macOS
|
# 开发模式
|
||||||
./cmd dev
|
./cmd dev
|
||||||
|
|
||||||
# 编译项目,仅限macOS
|
# 编译项目(这是网页端的,客户端请参考“.github/workflows/publish.yml”文件)
|
||||||
./cmd prod
|
./cmd prod
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### SSL 配置
|
||||||
|
|
||||||
### 运行命令的快捷方式
|
#### 方法1:自动配置
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 你可以使用以下命令来执行
|
# 执行指令,根据提示执行即可
|
||||||
./cmd artisan "your command" # 运行 artisan 命令
|
./cmd https
|
||||||
./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: 还原数据库)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### NGINX 代理 SSL
|
#### 方法2:Nginx 代理配置
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1、Nginx 代理配置添加
|
# 1、Nginx 代理配置添加
|
||||||
@ -97,8 +97,8 @@ proxy_set_header X-Forwarded-Host $http_host;
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
# 2、在项目下运行命令
|
# 2、执行指令(如果取消 Nginx 代理配置请运行:./cmd https close)
|
||||||
./cmd https
|
./cmd https agent
|
||||||
```
|
```
|
||||||
|
|
||||||
## 升级更新
|
## 升级更新
|
||||||
@ -106,42 +106,45 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|||||||
**注意:在升级之前请备份好你的数据!**
|
**注意:在升级之前请备份好你的数据!**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 方法1:在项目下运行命令
|
|
||||||
./cmd update
|
./cmd update
|
||||||
|
|
||||||
# (或者)方法2:如果方法1失败请使用此方法
|
|
||||||
git pull
|
|
||||||
./cmd mysql backup
|
|
||||||
./cmd uninstall
|
|
||||||
./cmd install
|
|
||||||
./cmd mysql recovery
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* 跨越大版本升级失败时请重试执行一次。
|
* 跨越大版本升级失败时请重试执行一次。
|
||||||
* 如果升级后出现502请运行 `./cmd restart` 重启服务即可。
|
* 如果升级后出现502请运行 `./cmd reup` 重启服务即可。
|
||||||
|
|
||||||
## 迁移项目
|
## 迁移项目
|
||||||
|
|
||||||
在新项目安装好之后按照以下步骤完成项目迁移:
|
在新项目安装好之后按照以下步骤完成项目迁移:
|
||||||
|
|
||||||
1、备份原数据库
|
1、备份 MariaDB 数据库
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 在旧的项目下运行命令
|
# 在旧的项目下执行指令
|
||||||
./cmd mysql backup
|
./cmd mysql backup
|
||||||
```
|
```
|
||||||
|
|
||||||
2、将`数据库备份文件`及`public/uploads`目录拷贝至新项目
|
> `./cmd mysql` 为 CLI 子命令名称,实际操作的是 MariaDB 容器。
|
||||||
|
|
||||||
|
2、将旧项目以下文件和目录拷贝至新项目同路径位置
|
||||||
|
|
||||||
|
- `数据库备份文件`
|
||||||
|
- `docker/appstore`
|
||||||
|
- `public/uploads`
|
||||||
|
|
||||||
3、还原数据库至新项目
|
3、还原数据库至新项目
|
||||||
```bash
|
```bash
|
||||||
# 在新的项目下运行命令
|
# 在新的项目下执行指令
|
||||||
./cmd mysql recovery
|
./cmd mysql recovery
|
||||||
```
|
```
|
||||||
|
|
||||||
## 卸载项目
|
## 卸载项目
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 在项目下运行命令
|
|
||||||
./cmd uninstall
|
./cmd uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 更多指令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cmd help
|
||||||
|
```
|
||||||
|
|||||||
@ -1,26 +1,31 @@
|
|||||||
# 发布说明
|
# 发布
|
||||||
|
|
||||||
## 发布前
|
## 准备工作
|
||||||
|
|
||||||
1. 添加环境变量 `APPLEID`、`APPLEIDPASS` 用于公证
|
1. 添加环境变量 `APPLEID`、`APPLEIDPASS` 用于公证
|
||||||
2. 添加环境变量 `CSC_LINK`、`CSC_KEY_PASSWORD` 用于签名
|
2. 添加环境变量 `CSC_LINK`、`CSC_KEY_PASSWORD` 用于签名
|
||||||
3. 添加环境变量 `GH_TOKEN`、`GH_REPOSITORY` 用于发布到GitHub
|
3. 添加环境变量 `GITHUB_TOKEN`、`GITHUB_REPOSITORY` 用于发布到GitHub(GitHub Actions 发布不需要)
|
||||||
4. 添加环境变量 `DP_KEY` 用于发布到私有服务器
|
4. 添加环境变量 `PUBLISH_KEY` 用于发布到私有服务器
|
||||||
|
|
||||||
## 通过 GitHub Actions 发布
|
## 发布版本
|
||||||
|
|
||||||
1. 执行 `npm run version` 生成版本
|
> 翻译、版本号、更新日志改由 `dootask-release` 技能完成(见 `.claude/skills/dootask-release/`)。
|
||||||
2. 执行 `npm run build` 编译前端
|
|
||||||
3. 执行 `git commit` 提交并推送
|
|
||||||
4. 添加并推送标签
|
|
||||||
|
|
||||||
## 本地发布
|
```shell
|
||||||
|
npm run build # 编译前端
|
||||||
|
```
|
||||||
|
|
||||||
1. 执行 `npm run version` 生成版本
|
说明:
|
||||||
2. 执行 `npm run build` 编译前端
|
|
||||||
3. 执行 `./cmd electron` 相关操作
|
|
||||||
|
|
||||||
## 编译App
|
- 执行 `npm run build` 作用是生成网页端;
|
||||||
|
- 客户端 (Windows、Mac、Android) 会通过 GitHub Actions 自动生成并发布;所以,如果要自动发布只需要提交git并推送即可;
|
||||||
|
- 如果想手动生成客户端执行 `./cmd electron` 根据提示选择操作。
|
||||||
|
|
||||||
1. 执行 `./cmd appbuild` 或 `./cmd appbuild setting` 编译
|
|
||||||
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用
|
## 编译 App
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./cmd appbuild publish # 编译生成App需要的资源
|
||||||
|
```
|
||||||
|
|
||||||
|
编译完后进入 `resources/mobile` EEUI框架目录内打包 Android 或 iOS 应用(Android 以实现 GitHub Actions 自动发布)
|
||||||
|
|||||||
323
_ide_helper.php
323
_ide_helper.php
@ -16045,7 +16045,7 @@
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @see \Maatwebsite\Excel\Mixins\DownloadCollection::downloadExcel()
|
* @see \Maatwebsite\Excel\Mixins\DownloadCollectionMixin::downloadExcel()
|
||||||
* @param string $fileName
|
* @param string $fileName
|
||||||
* @param string|null $writerType
|
* @param string|null $writerType
|
||||||
* @param mixed $withHeadings
|
* @param mixed $withHeadings
|
||||||
@ -16059,7 +16059,7 @@
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @see \Maatwebsite\Excel\Mixins\StoreCollection::storeExcel()
|
* @see \Maatwebsite\Excel\Mixins\StoreCollectionMixin::storeExcel()
|
||||||
* @param string $filePath
|
* @param string $filePath
|
||||||
* @param string|null $disk
|
* @param string|null $disk
|
||||||
* @param string|null $writerType
|
* @param string|null $writerType
|
||||||
@ -16439,6 +16439,247 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Laravolt\Avatar {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class Facade {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setGenerator($generator)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setGenerator($generator);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function create($name)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->create($name);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function applyTheme($config)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->applyTheme($config);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function addTheme($name, $config)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->addTheme($name, $config);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function toBase64()
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->toBase64();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function save($path, $quality = 90)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->save($path, $quality);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function toSvg()
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->toSvg();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function toGravatar($param = null)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->toGravatar($param);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function getInitial()
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->getInitial();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function getImageObject()
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->getImageObject();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function buildAvatar()
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->buildAvatar();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function getAttribute($key)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->getAttribute($key);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setTheme($theme)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setTheme($theme);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setBackground($hex)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setBackground($hex);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setForeground($hex)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setForeground($hex);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setDimension($width, $height = null)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setDimension($width, $height);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setFontSize($size)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setFontSize($size);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setFontFamily($font)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setFontFamily($font);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setBorder($size, $color, $radius = 0)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setBorder($size, $color, $radius);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setBorderRadius($radius)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setBorderRadius($radius);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setShape($shape)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setShape($shape);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setChars($chars)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setChars($chars);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function setFont($font)
|
||||||
|
{
|
||||||
|
/** @var \Laravolt\Avatar\Avatar $instance */
|
||||||
|
return $instance->setFont($font);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Maatwebsite\Excel\Facades {
|
namespace Maatwebsite\Excel\Facades {
|
||||||
@ -16467,9 +16708,10 @@
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
|
* @param string|null $disk Fallback for usage with named properties
|
||||||
* @param object $export
|
* @param object $export
|
||||||
* @param string $filePath
|
* @param string $filePath
|
||||||
* @param string|null $disk
|
* @param string|null $diskName
|
||||||
* @param string $writerType
|
* @param string $writerType
|
||||||
* @param mixed $diskOptions
|
* @param mixed $diskOptions
|
||||||
* @return bool
|
* @return bool
|
||||||
@ -16477,10 +16719,10 @@
|
|||||||
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
public static function store($export, $filePath, $diskName = null, $writerType = null, $diskOptions = [])
|
public static function store($export, $filePath, $diskName = null, $writerType = null, $diskOptions = [], $disk = null)
|
||||||
{
|
{
|
||||||
/** @var \Maatwebsite\Excel\Excel $instance */
|
/** @var \Maatwebsite\Excel\Excel $instance */
|
||||||
return $instance->store($export, $filePath, $diskName, $writerType, $diskOptions);
|
return $instance->store($export, $filePath, $diskName, $writerType, $diskOptions, $disk);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -16698,7 +16940,7 @@
|
|||||||
* @param $pathToFile string The file to open
|
* @param $pathToFile string The file to open
|
||||||
* @param \Madnest\Madzipper\Repositories\RepositoryInterface|string $type The type of the archive, defaults to zip, possible are zip, phar
|
* @param \Madnest\Madzipper\Repositories\RepositoryInterface|string $type The type of the archive, defaults to zip, possible are zip, phar
|
||||||
* @throws \RuntimeException
|
* @throws \RuntimeException
|
||||||
* @throws \Exception
|
* @throws Exception
|
||||||
* @throws \InvalidArgumentException
|
* @throws \InvalidArgumentException
|
||||||
* @return \Madnest\Madzipper\Madzipper Madzipper instance
|
* @return \Madnest\Madzipper\Madzipper Madzipper instance
|
||||||
* @static
|
* @static
|
||||||
@ -16712,7 +16954,7 @@
|
|||||||
* Create a new zip archive or open an existing one.
|
* Create a new zip archive or open an existing one.
|
||||||
*
|
*
|
||||||
* @param string $pathToFile
|
* @param string $pathToFile
|
||||||
* @throws \Exception
|
* @throws Exception
|
||||||
* @return self
|
* @return self
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
@ -16725,7 +16967,7 @@
|
|||||||
* Create a new phar file or open one.
|
* Create a new phar file or open one.
|
||||||
*
|
*
|
||||||
* @param string $pathToFile
|
* @param string $pathToFile
|
||||||
* @throws \Exception
|
* @throws Exception
|
||||||
* @return self
|
* @return self
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
@ -16738,7 +16980,7 @@
|
|||||||
* Create a new rar file or open one.
|
* Create a new rar file or open one.
|
||||||
*
|
*
|
||||||
* @param string $pathToFile
|
* @param string $pathToFile
|
||||||
* @throws \Exception
|
* @throws Exception
|
||||||
* @return self
|
* @return self
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
@ -16755,7 +16997,7 @@
|
|||||||
* @param $path string The path to extract to
|
* @param $path string The path to extract to
|
||||||
* @param array $files An array of files
|
* @param array $files An array of files
|
||||||
* @param int $methodFlags The Method the files should be treated
|
* @param int $methodFlags The Method the files should be treated
|
||||||
* @throws \Exception
|
* @throws Exception
|
||||||
* @return void
|
* @return void
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
@ -16782,7 +17024,7 @@
|
|||||||
* Gets the content of a single file if available.
|
* Gets the content of a single file if available.
|
||||||
*
|
*
|
||||||
* @param $filePath string The full path (including all folders) of the file in the zip
|
* @param $filePath string The full path (including all folders) of the file in the zip
|
||||||
* @throws \Exception
|
* @throws Exception
|
||||||
* @return mixed returns the content or throws an exception
|
* @return mixed returns the content or throws an exception
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
@ -18774,6 +19016,64 @@ namespace {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
|
* @see \Maatwebsite\Excel\Mixins\DownloadQueryMacro::__invoke()
|
||||||
|
* @param string $fileName
|
||||||
|
* @param string|null $writerType
|
||||||
|
* @param mixed $withHeadings
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function downloadExcel($fileName, $writerType = null, $withHeadings = false)
|
||||||
|
{
|
||||||
|
return \Illuminate\Database\Eloquent\Builder::downloadExcel($fileName, $writerType, $withHeadings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @see \Maatwebsite\Excel\Mixins\StoreQueryMacro::__invoke()
|
||||||
|
* @param string $filePath
|
||||||
|
* @param string|null $disk
|
||||||
|
* @param string|null $writerType
|
||||||
|
* @param mixed $withHeadings
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function storeExcel($filePath, $disk = null, $writerType = null, $withHeadings = false)
|
||||||
|
{
|
||||||
|
return \Illuminate\Database\Eloquent\Builder::storeExcel($filePath, $disk, $writerType, $withHeadings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @see \Maatwebsite\Excel\Mixins\ImportMacro::__invoke()
|
||||||
|
* @param string $filename
|
||||||
|
* @param string|null $disk
|
||||||
|
* @param string|null $readerType
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function import($filename, $disk = null, $readerType = null)
|
||||||
|
{
|
||||||
|
return \Illuminate\Database\Eloquent\Builder::import($filename, $disk, $readerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @see \Maatwebsite\Excel\Mixins\ImportAsMacro::__invoke()
|
||||||
|
* @param string $filename
|
||||||
|
* @param callable $mapping
|
||||||
|
* @param string|null $disk
|
||||||
|
* @param string|null $readerType
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
public static function importAs($filename, $mapping, $disk = null, $readerType = null)
|
||||||
|
{
|
||||||
|
return \Illuminate\Database\Eloquent\Builder::importAs($filename, $mapping, $disk, $readerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
* @see \App\Providers\AppServiceProvider::boot()
|
* @see \App\Providers\AppServiceProvider::boot()
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
@ -20814,6 +21114,7 @@ namespace {
|
|||||||
class View extends \Illuminate\Support\Facades\View {}
|
class View extends \Illuminate\Support\Facades\View {}
|
||||||
class Flare extends \Facade\Ignition\Facades\Flare {}
|
class Flare extends \Facade\Ignition\Facades\Flare {}
|
||||||
class Image extends \Intervention\Image\Facades\Image {}
|
class Image extends \Intervention\Image\Facades\Image {}
|
||||||
|
class Avatar extends \Laravolt\Avatar\Facade {}
|
||||||
class Excel extends \Maatwebsite\Excel\Facades\Excel {}
|
class Excel extends \Maatwebsite\Excel\Facades\Excel {}
|
||||||
class Madzipper extends \Madnest\Madzipper\Facades\Madzipper {}
|
class Madzipper extends \Madnest\Madzipper\Facades\Madzipper {}
|
||||||
class Captcha extends \Mews\Captcha\Facades\Captcha {}
|
class Captcha extends \Mews\Captcha\Facades\Captcha {}
|
||||||
|
|||||||
205
app/Console/Commands/GenerateManticoreVectors.php
Normal file
205
app/Console/Commands/GenerateManticoreVectors.php
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Console\Commands\Traits\ManticoreSyncLock;
|
||||||
|
use App\Models\File;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectTask;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Manticore\ManticoreFile;
|
||||||
|
use App\Module\Manticore\ManticoreKeyValue;
|
||||||
|
use App\Module\Manticore\ManticoreMsg;
|
||||||
|
use App\Module\Manticore\ManticoreProject;
|
||||||
|
use App\Module\Manticore\ManticoreTask;
|
||||||
|
use App\Module\Manticore\ManticoreUser;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步向量生成命令
|
||||||
|
*
|
||||||
|
* 用于后台批量生成已索引数据的向量,与全文索引解耦
|
||||||
|
* 使用双指针追踪:sync:xxxLastId(全文已同步)和 vector:xxxLastId(向量已生成)
|
||||||
|
*
|
||||||
|
* 运行模式:
|
||||||
|
* - 持续处理直到所有待处理数据完成
|
||||||
|
* - 每批处理完成后休眠几秒,避免 API 过载
|
||||||
|
* - 定时器只作为兜底触发机制
|
||||||
|
*/
|
||||||
|
class GenerateManticoreVectors extends Command
|
||||||
|
{
|
||||||
|
use ManticoreSyncLock;
|
||||||
|
|
||||||
|
protected $signature = 'manticore:generate-vectors
|
||||||
|
{--type=all : 类型 (msg/file/task/project/user/all)}
|
||||||
|
{--batch=50 : 每批 embedding 数量}
|
||||||
|
{--sleep=3 : 每批处理后休眠秒数}
|
||||||
|
{--reset : 重置向量进度指针}';
|
||||||
|
|
||||||
|
protected $description = '批量生成 Manticore 已索引数据的向量';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型配置
|
||||||
|
*/
|
||||||
|
private const TYPE_CONFIG = [
|
||||||
|
'msg' => [
|
||||||
|
'syncKey' => 'sync:manticoreMsgLastId',
|
||||||
|
'vectorKey' => 'vector:manticoreMsgLastId',
|
||||||
|
'class' => ManticoreMsg::class,
|
||||||
|
'model' => WebSocketDialogMsg::class,
|
||||||
|
'idField' => 'id',
|
||||||
|
],
|
||||||
|
'file' => [
|
||||||
|
'syncKey' => 'sync:manticoreFileLastId',
|
||||||
|
'vectorKey' => 'vector:manticoreFileLastId',
|
||||||
|
'class' => ManticoreFile::class,
|
||||||
|
'model' => File::class,
|
||||||
|
'idField' => 'id',
|
||||||
|
],
|
||||||
|
'task' => [
|
||||||
|
'syncKey' => 'sync:manticoreTaskLastId',
|
||||||
|
'vectorKey' => 'vector:manticoreTaskLastId',
|
||||||
|
'class' => ManticoreTask::class,
|
||||||
|
'model' => ProjectTask::class,
|
||||||
|
'idField' => 'id',
|
||||||
|
],
|
||||||
|
'project' => [
|
||||||
|
'syncKey' => 'sync:manticoreProjectLastId',
|
||||||
|
'vectorKey' => 'vector:manticoreProjectLastId',
|
||||||
|
'class' => ManticoreProject::class,
|
||||||
|
'model' => Project::class,
|
||||||
|
'idField' => 'id',
|
||||||
|
],
|
||||||
|
'user' => [
|
||||||
|
'syncKey' => 'sync:manticoreUserLastId',
|
||||||
|
'vectorKey' => 'vector:manticoreUserLastId',
|
||||||
|
'class' => ManticoreUser::class,
|
||||||
|
'model' => User::class,
|
||||||
|
'idField' => 'userid',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("search")) {
|
||||||
|
$this->error("应用「Manticore Search」未安装");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Apps::isInstalled("ai")) {
|
||||||
|
$this->error("应用「AI」未安装,无法生成向量");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->registerSignalHandlers();
|
||||||
|
|
||||||
|
if (!$this->acquireLock()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $this->option('type');
|
||||||
|
$batchSize = intval($this->option('batch'));
|
||||||
|
$sleepSeconds = intval($this->option('sleep'));
|
||||||
|
$reset = $this->option('reset');
|
||||||
|
|
||||||
|
if ($type === 'all') {
|
||||||
|
$types = array_keys(self::TYPE_CONFIG);
|
||||||
|
} else {
|
||||||
|
if (!isset(self::TYPE_CONFIG[$type])) {
|
||||||
|
$this->error("未知类型: {$type}。可用类型: msg, file, task, project, user, all");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
$types = [$type];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 持续处理直到所有类型都没有待处理数据
|
||||||
|
$round = 0;
|
||||||
|
do {
|
||||||
|
$round++;
|
||||||
|
$totalPending = 0;
|
||||||
|
|
||||||
|
foreach ($types as $t) {
|
||||||
|
if ($this->shouldStop) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$pending = $this->processType($t, $batchSize, $reset && $round === 1);
|
||||||
|
$totalPending += $pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果还有待处理数据,休眠后继续
|
||||||
|
if ($totalPending > 0 && !$this->shouldStop) {
|
||||||
|
$this->info("\n--- 第 {$round} 轮完成,剩余 {$totalPending} 条待处理,{$sleepSeconds} 秒后继续 ---\n");
|
||||||
|
sleep($sleepSeconds);
|
||||||
|
$this->setLock(); // 刷新锁
|
||||||
|
}
|
||||||
|
} while ($totalPending > 0 && !$this->shouldStop);
|
||||||
|
|
||||||
|
$this->info("\n向量生成完成(共 {$round} 轮)");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理单个类型的向量生成(每次处理一批)
|
||||||
|
*
|
||||||
|
* @param string $type 类型
|
||||||
|
* @param int $batchSize 每批数量
|
||||||
|
* @param bool $reset 是否重置进度
|
||||||
|
* @return int 剩余待处理数量
|
||||||
|
*/
|
||||||
|
private function processType(string $type, int $batchSize, bool $reset): int
|
||||||
|
{
|
||||||
|
$config = self::TYPE_CONFIG[$type];
|
||||||
|
|
||||||
|
// 获取进度指针
|
||||||
|
$syncLastId = intval(ManticoreKeyValue::get($config['syncKey'], 0));
|
||||||
|
$vectorLastId = $reset ? 0 : intval(ManticoreKeyValue::get($config['vectorKey'], 0));
|
||||||
|
|
||||||
|
if ($reset) {
|
||||||
|
ManticoreKeyValue::set($config['vectorKey'], 0);
|
||||||
|
$this->info("[{$type}] 已重置向量进度指针");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算待处理范围
|
||||||
|
$pendingCount = $syncLastId - $vectorLastId;
|
||||||
|
if ($pendingCount <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取待处理的 ID 列表(每次处理 batchSize * 5 条,让 generateVectorsBatch 内部再分批调用 API)
|
||||||
|
$modelClass = $config['model'];
|
||||||
|
$idField = $config['idField'];
|
||||||
|
$fetchCount = $batchSize * 5;
|
||||||
|
|
||||||
|
$ids = $modelClass::where($idField, '>', $vectorLastId)
|
||||||
|
->where($idField, '<=', $syncLastId)
|
||||||
|
->orderBy($idField)
|
||||||
|
->limit($fetchCount)
|
||||||
|
->pluck($idField)
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
if (empty($ids)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量生成向量
|
||||||
|
$manticoreClass = $config['class'];
|
||||||
|
$successCount = $manticoreClass::generateVectorsBatch($ids, $batchSize);
|
||||||
|
|
||||||
|
$currentLastId = end($ids);
|
||||||
|
|
||||||
|
// 更新向量进度指针
|
||||||
|
ManticoreKeyValue::set($config['vectorKey'], $currentLastId);
|
||||||
|
|
||||||
|
$remaining = $pendingCount - count($ids);
|
||||||
|
$this->info("[{$type}] 处理 " . count($ids) . " 条,成功 {$successCount},ID: {$vectorLastId} -> {$currentLastId},剩余 {$remaining}");
|
||||||
|
|
||||||
|
// 刷新锁
|
||||||
|
$this->setLock();
|
||||||
|
|
||||||
|
return max(0, $remaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
188
app/Console/Commands/RetryManticoreSync.php
Normal file
188
app/Console/Commands/RetryManticoreSync.php
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Console\Commands\Traits\ManticoreSyncLock;
|
||||||
|
use App\Models\File;
|
||||||
|
use App\Models\ManticoreSyncFailure;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectTask;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Manticore\ManticoreBase;
|
||||||
|
use App\Module\Manticore\ManticoreFile;
|
||||||
|
use App\Module\Manticore\ManticoreMsg;
|
||||||
|
use App\Module\Manticore\ManticoreProject;
|
||||||
|
use App\Module\Manticore\ManticoreTask;
|
||||||
|
use App\Module\Manticore\ManticoreUser;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class RetryManticoreSync extends Command
|
||||||
|
{
|
||||||
|
use ManticoreSyncLock;
|
||||||
|
|
||||||
|
protected $signature = 'manticore:retry-failures {--limit=100 : 每次处理的最大数量} {--stats : 显示统计信息}';
|
||||||
|
protected $description = '重试 Manticore 同步失败的记录';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("search")) {
|
||||||
|
$this->error("应用「Manticore Search」未安装");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示统计信息
|
||||||
|
if ($this->option('stats')) {
|
||||||
|
$this->showStats();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->registerSignalHandlers();
|
||||||
|
|
||||||
|
if (!$this->acquireLock()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('开始重试失败的同步任务...');
|
||||||
|
|
||||||
|
$limit = intval($this->option('limit'));
|
||||||
|
$failures = ManticoreSyncFailure::getPendingRetries($limit);
|
||||||
|
|
||||||
|
if ($failures->isEmpty()) {
|
||||||
|
$this->info('无待重试的记录');
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("找到 {$failures->count()} 条待重试记录");
|
||||||
|
|
||||||
|
$successCount = 0;
|
||||||
|
$failCount = 0;
|
||||||
|
|
||||||
|
foreach ($failures as $failure) {
|
||||||
|
if ($this->shouldStop) {
|
||||||
|
$this->info('收到停止信号,退出处理');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setLock();
|
||||||
|
|
||||||
|
$result = $this->retryOne($failure);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
$successCount++;
|
||||||
|
$this->info(" [成功] {$failure->data_type}:{$failure->data_id} ({$failure->action})");
|
||||||
|
} else {
|
||||||
|
$failCount++;
|
||||||
|
$this->warn(" [失败] {$failure->data_type}:{$failure->data_id} ({$failure->action}) - 第 {$failure->retry_count} 次");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("\n重试完成: 成功 {$successCount}, 失败 {$failCount}");
|
||||||
|
$this->releaseLock();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试单条失败记录
|
||||||
|
*/
|
||||||
|
private function retryOne(ManticoreSyncFailure $failure): bool
|
||||||
|
{
|
||||||
|
$type = $failure->data_type;
|
||||||
|
$id = $failure->data_id;
|
||||||
|
$action = $failure->action;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($action === 'delete') {
|
||||||
|
// 删除操作直接调用通用删除方法
|
||||||
|
return ManticoreBase::deleteVector($type, $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync 操作需要根据类型获取模型并同步
|
||||||
|
return $this->retrySyncByType($type, $id);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// 记录失败(会自动更新重试次数和时间)
|
||||||
|
ManticoreSyncFailure::recordFailure($type, $id, $action, $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型重试同步
|
||||||
|
*/
|
||||||
|
private function retrySyncByType(string $type, int $id): bool
|
||||||
|
{
|
||||||
|
switch ($type) {
|
||||||
|
case 'msg':
|
||||||
|
$model = WebSocketDialogMsg::find($id);
|
||||||
|
if (!$model) {
|
||||||
|
// 数据已删除,移除失败记录
|
||||||
|
ManticoreSyncFailure::removeSuccess($type, $id, 'sync');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ManticoreMsg::sync($model);
|
||||||
|
|
||||||
|
case 'file':
|
||||||
|
$model = File::find($id);
|
||||||
|
if (!$model) {
|
||||||
|
ManticoreSyncFailure::removeSuccess($type, $id, 'sync');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ManticoreFile::sync($model);
|
||||||
|
|
||||||
|
case 'task':
|
||||||
|
$model = ProjectTask::find($id);
|
||||||
|
if (!$model) {
|
||||||
|
ManticoreSyncFailure::removeSuccess($type, $id, 'sync');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ManticoreTask::sync($model);
|
||||||
|
|
||||||
|
case 'project':
|
||||||
|
$model = Project::find($id);
|
||||||
|
if (!$model) {
|
||||||
|
ManticoreSyncFailure::removeSuccess($type, $id, 'sync');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ManticoreProject::sync($model);
|
||||||
|
|
||||||
|
case 'user':
|
||||||
|
$model = User::find($id);
|
||||||
|
if (!$model) {
|
||||||
|
ManticoreSyncFailure::removeSuccess($type, $id, 'sync');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ManticoreUser::sync($model);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示统计信息
|
||||||
|
*/
|
||||||
|
private function showStats(): void
|
||||||
|
{
|
||||||
|
$stats = ManticoreSyncFailure::getStats();
|
||||||
|
|
||||||
|
$this->info('Manticore 同步失败统计:');
|
||||||
|
$this->info(" 总数: {$stats['total']}");
|
||||||
|
|
||||||
|
if (!empty($stats['by_type'])) {
|
||||||
|
$this->info(' 按类型:');
|
||||||
|
foreach ($stats['by_type'] as $type => $count) {
|
||||||
|
$this->info(" - {$type}: {$count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($stats['by_action'])) {
|
||||||
|
$this->info(' 按操作:');
|
||||||
|
foreach ($stats['by_action'] as $action => $count) {
|
||||||
|
$this->info(" - {$action}: {$count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
155
app/Console/Commands/SyncFileToManticore.php
Normal file
155
app/Console/Commands/SyncFileToManticore.php
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Console\Commands\Traits\ManticoreSyncLock;
|
||||||
|
use App\Models\File;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Manticore\ManticoreFile;
|
||||||
|
use App\Module\Manticore\ManticoreKeyValue;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SyncFileToManticore extends Command
|
||||||
|
{
|
||||||
|
use ManticoreSyncLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新数据
|
||||||
|
* --f: 全量更新 (默认)
|
||||||
|
* --i: 增量更新
|
||||||
|
*
|
||||||
|
* 清理数据
|
||||||
|
* --c: 清除索引
|
||||||
|
*
|
||||||
|
* 其他选项
|
||||||
|
* --sleep: 每批处理完成后休眠秒数
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected $signature = 'manticore:sync-files {--f} {--i} {--c} {--batch=100} {--sleep=3}';
|
||||||
|
protected $description = '同步文件数据到 Manticore Search';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("search")) {
|
||||||
|
$this->error("应用「Manticore Search」未安装");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->registerSignalHandlers();
|
||||||
|
|
||||||
|
if (!$this->acquireLock()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除索引
|
||||||
|
if ($this->option('c')) {
|
||||||
|
$this->info('清除索引...');
|
||||||
|
ManticoreKeyValue::clear();
|
||||||
|
ManticoreFile::clear();
|
||||||
|
$this->info("索引删除成功");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('开始同步文件数据...');
|
||||||
|
$this->syncFiles();
|
||||||
|
|
||||||
|
$this->info("\n同步完成");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步文件数据
|
||||||
|
*/
|
||||||
|
private function syncFiles(): void
|
||||||
|
{
|
||||||
|
$lastKey = "sync:manticoreFileLastId";
|
||||||
|
$isIncremental = $this->option('i');
|
||||||
|
$sleepSeconds = intval($this->option('sleep'));
|
||||||
|
$batchSize = $this->option('batch');
|
||||||
|
$maxFileSize = ManticoreFile::getMaxFileSize();
|
||||||
|
|
||||||
|
$round = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$round++;
|
||||||
|
$lastId = $isIncremental ? intval(ManticoreKeyValue::get($lastKey, 0)) : 0;
|
||||||
|
|
||||||
|
if ($round === 1) {
|
||||||
|
if ($lastId > 0) {
|
||||||
|
$this->info("\n增量同步文件数据(从ID {$lastId} 开始)...");
|
||||||
|
} else {
|
||||||
|
$this->info("\n全量同步文件数据...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = File::where('id', '>', $lastId)
|
||||||
|
->where('type', '!=', 'folder')
|
||||||
|
->where('size', '<=', $maxFileSize)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($count === 0) {
|
||||||
|
if ($round === 1) {
|
||||||
|
$this->info("无待同步数据");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("[第 {$round} 轮] 待同步 {$count} 个文件");
|
||||||
|
|
||||||
|
$num = 0;
|
||||||
|
$total = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if ($this->shouldStop) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = File::where('id', '>', $lastId)
|
||||||
|
->where('type', '!=', 'folder')
|
||||||
|
->where('size', '<=', $maxFileSize)
|
||||||
|
->orderBy('id')
|
||||||
|
->limit($batchSize)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($files->isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$num += count($files);
|
||||||
|
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
|
||||||
|
$this->info("{$num}/{$count} ({$progress}%) 文件ID {$files->first()->id} ~ {$files->last()->id}");
|
||||||
|
|
||||||
|
$this->setLock();
|
||||||
|
|
||||||
|
$syncCount = ManticoreFile::batchSync($files);
|
||||||
|
$total += $syncCount;
|
||||||
|
|
||||||
|
$lastId = $files->last()->id;
|
||||||
|
ManticoreKeyValue::set($lastKey, $lastId);
|
||||||
|
} while (count($files) == $batchSize && !$this->shouldStop);
|
||||||
|
|
||||||
|
$this->info("[第 {$round} 轮] 完成,同步 {$total} 个,最后ID {$lastId}");
|
||||||
|
|
||||||
|
if ($isIncremental && !$this->shouldStop) {
|
||||||
|
$newCount = File::where('id', '>', $lastId)
|
||||||
|
->where('type', '!=', 'folder')
|
||||||
|
->where('size', '<=', $maxFileSize)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($newCount > 0) {
|
||||||
|
$this->info("发现 {$newCount} 个新文件,{$sleepSeconds} 秒后继续...");
|
||||||
|
sleep($sleepSeconds);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
} while (!$this->shouldStop);
|
||||||
|
|
||||||
|
$this->info("同步文件结束(共 {$round} 轮)- 最后ID: " . ManticoreKeyValue::get($lastKey, 0));
|
||||||
|
$this->info("已索引文件数量: " . ManticoreFile::getIndexedCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
232
app/Console/Commands/SyncMsgToManticore.php
Normal file
232
app/Console/Commands/SyncMsgToManticore.php
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Console\Commands\Traits\ManticoreSyncLock;
|
||||||
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Manticore\ManticoreMsg;
|
||||||
|
use App\Module\Manticore\ManticoreKeyValue;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SyncMsgToManticore extends Command
|
||||||
|
{
|
||||||
|
use ManticoreSyncLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新数据
|
||||||
|
* --f: 全量更新 (默认)
|
||||||
|
* --i: 增量更新
|
||||||
|
*
|
||||||
|
* 清理数据
|
||||||
|
* --c: 清除索引
|
||||||
|
*
|
||||||
|
* 其他选项
|
||||||
|
* --dialog: 指定对话ID
|
||||||
|
* --sleep: 每批处理完成后休眠秒数
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected $signature = 'manticore:sync-msgs {--f} {--i} {--c} {--batch=100} {--dialog=} {--sleep=3}';
|
||||||
|
protected $description = '同步消息数据到 Manticore Search';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("search")) {
|
||||||
|
$this->error("应用「Manticore Search」未安装");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->registerSignalHandlers();
|
||||||
|
|
||||||
|
if (!$this->acquireLock()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除索引
|
||||||
|
if ($this->option('c')) {
|
||||||
|
$this->info('清除索引...');
|
||||||
|
ManticoreMsg::clear();
|
||||||
|
$this->info("索引删除成功");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dialogId = $this->option('dialog') ? intval($this->option('dialog')) : 0;
|
||||||
|
|
||||||
|
if ($dialogId > 0) {
|
||||||
|
$this->info("开始同步对话 {$dialogId} 的消息数据...");
|
||||||
|
$this->syncDialogMsgs($dialogId);
|
||||||
|
} else {
|
||||||
|
$this->info('开始同步消息数据...');
|
||||||
|
$this->syncMsgs();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("\n同步完成");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步所有消息
|
||||||
|
*/
|
||||||
|
private function syncMsgs(): void
|
||||||
|
{
|
||||||
|
$lastKey = "sync:manticoreMsgLastId";
|
||||||
|
$isIncremental = $this->option('i');
|
||||||
|
$sleepSeconds = intval($this->option('sleep'));
|
||||||
|
$batchSize = $this->option('batch');
|
||||||
|
|
||||||
|
$round = 0;
|
||||||
|
|
||||||
|
// 持续处理循环(增量模式下)
|
||||||
|
do {
|
||||||
|
$round++;
|
||||||
|
$lastId = $isIncremental ? intval(ManticoreKeyValue::get($lastKey, 0)) : 0;
|
||||||
|
|
||||||
|
if ($round === 1) {
|
||||||
|
if ($lastId > 0) {
|
||||||
|
$this->info("\n增量同步消息数据(从ID {$lastId} 开始)...");
|
||||||
|
} else {
|
||||||
|
$this->info("\n全量同步消息数据...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建基础查询条件
|
||||||
|
$count = WebSocketDialogMsg::where('id', '>', $lastId)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->where('bot', '!=', 1)
|
||||||
|
->whereNotNull('key')
|
||||||
|
->where('key', '!=', '')
|
||||||
|
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($count === 0) {
|
||||||
|
if ($round === 1) {
|
||||||
|
$this->info("无待同步数据");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("[第 {$round} 轮] 待同步 {$count} 条消息");
|
||||||
|
|
||||||
|
$num = 0;
|
||||||
|
$total = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if ($this->shouldStop) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$msgs = WebSocketDialogMsg::where('id', '>', $lastId)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->where('bot', '!=', 1)
|
||||||
|
->whereNotNull('key')
|
||||||
|
->where('key', '!=', '')
|
||||||
|
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES)
|
||||||
|
->orderBy('id')
|
||||||
|
->limit($batchSize)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($msgs->isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$num += count($msgs);
|
||||||
|
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
|
||||||
|
$this->info("{$num}/{$count} ({$progress}%) 消息ID {$msgs->first()->id} ~ {$msgs->last()->id}");
|
||||||
|
|
||||||
|
$this->setLock();
|
||||||
|
|
||||||
|
$syncCount = ManticoreMsg::batchSync($msgs);
|
||||||
|
$total += $syncCount;
|
||||||
|
|
||||||
|
$lastId = $msgs->last()->id;
|
||||||
|
ManticoreKeyValue::set($lastKey, $lastId);
|
||||||
|
} while (count($msgs) == $batchSize && !$this->shouldStop);
|
||||||
|
|
||||||
|
$this->info("[第 {$round} 轮] 完成,同步 {$total} 条,最后ID {$lastId}");
|
||||||
|
|
||||||
|
// 增量模式下,检查是否有新数据,有则继续
|
||||||
|
if ($isIncremental && !$this->shouldStop) {
|
||||||
|
$newCount = WebSocketDialogMsg::where('id', '>', $lastId)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->where('bot', '!=', 1)
|
||||||
|
->whereNotNull('key')
|
||||||
|
->where('key', '!=', '')
|
||||||
|
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($newCount > 0) {
|
||||||
|
$this->info("发现 {$newCount} 条新数据,{$sleepSeconds} 秒后继续...");
|
||||||
|
sleep($sleepSeconds);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break; // 非增量模式或无新数据,退出循环
|
||||||
|
|
||||||
|
} while (!$this->shouldStop);
|
||||||
|
|
||||||
|
$this->info("同步消息结束(共 {$round} 轮)- 最后ID: " . ManticoreKeyValue::get($lastKey, 0));
|
||||||
|
$this->info("已索引消息数量: " . ManticoreMsg::getIndexedCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步指定对话的消息
|
||||||
|
*
|
||||||
|
* @param int $dialogId 对话ID
|
||||||
|
*/
|
||||||
|
private function syncDialogMsgs(int $dialogId): void
|
||||||
|
{
|
||||||
|
$this->info("\n同步对话 {$dialogId} 的消息数据...");
|
||||||
|
|
||||||
|
$baseQuery = WebSocketDialogMsg::where('dialog_id', $dialogId)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->where('bot', '!=', 1)
|
||||||
|
->whereNotNull('key')
|
||||||
|
->where('key', '!=', '')
|
||||||
|
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES);
|
||||||
|
|
||||||
|
$num = 0;
|
||||||
|
$count = $baseQuery->count();
|
||||||
|
$batchSize = $this->option('batch');
|
||||||
|
$lastId = 0;
|
||||||
|
|
||||||
|
$total = 0;
|
||||||
|
$lastNum = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$msgs = WebSocketDialogMsg::where('dialog_id', $dialogId)
|
||||||
|
->where('id', '>', $lastId)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->where('bot', '!=', 1)
|
||||||
|
->whereNotNull('key')
|
||||||
|
->where('key', '!=', '')
|
||||||
|
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES)
|
||||||
|
->orderBy('id')
|
||||||
|
->limit($batchSize)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($msgs->isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$num += count($msgs);
|
||||||
|
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
|
||||||
|
if ($progress < 100) {
|
||||||
|
$progress = number_format($progress, 2);
|
||||||
|
}
|
||||||
|
$this->info("{$num}/{$count} ({$progress}%) 正在同步消息ID {$msgs->first()->id} ~ {$msgs->last()->id} ({$total}|{$lastNum})");
|
||||||
|
|
||||||
|
$this->setLock();
|
||||||
|
|
||||||
|
$lastNum = ManticoreMsg::batchSync($msgs);
|
||||||
|
$total += $lastNum;
|
||||||
|
|
||||||
|
$lastId = $msgs->last()->id;
|
||||||
|
} while (count($msgs) == $batchSize);
|
||||||
|
|
||||||
|
$this->info("同步对话 {$dialogId} 消息结束");
|
||||||
|
$this->info("该对话已索引消息数量: " . \App\Module\Manticore\ManticoreBase::getDialogIndexedMsgCount($dialogId));
|
||||||
|
}
|
||||||
|
}
|
||||||
146
app/Console/Commands/SyncProjectToManticore.php
Normal file
146
app/Console/Commands/SyncProjectToManticore.php
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Console\Commands\Traits\ManticoreSyncLock;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Manticore\ManticoreProject;
|
||||||
|
use App\Module\Manticore\ManticoreKeyValue;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SyncProjectToManticore extends Command
|
||||||
|
{
|
||||||
|
use ManticoreSyncLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新数据
|
||||||
|
* --f: 全量更新 (默认)
|
||||||
|
* --i: 增量更新
|
||||||
|
*
|
||||||
|
* 清理数据
|
||||||
|
* --c: 清除索引
|
||||||
|
*
|
||||||
|
* 其他选项
|
||||||
|
* --sleep: 每批处理完成后休眠秒数
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected $signature = 'manticore:sync-projects {--f} {--i} {--c} {--batch=100} {--sleep=3}';
|
||||||
|
protected $description = '同步项目数据到 Manticore Search';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("search")) {
|
||||||
|
$this->error("应用「Manticore Search」未安装");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->registerSignalHandlers();
|
||||||
|
|
||||||
|
if (!$this->acquireLock()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->option('c')) {
|
||||||
|
$this->info('清除索引...');
|
||||||
|
ManticoreProject::clear();
|
||||||
|
$this->info("索引删除成功");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('开始同步项目数据...');
|
||||||
|
$this->syncProjects();
|
||||||
|
|
||||||
|
$this->info("\n同步完成");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncProjects(): void
|
||||||
|
{
|
||||||
|
$lastKey = "sync:manticoreProjectLastId";
|
||||||
|
$isIncremental = $this->option('i');
|
||||||
|
$sleepSeconds = intval($this->option('sleep'));
|
||||||
|
$batchSize = $this->option('batch');
|
||||||
|
|
||||||
|
$round = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$round++;
|
||||||
|
$lastId = $isIncremental ? intval(ManticoreKeyValue::get($lastKey, 0)) : 0;
|
||||||
|
|
||||||
|
if ($round === 1) {
|
||||||
|
if ($lastId > 0) {
|
||||||
|
$this->info("\n增量同步项目数据(从ID {$lastId} 开始)...");
|
||||||
|
} else {
|
||||||
|
$this->info("\n全量同步项目数据...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = Project::where('id', '>', $lastId)
|
||||||
|
->whereNull('archived_at')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($count === 0) {
|
||||||
|
if ($round === 1) {
|
||||||
|
$this->info("无待同步数据");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("[第 {$round} 轮] 待同步 {$count} 个项目");
|
||||||
|
|
||||||
|
$num = 0;
|
||||||
|
$total = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if ($this->shouldStop) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$projects = Project::where('id', '>', $lastId)
|
||||||
|
->whereNull('archived_at')
|
||||||
|
->orderBy('id')
|
||||||
|
->limit($batchSize)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($projects->isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$num += count($projects);
|
||||||
|
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
|
||||||
|
$this->info("{$num}/{$count} ({$progress}%) 项目ID {$projects->first()->id} ~ {$projects->last()->id}");
|
||||||
|
|
||||||
|
$this->setLock();
|
||||||
|
|
||||||
|
$syncCount = ManticoreProject::batchSync($projects);
|
||||||
|
$total += $syncCount;
|
||||||
|
|
||||||
|
$lastId = $projects->last()->id;
|
||||||
|
ManticoreKeyValue::set($lastKey, $lastId);
|
||||||
|
} while (count($projects) == $batchSize && !$this->shouldStop);
|
||||||
|
|
||||||
|
$this->info("[第 {$round} 轮] 完成,同步 {$total} 个,最后ID {$lastId}");
|
||||||
|
|
||||||
|
if ($isIncremental && !$this->shouldStop) {
|
||||||
|
$newCount = Project::where('id', '>', $lastId)
|
||||||
|
->whereNull('archived_at')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($newCount > 0) {
|
||||||
|
$this->info("发现 {$newCount} 个新项目,{$sleepSeconds} 秒后继续...");
|
||||||
|
sleep($sleepSeconds);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
} while (!$this->shouldStop);
|
||||||
|
|
||||||
|
$this->info("同步项目结束(共 {$round} 轮)- 最后ID: " . ManticoreKeyValue::get($lastKey, 0));
|
||||||
|
$this->info("已索引项目数量: " . ManticoreProject::getIndexedCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
149
app/Console/Commands/SyncTaskToManticore.php
Normal file
149
app/Console/Commands/SyncTaskToManticore.php
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Console\Commands\Traits\ManticoreSyncLock;
|
||||||
|
use App\Models\ProjectTask;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Manticore\ManticoreTask;
|
||||||
|
use App\Module\Manticore\ManticoreKeyValue;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SyncTaskToManticore extends Command
|
||||||
|
{
|
||||||
|
use ManticoreSyncLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新数据
|
||||||
|
* --f: 全量更新 (默认)
|
||||||
|
* --i: 增量更新
|
||||||
|
*
|
||||||
|
* 清理数据
|
||||||
|
* --c: 清除索引
|
||||||
|
*
|
||||||
|
* 其他选项
|
||||||
|
* --sleep: 每批处理完成后休眠秒数
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected $signature = 'manticore:sync-tasks {--f} {--i} {--c} {--batch=100} {--sleep=3}';
|
||||||
|
protected $description = '同步任务数据到 Manticore Search';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("search")) {
|
||||||
|
$this->error("应用「Manticore Search」未安装");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->registerSignalHandlers();
|
||||||
|
|
||||||
|
if (!$this->acquireLock()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->option('c')) {
|
||||||
|
$this->info('清除索引...');
|
||||||
|
ManticoreTask::clear();
|
||||||
|
$this->info("索引删除成功");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('开始同步任务数据...');
|
||||||
|
$this->syncTasks();
|
||||||
|
|
||||||
|
$this->info("\n同步完成");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncTasks(): void
|
||||||
|
{
|
||||||
|
$lastKey = "sync:manticoreTaskLastId";
|
||||||
|
$isIncremental = $this->option('i');
|
||||||
|
$sleepSeconds = intval($this->option('sleep'));
|
||||||
|
$batchSize = $this->option('batch');
|
||||||
|
|
||||||
|
$round = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$round++;
|
||||||
|
$lastId = $isIncremental ? intval(ManticoreKeyValue::get($lastKey, 0)) : 0;
|
||||||
|
|
||||||
|
if ($round === 1) {
|
||||||
|
if ($lastId > 0) {
|
||||||
|
$this->info("\n增量同步任务数据(从ID {$lastId} 开始)...");
|
||||||
|
} else {
|
||||||
|
$this->info("\n全量同步任务数据...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = ProjectTask::where('id', '>', $lastId)
|
||||||
|
->whereNull('archived_at')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($count === 0) {
|
||||||
|
if ($round === 1) {
|
||||||
|
$this->info("无待同步数据");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("[第 {$round} 轮] 待同步 {$count} 个任务");
|
||||||
|
|
||||||
|
$num = 0;
|
||||||
|
$total = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if ($this->shouldStop) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tasks = ProjectTask::where('id', '>', $lastId)
|
||||||
|
->whereNull('archived_at')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->orderBy('id')
|
||||||
|
->limit($batchSize)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($tasks->isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$num += count($tasks);
|
||||||
|
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
|
||||||
|
$this->info("{$num}/{$count} ({$progress}%) 任务ID {$tasks->first()->id} ~ {$tasks->last()->id}");
|
||||||
|
|
||||||
|
$this->setLock();
|
||||||
|
|
||||||
|
$syncCount = ManticoreTask::batchSync($tasks);
|
||||||
|
$total += $syncCount;
|
||||||
|
|
||||||
|
$lastId = $tasks->last()->id;
|
||||||
|
ManticoreKeyValue::set($lastKey, $lastId);
|
||||||
|
} while (count($tasks) == $batchSize && !$this->shouldStop);
|
||||||
|
|
||||||
|
$this->info("[第 {$round} 轮] 完成,同步 {$total} 个,最后ID {$lastId}");
|
||||||
|
|
||||||
|
if ($isIncremental && !$this->shouldStop) {
|
||||||
|
$newCount = ProjectTask::where('id', '>', $lastId)
|
||||||
|
->whereNull('archived_at')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($newCount > 0) {
|
||||||
|
$this->info("发现 {$newCount} 个新任务,{$sleepSeconds} 秒后继续...");
|
||||||
|
sleep($sleepSeconds);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
} while (!$this->shouldStop);
|
||||||
|
|
||||||
|
$this->info("同步任务结束(共 {$round} 轮)- 最后ID: " . ManticoreKeyValue::get($lastKey, 0));
|
||||||
|
$this->info("已索引任务数量: " . ManticoreTask::getIndexedCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
149
app/Console/Commands/SyncUserToManticore.php
Normal file
149
app/Console/Commands/SyncUserToManticore.php
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Console\Commands\Traits\ManticoreSyncLock;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Manticore\ManticoreUser;
|
||||||
|
use App\Module\Manticore\ManticoreKeyValue;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SyncUserToManticore extends Command
|
||||||
|
{
|
||||||
|
use ManticoreSyncLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新数据
|
||||||
|
* --f: 全量更新 (默认)
|
||||||
|
* --i: 增量更新
|
||||||
|
*
|
||||||
|
* 清理数据
|
||||||
|
* --c: 清除索引
|
||||||
|
*
|
||||||
|
* 其他选项
|
||||||
|
* --sleep: 每批处理完成后休眠秒数
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected $signature = 'manticore:sync-users {--f} {--i} {--c} {--batch=100} {--sleep=3}';
|
||||||
|
protected $description = '同步用户数据到 Manticore Search';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("search")) {
|
||||||
|
$this->error("应用「Manticore Search」未安装");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->registerSignalHandlers();
|
||||||
|
|
||||||
|
if (!$this->acquireLock()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->option('c')) {
|
||||||
|
$this->info('清除索引...');
|
||||||
|
ManticoreUser::clear();
|
||||||
|
$this->info("索引删除成功");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('开始同步用户数据...');
|
||||||
|
$this->syncUsers();
|
||||||
|
|
||||||
|
$this->info("\n同步完成");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncUsers(): void
|
||||||
|
{
|
||||||
|
$lastKey = "sync:manticoreUserLastId";
|
||||||
|
$isIncremental = $this->option('i');
|
||||||
|
$sleepSeconds = intval($this->option('sleep'));
|
||||||
|
$batchSize = $this->option('batch');
|
||||||
|
|
||||||
|
$round = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$round++;
|
||||||
|
$lastId = $isIncremental ? intval(ManticoreKeyValue::get($lastKey, 0)) : 0;
|
||||||
|
|
||||||
|
if ($round === 1) {
|
||||||
|
if ($lastId > 0) {
|
||||||
|
$this->info("\n增量同步用户数据(从ID {$lastId} 开始)...");
|
||||||
|
} else {
|
||||||
|
$this->info("\n全量同步用户数据...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = User::where('userid', '>', $lastId)
|
||||||
|
->where('bot', 0)
|
||||||
|
->whereNull('disable_at')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($count === 0) {
|
||||||
|
if ($round === 1) {
|
||||||
|
$this->info("无待同步数据");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("[第 {$round} 轮] 待同步 {$count} 个用户");
|
||||||
|
|
||||||
|
$num = 0;
|
||||||
|
$total = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if ($this->shouldStop) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = User::where('userid', '>', $lastId)
|
||||||
|
->where('bot', 0)
|
||||||
|
->whereNull('disable_at')
|
||||||
|
->orderBy('userid')
|
||||||
|
->limit($batchSize)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($users->isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$num += count($users);
|
||||||
|
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
|
||||||
|
$this->info("{$num}/{$count} ({$progress}%) 用户ID {$users->first()->userid} ~ {$users->last()->userid}");
|
||||||
|
|
||||||
|
$this->setLock();
|
||||||
|
|
||||||
|
$syncCount = ManticoreUser::batchSync($users);
|
||||||
|
$total += $syncCount;
|
||||||
|
|
||||||
|
$lastId = $users->last()->userid;
|
||||||
|
ManticoreKeyValue::set($lastKey, $lastId);
|
||||||
|
} while (count($users) == $batchSize && !$this->shouldStop);
|
||||||
|
|
||||||
|
$this->info("[第 {$round} 轮] 完成,同步 {$total} 个,最后ID {$lastId}");
|
||||||
|
|
||||||
|
if ($isIncremental && !$this->shouldStop) {
|
||||||
|
$newCount = User::where('userid', '>', $lastId)
|
||||||
|
->where('bot', 0)
|
||||||
|
->whereNull('disable_at')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($newCount > 0) {
|
||||||
|
$this->info("发现 {$newCount} 个新用户,{$sleepSeconds} 秒后继续...");
|
||||||
|
sleep($sleepSeconds);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
} while (!$this->shouldStop);
|
||||||
|
|
||||||
|
$this->info("同步用户结束(共 {$round} 轮)- 最后ID: " . ManticoreKeyValue::get($lastKey, 0));
|
||||||
|
$this->info("已索引用户数量: " . ManticoreUser::getIndexedCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
90
app/Console/Commands/Traits/ManticoreSyncLock.php
Normal file
90
app/Console/Commands/Traits/ManticoreSyncLock.php
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Traits;
|
||||||
|
|
||||||
|
use Cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manticore 同步命令通用锁机制
|
||||||
|
*
|
||||||
|
* 提供:
|
||||||
|
* - 锁的获取、设置、释放
|
||||||
|
* - 信号处理(优雅退出)
|
||||||
|
* - 通用的命令初始化检查
|
||||||
|
*/
|
||||||
|
trait ManticoreSyncLock
|
||||||
|
{
|
||||||
|
private bool $shouldStop = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取锁信息
|
||||||
|
*/
|
||||||
|
private function getLock(): ?array
|
||||||
|
{
|
||||||
|
$lockKey = $this->getLockKey();
|
||||||
|
return Cache::has($lockKey) ? Cache::get($lockKey) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置锁(30分钟有效期,持续处理时需不断刷新)
|
||||||
|
*/
|
||||||
|
private function setLock(): void
|
||||||
|
{
|
||||||
|
$lockKey = $this->getLockKey();
|
||||||
|
Cache::put($lockKey, ['started_at' => date('Y-m-d H:i:s')], 1800);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放锁
|
||||||
|
*/
|
||||||
|
private function releaseLock(): void
|
||||||
|
{
|
||||||
|
$lockKey = $this->getLockKey();
|
||||||
|
Cache::forget($lockKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取锁的缓存键
|
||||||
|
*/
|
||||||
|
private function getLockKey(): string
|
||||||
|
{
|
||||||
|
return md5($this->signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信号处理器(SIGINT/SIGTERM)
|
||||||
|
*/
|
||||||
|
public function handleSignal(int $signal): void
|
||||||
|
{
|
||||||
|
$this->info("\n收到信号,将在当前批次完成后退出...");
|
||||||
|
$this->shouldStop = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册信号处理器
|
||||||
|
*/
|
||||||
|
private function registerSignalHandlers(): void
|
||||||
|
{
|
||||||
|
if (extension_loaded('pcntl')) {
|
||||||
|
pcntl_async_signals(true);
|
||||||
|
pcntl_signal(SIGINT, [$this, 'handleSignal']);
|
||||||
|
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查命令是否可以启动(锁检查)
|
||||||
|
*
|
||||||
|
* @return bool 返回 true 表示可以启动,false 表示已被占用
|
||||||
|
*/
|
||||||
|
private function acquireLock(): bool
|
||||||
|
{
|
||||||
|
$lockInfo = $this->getLock();
|
||||||
|
if ($lockInfo) {
|
||||||
|
$this->error("命令已在运行中,开始时间: {$lockInfo['started_at']}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$this->setLock();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
namespace App\Events;
|
||||||
|
|
||||||
use App\Models\WebSocket;
|
use App\Models\WebSocket;
|
||||||
use Cache;
|
use App\Services\RequestContext;
|
||||||
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
|
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
|
||||||
use Swoole\Http\Server;
|
use Swoole\Http\Server;
|
||||||
|
|
||||||
@ -16,9 +16,16 @@ class WorkerStartEvent implements WorkerStartInterface
|
|||||||
|
|
||||||
public function handle(Server $server, $workerId)
|
public function handle(Server $server, $workerId)
|
||||||
{
|
{
|
||||||
if (isset($server->startMsecTime) && Cache::get("swooleServerStartMsecTime") != $server->startMsecTime) {
|
// 仅在Worker进程启动时执行一次初始化代码
|
||||||
Cache::forever("swooleServerStartMsecTime", $server->startMsecTime);
|
$initTable = app('swoole')->initFlagTable;
|
||||||
WebSocket::query()->delete();
|
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;
|
protected $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $writeLog = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ApiException constructor.
|
* ApiException constructor.
|
||||||
* @param string $msg
|
* @param string|array $msg
|
||||||
* @param array $data
|
* @param array $data
|
||||||
* @param int $code
|
* @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'])) {
|
if (is_array($msg) && isset($msg['code'])) {
|
||||||
$code = $msg['code'];
|
$code = $msg['code'];
|
||||||
@ -24,6 +29,7 @@ class ApiException extends RuntimeException
|
|||||||
$msg = $msg['msg'];
|
$msg = $msg['msg'];
|
||||||
}
|
}
|
||||||
$this->data = $data;
|
$this->data = $data;
|
||||||
|
$this->writeLog = $writeLog && $code !== -1;
|
||||||
parent::__construct($msg, $code);
|
parent::__construct($msg, $code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,4 +40,12 @@ class ApiException extends RuntimeException
|
|||||||
{
|
{
|
||||||
return $this->data;
|
return $this->data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isWriteLog(): bool
|
||||||
|
{
|
||||||
|
return $this->writeLog;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,11 @@
|
|||||||
namespace App\Exceptions;
|
namespace App\Exceptions;
|
||||||
|
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
|
use App\Module\Image;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class Handler extends ExceptionHandler
|
class Handler extends ExceptionHandler
|
||||||
@ -51,6 +53,11 @@ class Handler extends ExceptionHandler
|
|||||||
*/
|
*/
|
||||||
public function render($request, Throwable $e)
|
public function render($request, Throwable $e)
|
||||||
{
|
{
|
||||||
|
if ($e instanceof NotFoundHttpException) {
|
||||||
|
if ($result = $this->ImagePathHandler($request)) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($e instanceof ApiException) {
|
if ($e instanceof ApiException) {
|
||||||
return response()->json(Base::retError($e->getMessage(), $e->getData(), $e->getCode()));
|
return response()->json(Base::retError($e->getMessage(), $e->getData(), $e->getCode()));
|
||||||
} elseif ($e instanceof ModelNotFoundException) {
|
} elseif ($e instanceof ModelNotFoundException) {
|
||||||
@ -67,7 +74,7 @@ class Handler extends ExceptionHandler
|
|||||||
public function report(Throwable $e)
|
public function report(Throwable $e)
|
||||||
{
|
{
|
||||||
if ($e instanceof ApiException) {
|
if ($e instanceof ApiException) {
|
||||||
if ($e->getCode() !== -1) {
|
if ($e->isWriteLog()) {
|
||||||
Log::error($e->getMessage(), [
|
Log::error($e->getMessage(), [
|
||||||
'code' => $e->getCode(),
|
'code' => $e->getCode(),
|
||||||
'data' => $e->getData(),
|
'data' => $e->getData(),
|
||||||
@ -78,4 +85,157 @@ class Handler extends ExceptionHandler
|
|||||||
parent::report($e);
|
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')) {
|
if (!function_exists('asset_main')) {
|
||||||
function asset_main($path, $secure = null)
|
function asset_main($path, $secure = null)
|
||||||
{
|
{
|
||||||
return preg_replace("/^https*:\/\//", "//", app('url')->asset($path, $secure));
|
return preg_replace("/^https?:\/\//", "//", app('url')->asset($path, $secure));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,22 +3,27 @@
|
|||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use Request;
|
use Request;
|
||||||
use Session;
|
|
||||||
use Response;
|
use Response;
|
||||||
use Madzipper;
|
use Madzipper;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use App\Module\Down;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
|
use App\Module\Doo;
|
||||||
|
use App\Module\Timer;
|
||||||
use App\Module\Ihttp;
|
use App\Module\Ihttp;
|
||||||
use App\Tasks\PushTask;
|
use App\Tasks\PushTask;
|
||||||
use App\Module\BillExport;
|
use App\Module\BillExport;
|
||||||
use App\Models\WebSocketDialog;
|
use App\Models\WebSocketDialog;
|
||||||
use App\Models\ApproveProcMsg;
|
use App\Models\ApproveProcMsg;
|
||||||
|
use App\Models\ApproveProcInstHistory;
|
||||||
use App\Exceptions\ApiException;
|
use App\Exceptions\ApiException;
|
||||||
use App\Models\UserDepartment;
|
use App\Models\UserDepartment;
|
||||||
use App\Models\WebSocketDialogMsg;
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Module\Apps;
|
||||||
use App\Module\BillMultipleExport;
|
use App\Module\BillMultipleExport;
|
||||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||||
|
use Swoole\Coroutine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiDefine approve
|
* @apiDefine approve
|
||||||
@ -28,16 +33,19 @@ use Hhxsv5\LaravelS\Swoole\Task\Task;
|
|||||||
class ApproveController extends AbstractController
|
class ApproveController extends AbstractController
|
||||||
{
|
{
|
||||||
private $flow_url = '';
|
private $flow_url = '';
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
Apps::isInstalledThrow('approve');
|
||||||
$this->flow_url = env('FLOW_URL') ?: 'http://approve';
|
$this->flow_url = env('FLOW_URL') ?: 'http://approve';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/approve/verifyToken 01. 验证APi登录
|
* @api {get} api/approve/verifyToken 验证APi登录
|
||||||
*
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup users
|
* @apiGroup approve
|
||||||
* @apiName verifyToken
|
* @apiName verifyToken
|
||||||
*
|
*
|
||||||
* @apiSuccess {String} version
|
* @apiSuccess {String} version
|
||||||
@ -55,7 +63,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/procdef/all 02. 查询流程定义
|
* @api {post} api/approve/procdef/all 查询流程定义
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -72,7 +80,7 @@ class ApproveController extends AbstractController
|
|||||||
{
|
{
|
||||||
User::auth();
|
User::auth();
|
||||||
$data['name'] = Request::input('name');
|
$data['name'] = Request::input('name');
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procdef/findAll', json_encode($data));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procdef/findAll', json_encode($data));
|
||||||
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$procdef || $procdef['status'] != 200 || $ret['ret'] == 0) {
|
if (!$procdef || $procdef['status'] != 200 || $ret['ret'] == 0) {
|
||||||
// info($ret);
|
// info($ret);
|
||||||
@ -82,7 +90,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/approve/procdef/del 03. 删除流程定义
|
* @api {get} api/approve/procdef/del 删除流程定义
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -99,7 +107,7 @@ class ApproveController extends AbstractController
|
|||||||
{
|
{
|
||||||
User::auth('admin');
|
User::auth('admin');
|
||||||
$data['id'] = Request::input('id');
|
$data['id'] = Request::input('id');
|
||||||
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/procdef/delById?'.http_build_query($data));
|
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/procdef/delById?' . http_build_query($data));
|
||||||
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$procdef || $procdef['status'] != 200) {
|
if (!$procdef || $procdef['status'] != 200) {
|
||||||
return Base::retError($procdef['message'] ?? '删除失败');
|
return Base::retError($procdef['message'] ?? '删除失败');
|
||||||
@ -108,7 +116,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/process/start 04. 启动流程(审批中)
|
* @api {post} api/approve/process/start 启动流程(审批中)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -132,7 +140,7 @@ class ApproveController extends AbstractController
|
|||||||
//
|
//
|
||||||
$var = json_decode(Request::input('var'), true);
|
$var = json_decode(Request::input('var'), true);
|
||||||
$data['var'] = $var;
|
$data['var'] = $var;
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/start', json_encode(Base::arrayKeyToCamel($data)));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/start', json_encode(Base::arrayKeyToCamel($data)));
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$process || $process['status'] != 200) {
|
if (!$process || $process['status'] != 200) {
|
||||||
return Base::retError($process['message'] ?? '启动失败');
|
return Base::retError($process['message'] ?? '启动失败');
|
||||||
@ -171,7 +179,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/process/addGlobalComment 05. 添加全局评论
|
* @api {post} api/approve/process/addGlobalComment 添加全局评论
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -194,7 +202,7 @@ class ApproveController extends AbstractController
|
|||||||
|
|
||||||
$processInst = $this->getProcessById($data['proc_inst_id']);
|
$processInst = $this->getProcessById($data['proc_inst_id']);
|
||||||
|
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/addGlobalComment', json_encode(Base::arrayKeyToCamel($data)));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/addGlobalComment', json_encode(Base::arrayKeyToCamel($data)));
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$process || $process['status'] != 200) {
|
if (!$process || $process['status'] != 200) {
|
||||||
return Base::retError($process['message'] ?? '添加失败');
|
return Base::retError($process['message'] ?? '添加失败');
|
||||||
@ -202,11 +210,11 @@ class ApproveController extends AbstractController
|
|||||||
|
|
||||||
// 推送通知
|
// 推送通知
|
||||||
$botUser = User::botGetOrCreate('approval-alert');
|
$botUser = User::botGetOrCreate('approval-alert');
|
||||||
foreach ( $processInst['userids'] as $id) {
|
foreach ($processInst['userids'] as $id) {
|
||||||
if($id != $user->userid){
|
if ($id != $user->userid) {
|
||||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $id);
|
$dialog = WebSocketDialog::checkUserDialog($botUser, $id);
|
||||||
$processInst['comment_user_id'] = $user->userid;
|
$processInst['comment_user_id'] = $user->userid;
|
||||||
$processInst['comment_content'] = json_decode($data['content'],true)['content'];
|
$processInst['comment_contents'] = json_decode($data['content'], true) ?? [];
|
||||||
$this->approveMsg('approve_comment_notifier', $dialog, $botUser, $processInst, $processInst);
|
$this->approveMsg('approve_comment_notifier', $dialog, $botUser, $processInst, $processInst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -216,7 +224,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/task/complete 06. 审批
|
* @api {post} api/approve/task/complete 审批
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -238,7 +246,7 @@ class ApproveController extends AbstractController
|
|||||||
$data['task_id'] = intval(Request::input('task_id'));
|
$data['task_id'] = intval(Request::input('task_id'));
|
||||||
$data['pass'] = Request::input('pass');
|
$data['pass'] = Request::input('pass');
|
||||||
$data['comment'] = Request::input('comment');
|
$data['comment'] = Request::input('comment');
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/task/complete', json_encode(Base::arrayKeyToCamel($data)));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/task/complete', json_encode(Base::arrayKeyToCamel($data)));
|
||||||
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$task || $task['status'] != 200) {
|
if (!$task || $task['status'] != 200) {
|
||||||
return Base::retError($task['message'] ?? '审批失败');
|
return Base::retError($task['message'] ?? '审批失败');
|
||||||
@ -261,12 +269,12 @@ class ApproveController extends AbstractController
|
|||||||
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process, $pass);
|
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process, $pass);
|
||||||
}
|
}
|
||||||
// 发起人
|
// 发起人
|
||||||
if($process['is_finished'] == true) {
|
if ($process['is_finished']) {
|
||||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $process['start_user_id']);
|
$dialog = WebSocketDialog::checkUserDialog($botUser, $process['start_user_id']);
|
||||||
if (!empty($dialog)) {
|
if (!empty($dialog)) {
|
||||||
$this->approveMsg('approve_submitter', $dialog, $botUser, ['userid' => $data['userid']], $process, $pass);
|
$this->approveMsg('approve_submitter', $dialog, $botUser, ['userid' => $data['userid']], $process, $pass);
|
||||||
}
|
}
|
||||||
}else if ($process['candidate']) {
|
} else if ($process['candidate']) {
|
||||||
// 下个审批人
|
// 下个审批人
|
||||||
$userid = explode(',', $process['candidate']);
|
$userid = explode(',', $process['candidate']);
|
||||||
$toUser = User::whereIn('userid', $userid)->get()->toArray();
|
$toUser = User::whereIn('userid', $userid)->get()->toArray();
|
||||||
@ -278,12 +286,12 @@ class ApproveController extends AbstractController
|
|||||||
if (empty($dialog)) {
|
if (empty($dialog)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process,'start');
|
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process, 'start');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 抄送人
|
// 抄送人
|
||||||
$notifier = $this->handleProcessNode($process, $task['step']);
|
$notifier = $this->handleProcessNode($process);
|
||||||
if ($notifier && $pass == 'pass') {
|
if ($notifier && $pass == 'pass') {
|
||||||
foreach ($notifier as $val) {
|
foreach ($notifier as $val) {
|
||||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $val['target_id']);
|
$dialog = WebSocketDialog::checkUserDialog($botUser, $val['target_id']);
|
||||||
@ -292,11 +300,11 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Base::retSuccess( $pass == 'pass' ? '已通过' : '已拒绝', $task);
|
return Base::retSuccess($pass == 'pass' ? '已通过' : '已拒绝', $task);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/task/withdraw 07. 撤回
|
* @api {post} api/approve/task/withdraw 撤回
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -316,7 +324,7 @@ class ApproveController extends AbstractController
|
|||||||
$data['userid'] = (string)$user->userid;
|
$data['userid'] = (string)$user->userid;
|
||||||
$data['task_id'] = intval(Request::input('task_id'));
|
$data['task_id'] = intval(Request::input('task_id'));
|
||||||
$data['proc_inst_id'] = intval(Request::input('proc_inst_id'));
|
$data['proc_inst_id'] = intval(Request::input('proc_inst_id'));
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/task/withdraw', json_encode(Base::arrayKeyToCamel($data)));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/task/withdraw', json_encode(Base::arrayKeyToCamel($data)));
|
||||||
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$task || $task['status'] != 200) {
|
if (!$task || $task['status'] != 200) {
|
||||||
return Base::retError($task['message'] ?? '撤回失败');
|
return Base::retError($task['message'] ?? '撤回失败');
|
||||||
@ -341,7 +349,38 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/process/findTask 08. 查询需要我审批的流程(审批中)
|
* @api {post} api/approve/process/delById 删除审批(流程实例)
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份;仅可删除已结束的审批,且仅发起人或管理员可删
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup approve
|
||||||
|
* @apiName process__delById
|
||||||
|
*
|
||||||
|
* @apiQuery {Number} proc_inst_id 流程实例ID
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function process__delById()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$data['userid'] = (string)$user->userid;
|
||||||
|
$data['proc_inst_id'] = intval(Request::input('proc_inst_id'));
|
||||||
|
$data['is_admin'] = $user->isAdmin();
|
||||||
|
if ($data['proc_inst_id'] <= 0) {
|
||||||
|
return Base::retError('参数错误');
|
||||||
|
}
|
||||||
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/delById', json_encode(Base::arrayKeyToCamel($data)));
|
||||||
|
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
|
if (!$task || $task['status'] != 200) {
|
||||||
|
return Base::retError($task['message'] ?? '删除失败');
|
||||||
|
}
|
||||||
|
return Base::retSuccess('已删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} api/approve/process/findTask 查询需要我审批的流程(审批中)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -366,7 +405,7 @@ class ApproveController extends AbstractController
|
|||||||
$data['sort'] = Request::input('sort');
|
$data['sort'] = Request::input('sort');
|
||||||
$data['pageIndex'] = intval(Request::input('page'));
|
$data['pageIndex'] = intval(Request::input('page'));
|
||||||
$data['pageSize'] = intval(Request::input('page_size'));
|
$data['pageSize'] = intval(Request::input('page_size'));
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findTask', json_encode(Base::arrayKeyToCamel($data)));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findTask', json_encode(Base::arrayKeyToCamel($data)));
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$process || $process['status'] != 200) {
|
if (!$process || $process['status'] != 200) {
|
||||||
return Base::retError($process['message'] ?? '查询失败');
|
return Base::retError($process['message'] ?? '查询失败');
|
||||||
@ -380,11 +419,11 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
$val['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
|
$val['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
|
||||||
}
|
}
|
||||||
return Base::retSuccess('success',$res);
|
return Base::retSuccess('success', $res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/process/startByMyselfAll 09. 查询我启动的流程(全部)
|
* @api {post} api/approve/process/startByMyselfAll 查询我启动的流程(全部)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -409,7 +448,7 @@ class ApproveController extends AbstractController
|
|||||||
$data['state'] = intval(Request::input('state')); //状态
|
$data['state'] = intval(Request::input('state')); //状态
|
||||||
$data['pageIndex'] = intval(Request::input('page'));
|
$data['pageIndex'] = intval(Request::input('page'));
|
||||||
$data['pageSize'] = intval(Request::input('page_size'));
|
$data['pageSize'] = intval(Request::input('page_size'));
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/startByMyselfAll', json_encode($data));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/startByMyselfAll', json_encode($data));
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$process || $process['status'] != 200) {
|
if (!$process || $process['status'] != 200) {
|
||||||
return Base::retError($process['message'] ?? '查询失败');
|
return Base::retError($process['message'] ?? '查询失败');
|
||||||
@ -427,7 +466,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/process/startByMyself 10. 查询我启动的流程(审批中)
|
* @api {post} api/approve/process/startByMyself 查询我启动的流程(审批中)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -447,7 +486,7 @@ class ApproveController extends AbstractController
|
|||||||
$data['userid'] = (string)$user->userid;
|
$data['userid'] = (string)$user->userid;
|
||||||
$data['pageIndex'] = intval(Request::input('page'));
|
$data['pageIndex'] = intval(Request::input('page'));
|
||||||
$data['pageSize'] = intval(Request::input('page_size'));
|
$data['pageSize'] = intval(Request::input('page_size'));
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/startByMyself', json_encode($data));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/startByMyself', json_encode($data));
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$process || $process['status'] != 200) {
|
if (!$process || $process['status'] != 200) {
|
||||||
return Base::retError($process['message'] ?? '查询失败');
|
return Base::retError($process['message'] ?? '查询失败');
|
||||||
@ -465,7 +504,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/process/findProcNotify 11. 查询抄送我的流程(审批中)
|
* @api {post} api/approve/process/findProcNotify 查询抄送我的流程(审批中)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -491,7 +530,7 @@ class ApproveController extends AbstractController
|
|||||||
$data['pageIndex'] = intval(Request::input('page'));
|
$data['pageIndex'] = intval(Request::input('page'));
|
||||||
$data['pageSize'] = intval(Request::input('page_size'));
|
$data['pageSize'] = intval(Request::input('page_size'));
|
||||||
|
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findProcNotify', json_encode($data));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findProcNotify', json_encode($data));
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$process || $process['status'] != 200) {
|
if (!$process || $process['status'] != 200) {
|
||||||
return Base::retError($process['message'] ?? '查询失败');
|
return Base::retError($process['message'] ?? '查询失败');
|
||||||
@ -509,7 +548,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/approve/identitylink/findParticipant 12. 查询流程实例的参与者(审批中)
|
* @api {get} api/approve/identitylink/findParticipant 查询流程实例的参与者(审批中)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -526,7 +565,7 @@ class ApproveController extends AbstractController
|
|||||||
{
|
{
|
||||||
User::auth();
|
User::auth();
|
||||||
$proc_inst_id = Request::input('proc_inst_id');
|
$proc_inst_id = Request::input('proc_inst_id');
|
||||||
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/identitylink/findParticipant?procInstId=' . $proc_inst_id);
|
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/identitylink/findParticipant?procInstId=' . $proc_inst_id);
|
||||||
$identitylink = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$identitylink = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$identitylink || $identitylink['status'] != 200) {
|
if (!$identitylink || $identitylink['status'] != 200) {
|
||||||
return Base::retError($identitylink['message'] ?? '查询失败');
|
return Base::retError($identitylink['message'] ?? '查询失败');
|
||||||
@ -544,7 +583,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/procHistory/findTask 13. 查询需要我审批的流程(已结束)
|
* @api {post} api/approve/procHistory/findTask 查询需要我审批的流程(已结束)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -569,7 +608,7 @@ class ApproveController extends AbstractController
|
|||||||
$data['sort'] = Request::input('sort');
|
$data['sort'] = Request::input('sort');
|
||||||
$data['pageIndex'] = intval(Request::input('page'));
|
$data['pageIndex'] = intval(Request::input('page'));
|
||||||
$data['pageSize'] = intval(Request::input('page_size'));
|
$data['pageSize'] = intval(Request::input('page_size'));
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/findTask', json_encode(Base::arrayKeyToCamel($data)));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/findTask', json_encode(Base::arrayKeyToCamel($data)));
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$process || $process['status'] != 200) {
|
if (!$process || $process['status'] != 200) {
|
||||||
return Base::retError($process['message'] ?? '查询失败');
|
return Base::retError($process['message'] ?? '查询失败');
|
||||||
@ -587,7 +626,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/procHistory/startByMyself 14. 查询我启动的流程(已结束)
|
* @api {post} api/approve/procHistory/startByMyself 查询我启动的流程(已结束)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -607,7 +646,7 @@ class ApproveController extends AbstractController
|
|||||||
$data['userid'] = (string)$user->userid;
|
$data['userid'] = (string)$user->userid;
|
||||||
$data['pageIndex'] = intval(Request::input('page'));
|
$data['pageIndex'] = intval(Request::input('page'));
|
||||||
$data['pageSize'] = intval(Request::input('page_size'));
|
$data['pageSize'] = intval(Request::input('page_size'));
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/startByMyself', json_encode($data));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/startByMyself', json_encode($data));
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$process || $process['status'] != 200) {
|
if (!$process || $process['status'] != 200) {
|
||||||
return Base::retError($process['message'] ?? '查询失败');
|
return Base::retError($process['message'] ?? '查询失败');
|
||||||
@ -625,7 +664,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/procHistory/findProcNotify 15. 查询抄送我的流程(已结束)
|
* @api {post} api/approve/procHistory/findProcNotify 查询抄送我的流程(已结束)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -651,7 +690,7 @@ class ApproveController extends AbstractController
|
|||||||
$data['pageIndex'] = intval(Request::input('page'));
|
$data['pageIndex'] = intval(Request::input('page'));
|
||||||
$data['pageSize'] = intval(Request::input('page_size'));
|
$data['pageSize'] = intval(Request::input('page_size'));
|
||||||
|
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/findProcNotify', json_encode($data));
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/findProcNotify', json_encode($data));
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$process || $process['status'] != 200) {
|
if (!$process || $process['status'] != 200) {
|
||||||
return Base::retError($process['message'] ?? '查询失败');
|
return Base::retError($process['message'] ?? '查询失败');
|
||||||
@ -669,7 +708,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/approve/identitylinkHistory/findParticipant 16. 查询流程实例的参与者(已结束)
|
* @api {get} api/approve/identitylinkHistory/findParticipant 查询流程实例的参与者(已结束)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -686,7 +725,7 @@ class ApproveController extends AbstractController
|
|||||||
{
|
{
|
||||||
User::auth();
|
User::auth();
|
||||||
$proc_inst_id = Request::input('proc_inst_id');
|
$proc_inst_id = Request::input('proc_inst_id');
|
||||||
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/identitylinkHistory/findParticipant?procInstId=' . $proc_inst_id);
|
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/identitylinkHistory/findParticipant?procInstId=' . $proc_inst_id);
|
||||||
$identitylink = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$identitylink = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$identitylink || $identitylink['status'] != 200) {
|
if (!$identitylink || $identitylink['status'] != 200) {
|
||||||
return Base::retError($identitylink['message'] ?? '查询失败');
|
return Base::retError($identitylink['message'] ?? '查询失败');
|
||||||
@ -704,7 +743,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/approve/process/detail 17. 根据流程ID查询流程详情
|
* @api {get} api/approve/process/detail 根据流程ID查询流程详情
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -726,7 +765,7 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} api/approve/export 18. 导出数据
|
* @api {post} api/approve/export 导出数据
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -750,146 +789,207 @@ class ApproveController extends AbstractController
|
|||||||
$data['isFinished'] = intval(Request::input('is_finished')); //是否完成
|
$data['isFinished'] = intval(Request::input('is_finished')); //是否完成
|
||||||
$date = Request::input('date');
|
$date = Request::input('date');
|
||||||
$data['startTime'] = $date[0]; //开始时间
|
$data['startTime'] = $date[0]; //开始时间
|
||||||
$data['endTime'] =Carbon::parse($date[1])->addDay()->toDateString(); //结束时间 + 1天
|
$data['endTime'] = Carbon::parse($date[1])->addDay()->toDateString(); //结束时间 + 1天
|
||||||
//
|
//
|
||||||
if (empty($name) || empty($date)) {
|
if (empty($name) || empty($date)) {
|
||||||
return Base::retError('参数错误');
|
return Base::retError('参数错误');
|
||||||
}
|
}
|
||||||
if (!(is_array($date) && Base::isDate($date[0]) && Base::isDate($date[1]))) {
|
if (!(is_array($date) && Timer::isDate($date[0]) && Timer::isDate($date[1]))) {
|
||||||
return Base::retError('日期选择错误');
|
return Base::retError('日期选择错误');
|
||||||
}
|
}
|
||||||
if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) {
|
if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) {
|
||||||
return Base::retError('日期范围限制最大35天');
|
return Base::retError('日期范围限制最大35天');
|
||||||
}
|
}
|
||||||
//
|
$botUser = User::botGetOrCreate('system-msg');
|
||||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findAllProcIns', json_encode($data));
|
if (empty($botUser)) {
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
return Base::retError('系统机器人不存在');
|
||||||
if (!$process || $process['status'] != 200) {
|
|
||||||
return Base::retError($process['message'] ?? '查询失败');
|
|
||||||
}
|
}
|
||||||
|
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||||
//
|
//
|
||||||
$res = Base::arrayKeyToUnderline($process['data']);
|
$doo = Doo::load();
|
||||||
//
|
go(function () use ($doo, $data, $user, $botUser, $dialog) {
|
||||||
$headings = [];
|
Coroutine::sleep(1);
|
||||||
$headings[] = '申请编号';
|
|
||||||
$headings[] = '标题';
|
|
||||||
$headings[] = '申请状态';
|
|
||||||
$headings[] = '发起时间';
|
|
||||||
$headings[] = '完成时间';
|
|
||||||
$headings[] = '发起人工号';
|
|
||||||
$headings[] = '发起人User ID';
|
|
||||||
$headings[] = '发起人姓名';
|
|
||||||
$headings[] = '发起人部门';
|
|
||||||
$headings[] = '发起人部门ID';
|
|
||||||
$headings[] = '部门负责人';
|
|
||||||
$headings[] = '历史审批人';
|
|
||||||
$headings[] = '历史办理人';
|
|
||||||
$headings[] = '审批记录';
|
|
||||||
$headings[] = '当前处理人';
|
|
||||||
$headings[] = '审批节点';
|
|
||||||
$headings[] = '审批人数';
|
|
||||||
$headings[] = '审批耗时';
|
|
||||||
$headings[] = '假期类型';
|
|
||||||
$headings[] = '开始时间';
|
|
||||||
$headings[] = '结束时间';
|
|
||||||
$headings[] = '时长';
|
|
||||||
$headings[] = '请假事由';
|
|
||||||
$headings[] = '请假单位';
|
|
||||||
//
|
|
||||||
$sheets = [];
|
|
||||||
$datas = [];
|
|
||||||
foreach ($res as $val) {
|
|
||||||
//
|
//
|
||||||
$nickname = Base::filterEmoji($val['start_user_name']);
|
$content = [];
|
||||||
$participant = $this->getUserProcessParticipantById($val['id']); // 获取参与人
|
$content[] = [
|
||||||
$participant = $this->handleParticipant($val, $participant['data']); // 处理参与人返回数据
|
'content' => '导出审批数据已完成',
|
||||||
//
|
'style' => 'font-weight: bold;padding-bottom: 4px;',
|
||||||
$job_number = ''; // 发起人工号
|
|
||||||
$department_leader = User::userid2nickname(UserDepartment::find(1, ['owner_userid'])['owner_userid']); // 部门负责人
|
|
||||||
$historical_approver = $participant['historical_approver'] ?? ''; // 历史审批人
|
|
||||||
$historical_agent = ''; // 历史办理人
|
|
||||||
$approval_record = $participant['approval_record'] ?? ''; // 审批记录
|
|
||||||
$current_handler = !$val['is_finished'] ? implode(',', User::whereIn('userid', explode(';', $val['candidate']))->pluck('nickname')->toArray()) : ''; // 当前处理人
|
|
||||||
$approved_node = $participant['approved_node'] ?? 0; // 审批节点
|
|
||||||
$approved_num = $participant['approved_num'] ?? 0; // 审批人数
|
|
||||||
// 计算审批耗时
|
|
||||||
$startTime = Carbon::parse($val['start_time'])->timestamp;
|
|
||||||
$endTime = $val['end_time'] ? Carbon::parse($val['end_time'])->timestamp : time();
|
|
||||||
$approval_time = Base::timeDiff($startTime, $endTime); // 审批耗时
|
|
||||||
// 计算时长
|
|
||||||
$varStartTime = Carbon::parse($val['var']['start_time']);
|
|
||||||
$varEndTime = Carbon::parse($val['var']['end_time']);
|
|
||||||
$duration = $varEndTime->floatDiffInHours($varStartTime);
|
|
||||||
$duration_unit = '小时'; // 时长单位
|
|
||||||
$datas[] = [
|
|
||||||
$val['id'], // 申请编号
|
|
||||||
$val['proc_def_name'], // 标题
|
|
||||||
$this->getStateDescription($val['state']), // 申请状态
|
|
||||||
$val['start_time'], // 发起时间
|
|
||||||
$val['end_time'], // 完成时间
|
|
||||||
$job_number, // 发起人工号
|
|
||||||
$val['start_user_id'], // 发起人User ID
|
|
||||||
$nickname, // 发起人姓名
|
|
||||||
$val['department'], // 发起人部门
|
|
||||||
$val['department_id'], // 发起人部门ID
|
|
||||||
$department_leader, // 部门负责人
|
|
||||||
$historical_approver, // 历史审批人
|
|
||||||
$historical_agent, // 历史办理人
|
|
||||||
$approval_record, // 审批记录
|
|
||||||
$current_handler, // 当前处理人
|
|
||||||
$approved_node, // 审批节点
|
|
||||||
$approved_num, // 审批人数
|
|
||||||
$approval_time, // 审批耗时
|
|
||||||
$val['var']['type'], // 假期类型
|
|
||||||
$val['var']['start_time'], // 开始时间
|
|
||||||
$val['var']['end_time'], // 结束时间
|
|
||||||
$duration, // 时长
|
|
||||||
$val['var']['description'], // 请假事由
|
|
||||||
$duration_unit, // 请假单位
|
|
||||||
];
|
];
|
||||||
}
|
//
|
||||||
if (empty($datas)) {
|
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findAllProcIns', json_encode($data));
|
||||||
return Base::retError('没有任何数据');
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
}
|
if (!$process || $process['status'] != 200) {
|
||||||
|
$content[] = [
|
||||||
|
'content' => $process['message'] ?? '查询失败',
|
||||||
|
'style' => 'color: #ff0000;',
|
||||||
|
];
|
||||||
|
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||||
|
'type' => 'content',
|
||||||
|
'title' => $content[0]['content'],
|
||||||
|
'content' => $content,
|
||||||
|
], $botUser->userid, true, false, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
$res = Base::arrayKeyToUnderline($process['data']);
|
||||||
|
//
|
||||||
|
$headings = [];
|
||||||
|
$headings[] = $doo->translate('申请编号');
|
||||||
|
$headings[] = $doo->translate('标题');
|
||||||
|
$headings[] = $doo->translate('申请状态');
|
||||||
|
$headings[] = $doo->translate('发起时间');
|
||||||
|
$headings[] = $doo->translate('完成时间');
|
||||||
|
$headings[] = $doo->translate('发起人工号');
|
||||||
|
$headings[] = $doo->translate('发起人User ID');
|
||||||
|
$headings[] = $doo->translate('发起人姓名');
|
||||||
|
$headings[] = $doo->translate('发起人部门');
|
||||||
|
$headings[] = $doo->translate('发起人部门ID');
|
||||||
|
$headings[] = $doo->translate('部门负责人');
|
||||||
|
$headings[] = $doo->translate('历史审批人');
|
||||||
|
$headings[] = $doo->translate('历史办理人');
|
||||||
|
$headings[] = $doo->translate('审批记录');
|
||||||
|
$headings[] = $doo->translate('当前处理人');
|
||||||
|
$headings[] = $doo->translate('审批节点');
|
||||||
|
$headings[] = $doo->translate('审批人数');
|
||||||
|
$headings[] = $doo->translate('审批耗时');
|
||||||
|
$headings[] = $doo->translate('假期类型');
|
||||||
|
$headings[] = $doo->translate('开始时间');
|
||||||
|
$headings[] = $doo->translate('结束时间');
|
||||||
|
$headings[] = $doo->translate('时长');
|
||||||
|
$headings[] = $doo->translate('请假事由');
|
||||||
|
$headings[] = $doo->translate('请假单位');
|
||||||
|
//
|
||||||
|
$datas = [];
|
||||||
|
foreach ($res as $val) {
|
||||||
|
//
|
||||||
|
$nickname = Base::filterEmoji($val['start_user_name']);
|
||||||
|
$participant = $this->getUserProcessParticipantById($val['id']); // 获取参与人
|
||||||
|
$participant = $this->handleParticipant($val, $participant['data']); // 处理参与人返回数据
|
||||||
|
//
|
||||||
|
$job_number = ''; // 发起人工号
|
||||||
|
$department_leader = User::userid2nickname(UserDepartment::find(1, ['owner_userid'])['owner_userid']); // 部门负责人
|
||||||
|
$historical_approver = $participant['historical_approver'] ?? ''; // 历史审批人
|
||||||
|
$historical_agent = ''; // 历史办理人
|
||||||
|
$approval_record = $participant['approval_record'] ?? ''; // 审批记录
|
||||||
|
$current_handler = !$val['is_finished'] ? implode(',', User::whereIn('userid', explode(';', $val['candidate']))->pluck('nickname')->toArray()) : ''; // 当前处理人
|
||||||
|
$approved_node = $participant['approved_node'] ?? 0; // 审批节点
|
||||||
|
$approved_num = $participant['approved_num'] ?? 0; // 审批人数
|
||||||
|
// 计算审批耗时
|
||||||
|
$startTime = Carbon::parse($val['start_time'])->timestamp;
|
||||||
|
$endTime = $val['end_time'] ? Carbon::parse($val['end_time'])->timestamp : time();
|
||||||
|
$approval_time = $doo->translate(Timer::timeDiff($startTime, $endTime)); // 审批耗时
|
||||||
|
// 计算时长
|
||||||
|
$varStartTime = Carbon::parse($val['var']['start_time']);
|
||||||
|
$varEndTime = Carbon::parse($val['var']['end_time']);
|
||||||
|
$duration = $varEndTime->floatDiffInHours($varStartTime);
|
||||||
|
$duration_unit = $doo->translate('小时'); // 时长单位
|
||||||
|
$datas[] = [
|
||||||
|
$val['id'], // 申请编号
|
||||||
|
$val['proc_def_name'], // 标题
|
||||||
|
$this->getStateDescription($val['state']), // 申请状态
|
||||||
|
$val['start_time'], // 发起时间
|
||||||
|
$val['end_time'], // 完成时间
|
||||||
|
$job_number, // 发起人工号
|
||||||
|
$val['start_user_id'], // 发起人User ID
|
||||||
|
$nickname, // 发起人姓名
|
||||||
|
$val['department'], // 发起人部门
|
||||||
|
$val['department_id'], // 发起人部门ID
|
||||||
|
$department_leader, // 部门负责人
|
||||||
|
$historical_approver, // 历史审批人
|
||||||
|
$historical_agent, // 历史办理人
|
||||||
|
$approval_record, // 审批记录
|
||||||
|
$current_handler, // 当前处理人
|
||||||
|
$approved_node, // 审批节点
|
||||||
|
$approved_num, // 审批人数
|
||||||
|
$approval_time, // 审批耗时
|
||||||
|
$val['var']['type'], // 假期类型
|
||||||
|
$val['var']['start_time'], // 开始时间
|
||||||
|
$val['var']['end_time'], // 结束时间
|
||||||
|
$duration, // 时长
|
||||||
|
$val['var']['description'], // 请假事由
|
||||||
|
$duration_unit, // 请假单位
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (empty($datas)) {
|
||||||
|
$content[] = [
|
||||||
|
'content' => '没有任何数据',
|
||||||
|
'style' => 'color: #ff0000;',
|
||||||
|
];
|
||||||
|
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||||
|
'type' => 'content',
|
||||||
|
'title' => $content[0]['content'],
|
||||||
|
'content' => $content,
|
||||||
|
], $botUser->userid, true, false, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
$title = $doo->translate("审批记录");
|
||||||
|
$sheets = [
|
||||||
|
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles(["A1:Y1" => ["font" => ["bold" => true]]])
|
||||||
|
];
|
||||||
|
//
|
||||||
|
$fileName = $title . '_' . Timer::time() . '.xlsx';
|
||||||
|
$filePath = "temp/approve/export/" . date("Ym", Timer::time());
|
||||||
|
$export = new BillMultipleExport($sheets);
|
||||||
|
$res = $export->store($filePath . "/" . $fileName);
|
||||||
|
if ($res != 1) {
|
||||||
|
$content[] = [
|
||||||
|
'content' => "导出失败,{$fileName}!",
|
||||||
|
'style' => 'color: #ff0000;',
|
||||||
|
];
|
||||||
|
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||||
|
'type' => 'content',
|
||||||
|
'title' => $content[0]['content'],
|
||||||
|
'content' => $content,
|
||||||
|
], $botUser->userid, true, false, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
|
||||||
|
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
|
||||||
|
$zipPath = storage_path($zipFile);
|
||||||
|
if (file_exists($zipPath)) {
|
||||||
|
Base::deleteDirAndFile($zipPath, true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
//
|
||||||
|
if (file_exists($zipPath)) {
|
||||||
|
$key = Down::cache_encode([
|
||||||
|
'file' => $zipFile,
|
||||||
|
]);
|
||||||
|
$fileUrl = Base::fillUrl('api/approve/down?key=' . $key);
|
||||||
|
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||||
|
'type' => 'file_download',
|
||||||
|
'title' => '导出审批数据已完成',
|
||||||
|
'name' => $fileName,
|
||||||
|
'size' => filesize($zipPath),
|
||||||
|
'url' => $fileUrl,
|
||||||
|
], $botUser->userid, true, false, true);
|
||||||
|
} else {
|
||||||
|
$content[] = [
|
||||||
|
'content' => "打包失败,请稍后再试...",
|
||||||
|
'style' => 'color: #ff0000;',
|
||||||
|
];
|
||||||
|
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||||
|
'type' => 'content',
|
||||||
|
'title' => $content[0]['content'],
|
||||||
|
'content' => $content,
|
||||||
|
], $botUser->userid, true, false, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
//
|
//
|
||||||
$title = "Sheet1";
|
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||||
$sheets = [
|
'type' => 'content',
|
||||||
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles(["A1:Y1" => ["font" => ["bold" => true]]])
|
'content' => '正在导出审批数据,请稍等...',
|
||||||
];
|
], $botUser->userid, true, false, true);
|
||||||
//
|
//
|
||||||
$fileName = '审批记录_' . Base::time() . '.xls';
|
return Base::retSuccess('success');
|
||||||
$filePath = "temp/approve/export/" . date("Ym", Base::time());
|
|
||||||
$export = new BillMultipleExport($sheets);
|
|
||||||
$res = $export->store($filePath . "/" . $fileName);
|
|
||||||
if ($res != 1) {
|
|
||||||
return Base::retError('导出失败,' . $fileName . '!');
|
|
||||||
}
|
|
||||||
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
|
|
||||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
|
|
||||||
$zipPath = storage_path($zipFile);
|
|
||||||
if (file_exists($zipPath)) {
|
|
||||||
Base::deleteDirAndFile($zipPath, true);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
|
||||||
} catch (\Throwable) {
|
|
||||||
}
|
|
||||||
//
|
|
||||||
if (file_exists($zipPath)) {
|
|
||||||
$base64 = base64_encode(Base::array2string([
|
|
||||||
'file' => $zipFile,
|
|
||||||
]));
|
|
||||||
Session::put('approve::export:userid', $user->userid);
|
|
||||||
return Base::retSuccess('success', [
|
|
||||||
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
|
|
||||||
'url' => Base::fillUrl('api/approve/down?key=' . urlencode($base64)),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
return Base::retError('打包失败,请稍后再试...');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStateDescription($state) {
|
function getStateDescription($state)
|
||||||
|
{
|
||||||
$state_map = array(
|
$state_map = array(
|
||||||
0 => '全部',
|
0 => '全部',
|
||||||
1 => '审批中',
|
1 => '审批中',
|
||||||
@ -897,14 +997,14 @@ class ApproveController extends AbstractController
|
|||||||
3 => '拒绝',
|
3 => '拒绝',
|
||||||
4 => '撤回'
|
4 => '撤回'
|
||||||
);
|
);
|
||||||
return isset($state_map[$state]) ? $state_map[$state] : '';
|
return $state_map[$state] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/approve/down 19. 下载导出的审批数据
|
* @api {get} api/approve/down 下载导出的审批数据
|
||||||
*
|
*
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup system
|
* @apiGroup approve
|
||||||
* @apiName down
|
* @apiName down
|
||||||
*
|
*
|
||||||
* @apiParam {String} key 通过export接口得到的下载钥匙
|
* @apiParam {String} key 通过export接口得到的下载钥匙
|
||||||
@ -913,15 +1013,10 @@ class ApproveController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function down()
|
public function down()
|
||||||
{
|
{
|
||||||
$userid = Session::get('approve::export:userid');
|
$array = Down::cache_decode();
|
||||||
if (empty($userid)) {
|
|
||||||
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502);
|
|
||||||
}
|
|
||||||
//
|
|
||||||
$array = Base::string2array(base64_decode(urldecode(Request::input('key'))));
|
|
||||||
$file = $array['file'];
|
$file = $array['file'];
|
||||||
if (empty($file) || !file_exists(storage_path($file))) {
|
if (empty($file) || !file_exists(storage_path($file))) {
|
||||||
return Base::ajaxError("文件不存在!", [], 0, 502);
|
return Base::ajaxError("文件不存在!", [], 0, 403);
|
||||||
}
|
}
|
||||||
return Response::download(storage_path($file));
|
return Response::download(storage_path($file));
|
||||||
}
|
}
|
||||||
@ -951,20 +1046,19 @@ class ApproveController extends AbstractController
|
|||||||
}
|
}
|
||||||
// 审批记录
|
// 审批记录
|
||||||
$name = $val['username'] . '|';
|
$name = $val['username'] . '|';
|
||||||
$call = $val['step'] == 0 ? '发起审批'. '|' : '同意' . '|';
|
$call = $val['step'] == 0 ? '发起审批' . '|' : '同意' . '|';
|
||||||
$time =$val['step'] == 0 ? $process['start_time'] . '|' : '';
|
$time = $val['step'] == 0 ? $process['start_time'] . '|' : '';
|
||||||
$comment = $val['step'] == 0 ? '' : ($val['comment'] ?? '') . '|';
|
$comment = $val['step'] == 0 ? '' : ($val['comment'] ?? '') . '|';
|
||||||
$res['approval_record'] .= $name . $call . $time . $comment;
|
$res['approval_record'] .= $name . $call . $time . $comment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$res['historical_approver'] = trim(implode(';', $historical_approver), ';');
|
$res['historical_approver'] = trim(implode(';', $historical_approver), ';');
|
||||||
$res['approved_node'] = $approved_node;
|
$res['approved_node'] = $approved_node;
|
||||||
$res['approved_num'] = $approved_num;
|
$res['approved_num'] = $approved_num;
|
||||||
$res['historical_agent'] = $res['historical_approver'];
|
$res['historical_agent'] = $res['historical_approver'];
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 审批机器人消息
|
// 审批机器人消息
|
||||||
public function approveMsg($type, $dialog, $botUser, $toUser, $process, $action = null)
|
public function approveMsg($type, $dialog, $botUser, $toUser, $process, $action = null)
|
||||||
{
|
{
|
||||||
@ -976,50 +1070,71 @@ class ApproveController extends AbstractController
|
|||||||
'department' => $process['department'],
|
'department' => $process['department'],
|
||||||
'type' => $process['var']['type'],
|
'type' => $process['var']['type'],
|
||||||
'start_time' => $process['var']['start_time'],
|
'start_time' => $process['var']['start_time'],
|
||||||
'start_day_of_week' => '周'.Base::getTimeWeek(Carbon::parse($process['var']['start_time'])->timestamp),
|
'start_day_of_week' => '周' . Timer::getWeek(Carbon::parse($process['var']['start_time'])->timestamp),
|
||||||
'end_time' => $process['var']['end_time'],
|
'end_time' => $process['var']['end_time'],
|
||||||
'end_day_of_week' => '周'.Base::getTimeWeek(Carbon::parse($process['var']['end_time'])->timestamp),
|
'end_day_of_week' => '周' . Timer::getWeek(Carbon::parse($process['var']['end_time'])->timestamp),
|
||||||
'description' => $process['var']['description'],
|
'description' => $process['var']['description'],
|
||||||
'comment_nickname' => $process['comment_user_id'] ? User::userid2nickname($process['comment_user_id']) : '',
|
'comment_nickname' => $process['comment_user_id'] ? User::userid2nickname($process['comment_user_id']) : '',
|
||||||
'comment_content' => $process['comment_content'] ?? ''
|
'comment_content' => $process['comment_contents']['content'] ?? '',
|
||||||
|
'comment_pictures' => $process['comment_contents']['pictures'] ?? []
|
||||||
];
|
];
|
||||||
$text = view('push.bot', ['type' => $type, 'action' => $action, 'is_finished' => $process['is_finished'], 'data' => (object)$data])->render();
|
$thumb = null;
|
||||||
$text = preg_replace("/^\x20+/", "", $text);
|
if ($type === 'approve_reviewer') {
|
||||||
$text = preg_replace("/\n\x20+/", "\n", $text);
|
$thumb = $process['var']['other'];
|
||||||
$msg_action = null;
|
} elseif ($type === 'approve_comment_notifier') {
|
||||||
|
$thumb = $data['comment_pictures'] ? $data['comment_pictures'][0] : null;
|
||||||
|
}
|
||||||
|
if ($thumb && file_exists(public_path($thumb))) {
|
||||||
|
$imageSize = getimagesize(public_path($thumb));
|
||||||
|
$data['thumb'] = [
|
||||||
|
'url' => Base::fillUrl($thumb),
|
||||||
|
'width' => $imageSize[0],
|
||||||
|
'height' => $imageSize[1]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$msgAction = null;
|
||||||
|
$msgData = [
|
||||||
|
'type' => $type,
|
||||||
|
'action' => $action,
|
||||||
|
'is_finished' => $process['is_finished'],
|
||||||
|
'data' => $data
|
||||||
|
];
|
||||||
|
$msgData['title'] = match ($type) {
|
||||||
|
'approve_reviewer' => $data['nickname'] . " 提交的「{$data['proc_def_name']}」待你审批",
|
||||||
|
'approve_notifier' => "抄送 {$data['nickname']} 提交的「{$data['proc_def_name']}」记录",
|
||||||
|
'approve_comment_notifier' => $data['comment_nickname'] . " 评论了 {$data['nickname']} 的「{$data['proc_def_name']}」审批",
|
||||||
|
'approve_submitter' => $action == 'pass' ? "您发起的「{$data['proc_def_name']}」已通过" : "您发起的「{$data['proc_def_name']}」被 {$data['nickname']} 拒绝",
|
||||||
|
default => '不支持的指令',
|
||||||
|
};
|
||||||
if ($action == 'withdraw' || $action == 'pass' || $action == 'refuse') {
|
if ($action == 'withdraw' || $action == 'pass' || $action == 'refuse') {
|
||||||
// 任务完成,给发起人发送消息
|
// 任务完成,给发起人发送消息
|
||||||
if($type == 'approve_submitter' && $action != 'withdraw'){
|
if ($type == 'approve_submitter' && $action != 'withdraw') {
|
||||||
return WebSocketDialogMsg::sendMsg($msg_action, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
|
return WebSocketDialogMsg::sendMsg($msgAction, $dialog->id, 'template', $msgData, $botUser->userid, false, false, true);
|
||||||
}
|
}
|
||||||
// 查找最后一条消息msg_id
|
// 查找最后一条消息msg_id
|
||||||
$msg_action = 'update-'.$toUser['msg_id'];
|
$msgAction = 'change-' . $toUser['msg_id'];
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
try {
|
$msg = WebSocketDialogMsg::sendMsg($msgAction, $dialog->id, 'template', $msgData, $process['start_user_id'], false, false, true);
|
||||||
$msg = WebSocketDialogMsg::sendMsg($msg_action, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
|
// 关联信息
|
||||||
// 关联信息
|
if ($action == 'start') {
|
||||||
if ($action == 'start') {
|
$proc_msg = new ApproveProcMsg();
|
||||||
$proc_msg = new ApproveProcMsg();
|
$proc_msg->proc_inst_id = $process['id'];
|
||||||
$proc_msg->proc_inst_id = $process['id'];
|
$proc_msg->msg_id = $msg['data']->id;
|
||||||
$proc_msg->msg_id = $msg['data']->id;
|
$proc_msg->userid = $toUser['userid'];
|
||||||
$proc_msg->userid = $toUser['userid'];
|
$proc_msg->save();
|
||||||
$proc_msg->save();
|
}
|
||||||
}
|
// 更新审批 未读数量
|
||||||
// 更新工作报告 未读数量
|
if ($type == 'approve_reviewer' && $toUser['userid']) {
|
||||||
if($type == 'approve_reviewer' && $toUser['userid']){
|
$params = [
|
||||||
$params = [
|
'userid' => [$toUser['userid'], User::userid()],
|
||||||
'userid' => [ $toUser['userid'], User::auth()->userid() ],
|
'msg' => [
|
||||||
'msg' => [
|
'type' => 'approve',
|
||||||
'type' => 'approve',
|
'action' => 'unread',
|
||||||
'action' => 'unread',
|
'userid' => $toUser['userid'],
|
||||||
'userid' => $toUser['userid'],
|
]
|
||||||
]
|
];
|
||||||
];
|
Task::deliver(new PushTask($params, false));
|
||||||
Task::deliver(new PushTask($params, false));
|
|
||||||
}
|
|
||||||
} catch (\Throwable $th) {
|
|
||||||
//throw $th;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1028,7 +1143,7 @@ class ApproveController extends AbstractController
|
|||||||
public function getProcessById($id)
|
public function getProcessById($id)
|
||||||
{
|
{
|
||||||
$data['id'] = intval($id);
|
$data['id'] = intval($id);
|
||||||
$ret = Ihttp::ihttp_get($this->flow_url."/api/v1/workflow/process/findById?".http_build_query($data));
|
$ret = Ihttp::ihttp_get($this->flow_url . "/api/v1/workflow/process/findById?" . http_build_query($data));
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$process || $process['status'] != 200) {
|
if (!$process || $process['status'] != 200) {
|
||||||
throw new ApiException($process['message'] ?? '查询失败');
|
throw new ApiException($process['message'] ?? '查询失败');
|
||||||
@ -1047,15 +1162,16 @@ class ApproveController extends AbstractController
|
|||||||
$val['node_user_list'][$k]['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
|
$val['node_user_list'][$k]['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
|
||||||
$res['userids'][] = $item['target_id'];
|
$res['userids'][] = $item['target_id'];
|
||||||
}
|
}
|
||||||
}else if($val['aprover_id']){
|
} else if ($val['aprover_id']) {
|
||||||
$info = User::whereUserid($val['aprover_id'])->first();
|
$info = User::whereUserid($val['aprover_id'])->first();
|
||||||
$val['userimg'] = $info ? User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname) : '';
|
$val['userimg'] = $info ? User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname) : '';
|
||||||
$res['userids'][] = $val['aprover_id'];
|
$res['userids'][] = $val['aprover_id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 全局评论
|
// 全局评论
|
||||||
if(isset($res['global_comments'])){
|
unset($res['global_comment']);
|
||||||
foreach ($res['global_comments'] as $k => &$globalComment) {
|
if (isset($res['global_comments'])) {
|
||||||
|
foreach ($res['global_comments'] as $k => $globalComment) {
|
||||||
$info = User::whereUserid($globalComment['user_id'])->first();
|
$info = User::whereUserid($globalComment['user_id'])->first();
|
||||||
if (!$info) {
|
if (!$info) {
|
||||||
continue;
|
continue;
|
||||||
@ -1063,6 +1179,8 @@ class ApproveController extends AbstractController
|
|||||||
$res['global_comments'][$k]['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
|
$res['global_comments'][$k]['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
|
||||||
$res['global_comments'][$k]['nickname'] = $info->nickname;
|
$res['global_comments'][$k]['nickname'] = $info->nickname;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
$res['global_comments'] = [];
|
||||||
}
|
}
|
||||||
$info = User::whereUserid($res['start_user_id'])->first();
|
$info = User::whereUserid($res['start_user_id'])->first();
|
||||||
$res['userimg'] = $info ? User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname) : '';
|
$res['userimg'] = $info ? User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname) : '';
|
||||||
@ -1095,7 +1213,7 @@ class ApproveController extends AbstractController
|
|||||||
public function getUserProcessParticipantById($id)
|
public function getUserProcessParticipantById($id)
|
||||||
{
|
{
|
||||||
$data['procInstId'] = intval($id);
|
$data['procInstId'] = intval($id);
|
||||||
$ret = Ihttp::ihttp_get($this->flow_url."/api/v1/workflow/identitylink/findParticipantAll?".http_build_query($data));
|
$ret = Ihttp::ihttp_get($this->flow_url . "/api/v1/workflow/identitylink/findParticipantAll?" . http_build_query($data));
|
||||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
if (!$process || $process['status'] != 200) {
|
if (!$process || $process['status'] != 200) {
|
||||||
throw new ApiException($process['message'] ?? '查询失败');
|
throw new ApiException($process['message'] ?? '查询失败');
|
||||||
@ -1105,10 +1223,10 @@ class ApproveController extends AbstractController
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/approve/user/status 20. 获取用户审批状态
|
* @api {get} api/approve/user/status 获取用户审批状态
|
||||||
*
|
*
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup system
|
* @apiGroup approve
|
||||||
* @apiName user__status
|
* @apiName user__status
|
||||||
*
|
*
|
||||||
* @apiParam {String} userid
|
* @apiParam {String} userid
|
||||||
@ -1119,14 +1237,32 @@ class ApproveController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function user__status()
|
public function user__status()
|
||||||
{
|
{
|
||||||
$data['userid'] = intval(Request::input('userid'));
|
$userid = intval(Request::input('userid'));
|
||||||
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/process/getUserApprovalStatus?'.http_build_query($data));
|
$status = ApproveProcInstHistory::getUserApprovalStatus($userid);
|
||||||
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
return Base::retSuccess('success', $status);
|
||||||
if (isset($procdef['status']) && $procdef['status'] == 200) {
|
|
||||||
return Base::retSuccess('success', isset($procdef['data']["proc_def_name"]) ? $procdef['data']["proc_def_name"] : '');
|
|
||||||
}
|
|
||||||
return Base::retSuccess('success', '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/approve/process/doto 查询需要我审批的流程数量
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup approve
|
||||||
|
* @apiName process__doto
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function process__doto()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/process/findTaskTotal?userid=' . $user->userid);
|
||||||
|
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||||
|
if (!$process || $process['status'] != 200) {
|
||||||
|
return Base::retError($process['message'] ?? '查询失败');
|
||||||
|
}
|
||||||
|
return Base::retSuccess('success', $process['data']);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
307
app/Http/Controllers/Api/AssistantController.php
Normal file
307
app/Http/Controllers/Api/AssistantController.php
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Models\AiAssistantSession;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Module\AI;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Base;
|
||||||
|
use Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiDefine assistant
|
||||||
|
*
|
||||||
|
* 助手
|
||||||
|
*/
|
||||||
|
class AssistantController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
Apps::isInstalledThrow('ai');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} api/assistant/auth 生成授权码
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份,生成 AI 流式会话的 stream_key
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup assistant
|
||||||
|
* @apiName auth
|
||||||
|
*
|
||||||
|
* @apiParam {String} model_type 模型类型
|
||||||
|
* @apiParam {String} model_name 模型名称
|
||||||
|
* @apiParam {JSON} context 上下文数组
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
* @apiSuccess {String} data.stream_key 流式会话凭证
|
||||||
|
*/
|
||||||
|
public function auth()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$user->checkChatInformation();
|
||||||
|
|
||||||
|
$modelType = trim(Request::input('model_type', ''));
|
||||||
|
$modelName = trim(Request::input('model_name', ''));
|
||||||
|
$contextInput = Request::input('context', []);
|
||||||
|
|
||||||
|
return AI::createStreamKey($modelType, $modelName, $contextInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/assistant/models 获取AI模型
|
||||||
|
*
|
||||||
|
* @apiDescription 获取所有AI机器人模型设置
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup assistant
|
||||||
|
* @apiName models
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function models()
|
||||||
|
{
|
||||||
|
$setting = Base::setting('aibotSetting');
|
||||||
|
$setting = array_filter($setting, function ($value, $key) {
|
||||||
|
return str_ends_with($key, '_models') || str_ends_with($key, '_model');
|
||||||
|
}, ARRAY_FILTER_USE_BOTH);
|
||||||
|
|
||||||
|
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} api/assistant/match-elements 元素向量匹配
|
||||||
|
*
|
||||||
|
* @apiDescription 通过向量相似度匹配页面元素,用于智能查找与查询语义相关的元素
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup assistant
|
||||||
|
* @apiName match_elements
|
||||||
|
*
|
||||||
|
* @apiParam {String} query 搜索关键词
|
||||||
|
* @apiParam {Array} elements 元素列表,每个元素包含 ref 和 name 字段
|
||||||
|
* @apiParam {Number} [top_k=10] 返回的匹配数量,最大50
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
* @apiSuccess {Array} data.matches 匹配结果数组,按相似度降序排列
|
||||||
|
*/
|
||||||
|
public function match_elements()
|
||||||
|
{
|
||||||
|
User::auth();
|
||||||
|
|
||||||
|
$query = trim(Request::input('query', ''));
|
||||||
|
$elements = Request::input('elements', []);
|
||||||
|
$topK = min(intval(Request::input('top_k', 10)), 50);
|
||||||
|
|
||||||
|
if (empty($query) || empty($elements)) {
|
||||||
|
return Base::retError('参数不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取查询向量
|
||||||
|
$queryResult = AI::getEmbedding($query);
|
||||||
|
if (Base::isError($queryResult)) {
|
||||||
|
return $queryResult;
|
||||||
|
}
|
||||||
|
$queryVector = $queryResult['data'];
|
||||||
|
|
||||||
|
// 计算相似度并排序
|
||||||
|
$scored = [];
|
||||||
|
foreach ($elements as $el) {
|
||||||
|
$name = $el['name'] ?? '';
|
||||||
|
if (empty($name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$elResult = AI::getEmbedding($name);
|
||||||
|
if (Base::isError($elResult)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$similarity = $this->cosineSimilarity($queryVector, $elResult['data']);
|
||||||
|
$scored[] = [
|
||||||
|
'element' => $el,
|
||||||
|
'similarity' => $similarity,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按相似度降序排序
|
||||||
|
usort($scored, fn($a, $b) => $b['similarity'] <=> $a['similarity']);
|
||||||
|
|
||||||
|
return Base::retSuccess('success', [
|
||||||
|
'matches' => array_slice($scored, 0, $topK),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算两个向量的余弦相似度
|
||||||
|
*/
|
||||||
|
private function cosineSimilarity(array $a, array $b): float
|
||||||
|
{
|
||||||
|
$dotProduct = 0;
|
||||||
|
$normA = 0;
|
||||||
|
$normB = 0;
|
||||||
|
$count = count($a);
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$dotProduct += $a[$i] * $b[$i];
|
||||||
|
$normA += $a[$i] * $a[$i];
|
||||||
|
$normB += $b[$i] * $b[$i];
|
||||||
|
}
|
||||||
|
$denominator = sqrt($normA) * sqrt($normB);
|
||||||
|
if ($denominator == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return $dotProduct / $denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话列表
|
||||||
|
*/
|
||||||
|
public function session__list()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$sessionKey = trim(Request::input('session_key', 'default'));
|
||||||
|
|
||||||
|
$sessions = AiAssistantSession::where('userid', $user->userid)
|
||||||
|
->where('session_key', $sessionKey)
|
||||||
|
->orderByDesc('updated_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$list = [];
|
||||||
|
foreach ($sessions as $session) {
|
||||||
|
$data = Base::json2array($session->data);
|
||||||
|
$images = Base::json2array($session->images);
|
||||||
|
foreach ($images as $imageId => $path) {
|
||||||
|
$images[$imageId] = Base::fillUrl($path);
|
||||||
|
}
|
||||||
|
$list[] = [
|
||||||
|
'id' => $session->session_id,
|
||||||
|
'title' => $session->title,
|
||||||
|
'responses' => $data,
|
||||||
|
'images' => $images,
|
||||||
|
'sceneKey' => $session->scene_key,
|
||||||
|
'createdAt' => $session->created_at ? $session->created_at->getTimestampMs() : 0,
|
||||||
|
'updatedAt' => $session->updated_at ? $session->updated_at->getTimestampMs() : 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('success', $list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存会话
|
||||||
|
*/
|
||||||
|
public function session__save()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$sessionKey = trim(Request::input('session_key', 'default'));
|
||||||
|
$sessionId = trim(Request::input('session_id', ''));
|
||||||
|
$sceneKey = trim(Request::input('scene_key', ''));
|
||||||
|
$title = trim(Request::input('title', ''));
|
||||||
|
$data = Request::input('data', []);
|
||||||
|
$newImages = Request::input('new_images', []);
|
||||||
|
|
||||||
|
if (empty($sessionId)) {
|
||||||
|
return Base::retError('session_id 不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$newImageUrls = [];
|
||||||
|
if (is_array($newImages)) {
|
||||||
|
$path = 'uploads/assistant/' . date('Ym') . '/' . $user->userid . '/';
|
||||||
|
foreach ($newImages as $img) {
|
||||||
|
$imageId = $img['imageId'] ?? '';
|
||||||
|
$dataUrl = $img['dataUrl'] ?? '';
|
||||||
|
if (empty($imageId) || empty($dataUrl)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$result = Base::image64save([
|
||||||
|
'image64' => $dataUrl,
|
||||||
|
'path' => $path,
|
||||||
|
'autoThumb' => false,
|
||||||
|
]);
|
||||||
|
if (Base::isSuccess($result)) {
|
||||||
|
$newImageUrls[$imageId] = $result['data']['path'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$session = AiAssistantSession::where('userid', $user->userid)
|
||||||
|
->where('session_key', $sessionKey)
|
||||||
|
->where('session_id', $sessionId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$imageMap = $newImageUrls;
|
||||||
|
if ($session) {
|
||||||
|
$existingImages = Base::json2array($session->images);
|
||||||
|
$imageMap = array_merge($existingImages, $newImageUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
$session = AiAssistantSession::createInstance([
|
||||||
|
'userid' => $user->userid,
|
||||||
|
'session_key' => $sessionKey,
|
||||||
|
'session_id' => $sessionId,
|
||||||
|
'scene_key' => $sceneKey,
|
||||||
|
'title' => mb_substr($title, 0, 255),
|
||||||
|
'data' => Base::array2json(is_array($data) ? $data : []),
|
||||||
|
'images' => Base::array2json($imageMap),
|
||||||
|
], $session?->id);
|
||||||
|
$session->save();
|
||||||
|
|
||||||
|
// 仅返回本次新增的图片URL
|
||||||
|
$urls = [];
|
||||||
|
foreach ($newImageUrls as $imageId => $path) {
|
||||||
|
$urls[$imageId] = Base::fillUrl($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('success', [
|
||||||
|
'image_urls' => $urls,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会话
|
||||||
|
*/
|
||||||
|
public function session__delete()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$sessionKey = trim(Request::input('session_key', 'default'));
|
||||||
|
$sessionId = trim(Request::input('session_id', ''));
|
||||||
|
$clearAll = Request::input('clear_all', false);
|
||||||
|
|
||||||
|
$query = AiAssistantSession::where('userid', $user->userid)
|
||||||
|
->where('session_key', $sessionKey);
|
||||||
|
|
||||||
|
if ($clearAll) {
|
||||||
|
$sessions = $query->get();
|
||||||
|
foreach ($sessions as $session) {
|
||||||
|
$this->deleteSessionImages($session);
|
||||||
|
}
|
||||||
|
$query->delete();
|
||||||
|
} else {
|
||||||
|
if (empty($sessionId)) {
|
||||||
|
return Base::retError('session_id 不能为空');
|
||||||
|
}
|
||||||
|
$session = $query->where('session_id', $sessionId)->first();
|
||||||
|
if ($session) {
|
||||||
|
$this->deleteSessionImages($session);
|
||||||
|
$session->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('success');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteSessionImages(AiAssistantSession $session)
|
||||||
|
{
|
||||||
|
$images = Base::json2array($session->images);
|
||||||
|
foreach ($images as $path) {
|
||||||
|
$fullPath = public_path($path);
|
||||||
|
if (file_exists($fullPath)) {
|
||||||
|
@unlink($fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
196
app/Http/Controllers/Api/ComplaintController.php
Executable file
196
app/Http/Controllers/Api/ComplaintController.php
Executable file
@ -0,0 +1,196 @@
|
|||||||
|
<?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 complaint
|
||||||
|
*
|
||||||
|
* 投诉
|
||||||
|
*/
|
||||||
|
class ComplaintController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @api {get} api/complaint/lists 获取举报投诉列表
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup complaint
|
||||||
|
* @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 返回数据
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response-Data:
|
||||||
|
* {
|
||||||
|
* "current_page": 1,
|
||||||
|
* "data": [
|
||||||
|
* {
|
||||||
|
* "id": 1,
|
||||||
|
* "dialog_id": 100,
|
||||||
|
* "userid": 1,
|
||||||
|
* "type": 1,
|
||||||
|
* "reason": "举报原因",
|
||||||
|
* "imgs": [],
|
||||||
|
* "status": 0,
|
||||||
|
* "created_at": "2025-01-01 00:00:00",
|
||||||
|
* "updated_at": "2025-01-01 00:00:00"
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "first_page_url": "http://example.com/api/complaint/lists?page=1",
|
||||||
|
* "from": 1,
|
||||||
|
* "last_page": 1,
|
||||||
|
* "last_page_url": "http://example.com/api/complaint/lists?page=1",
|
||||||
|
* "next_page_url": null,
|
||||||
|
* "path": "http://example.com/api/complaint/lists",
|
||||||
|
* "per_page": 50,
|
||||||
|
* "prev_page_url": null,
|
||||||
|
* "to": 1,
|
||||||
|
* "total": 1
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function lists()
|
||||||
|
{
|
||||||
|
$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 {post} api/complaint/submit 举报投诉
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup complaint
|
||||||
|
* @apiName submit
|
||||||
|
*
|
||||||
|
* @apiBody {Number} dialog_id 对话ID
|
||||||
|
* @apiBody {Number} type 类型
|
||||||
|
* @apiBody {String} reason 原因
|
||||||
|
* @apiBody {Object[]} [imgs] 图片数组(可选)
|
||||||
|
* @apiBody {String} imgs.path 图片路径
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response-Data:
|
||||||
|
* []
|
||||||
|
*/
|
||||||
|
public function submit()
|
||||||
|
{
|
||||||
|
$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 {post} api/complaint/action 举报投诉 - 操作
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份(管理员权限)
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup complaint
|
||||||
|
* @apiName action
|
||||||
|
*
|
||||||
|
* @apiBody {Number} id 投诉ID
|
||||||
|
* @apiBody {String} type 操作类型:handle=已处理,delete=删除
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response-Data:
|
||||||
|
* []
|
||||||
|
*/
|
||||||
|
public function action()
|
||||||
|
{
|
||||||
|
$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
@ -11,8 +11,14 @@ use App\Models\FileContent;
|
|||||||
use App\Models\FileLink;
|
use App\Models\FileLink;
|
||||||
use App\Models\FileUser;
|
use App\Models\FileUser;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\UserRecentItem;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
|
use App\Module\Down;
|
||||||
|
use App\Module\Lock;
|
||||||
|
use App\Module\Timer;
|
||||||
use App\Module\Ihttp;
|
use App\Module\Ihttp;
|
||||||
|
use App\Module\Manticore\ManticoreFile;
|
||||||
|
use Response;
|
||||||
use Swoole\Coroutine;
|
use Swoole\Coroutine;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Redirect;
|
use Redirect;
|
||||||
@ -27,7 +33,7 @@ use ZipArchive;
|
|||||||
class FileController extends AbstractController
|
class FileController extends AbstractController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/lists 01. 获取文件列表
|
* @api {get} api/file/lists 获取文件列表
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -44,14 +50,13 @@ class FileController extends AbstractController
|
|||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
//
|
//
|
||||||
$data = Request::all();
|
$pid = intval(Request::input('pid'));
|
||||||
$pid = intval($data['pid']);
|
|
||||||
//
|
//
|
||||||
return Base::retSuccess('success', (new File)->getFileList($user, $pid));
|
return Base::retSuccess('success', (new File)->getFileList($user, $pid));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/one 02. 获取单条数据
|
* @api {get} api/file/one 获取单条数据
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -61,6 +66,14 @@ class FileController extends AbstractController
|
|||||||
* @apiParam {Number|String} id
|
* @apiParam {Number|String} id
|
||||||
* - Number 文件ID(需要登录)
|
* - Number 文件ID(需要登录)
|
||||||
* - String 链接码(不需要登录,用于预览)
|
* - String 链接码(不需要登录,用于预览)
|
||||||
|
* @apiParam {String} [with_url] 是否返回文件访问URL
|
||||||
|
* - no: 不返回(默认)
|
||||||
|
* - yes: 返回content_url字段
|
||||||
|
* @apiParam {String} [with_text] 是否提取文件文本内容(用于AI阅读,支持分页)
|
||||||
|
* - no: 不提取(默认)
|
||||||
|
* - yes: 提取文本内容,支持 docx/xlsx/pptx/pdf/txt 等格式
|
||||||
|
* @apiParam {Number} [text_offset] with_text=yes时有效,文本起始位置(字符数),默认0
|
||||||
|
* @apiParam {Number} [text_limit] with_text=yes时有效,文本获取长度(字符数),默认50000,最大200000
|
||||||
*
|
*
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
@ -69,11 +82,15 @@ class FileController extends AbstractController
|
|||||||
public function one()
|
public function one()
|
||||||
{
|
{
|
||||||
$id = Request::input('id');
|
$id = Request::input('id');
|
||||||
|
$with_url = Request::input('with_url', 'no');
|
||||||
|
$with_text = Request::input('with_text', 'no');
|
||||||
|
$text_offset = intval(Request::input('text_offset', 0));
|
||||||
|
$text_limit = intval(Request::input('text_limit', 50000));
|
||||||
//
|
//
|
||||||
$permission = 0;
|
$permission = 0;
|
||||||
if (Base::isNumber($id)) {
|
if (Base::isNumber($id)) {
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
$file = File::permissionFind(intval($id), $user, 0, $permission);
|
$file = File::permissionFind(intval($id), $user, $with_url === 'yes' ? 1 : 0, $permission);
|
||||||
} elseif ($id) {
|
} elseif ($id) {
|
||||||
$fileLink = FileLink::whereCode($id)->first();
|
$fileLink = FileLink::whereCode($id)->first();
|
||||||
$file = $fileLink?->file;
|
$file = $fileLink?->file;
|
||||||
@ -85,25 +102,87 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
return Base::retError($msg, $data);
|
return Base::retError($msg, $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果文件不允许游客访问,则需要登录
|
||||||
|
if (!$file->guest_access) {
|
||||||
|
User::auth();
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileLink->increment("num");
|
||||||
} else {
|
} else {
|
||||||
return Base::retError('参数错误');
|
return Base::retError('参数错误');
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$array = $file->toArray();
|
$array = $file->toArray();
|
||||||
$array['permission'] = $permission;
|
$array['permission'] = $permission;
|
||||||
|
|
||||||
|
// 如果请求返回文件URL
|
||||||
|
if ($with_url === 'yes') {
|
||||||
|
$array['content_url'] = FileContent::getFileUrl($file->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果请求提取文本内容
|
||||||
|
if ($with_text === 'yes') {
|
||||||
|
$array['text_content'] = ManticoreFile::extractFileContentPaginated($file, $text_offset, $text_limit);
|
||||||
|
}
|
||||||
|
|
||||||
return Base::retSuccess('success', $array);
|
return Base::retSuccess('success', $array);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/search 03. 搜索文件列表
|
* @api {get} api/file/fetch 通过路径获取文件文本内容
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 用于 MCP/AI 工具通过文件路径获取内容,支持分页获取大文件
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup file
|
||||||
|
* @apiName fetch
|
||||||
|
*
|
||||||
|
* @apiParam {String} path 文件路径(相对于系统根目录,如 uploads/file/...)
|
||||||
|
* @apiParam {Number} [offset] 起始位置(字符数),默认0
|
||||||
|
* @apiParam {Number} [limit] 获取长度(字符数),默认50000,最大200000
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
* - content: 文本内容
|
||||||
|
* - total_length: 完整内容总长度
|
||||||
|
* - offset: 当前起始位置
|
||||||
|
* - limit: 本次获取长度
|
||||||
|
* - has_more: 是否还有更多内容
|
||||||
|
*/
|
||||||
|
public function fetch()
|
||||||
|
{
|
||||||
|
User::auth();
|
||||||
|
//
|
||||||
|
$path = trim(Request::input('path'));
|
||||||
|
$offset = intval(Request::input('offset', 0));
|
||||||
|
$limit = intval(Request::input('limit', 50000));
|
||||||
|
|
||||||
|
if (empty($path)) {
|
||||||
|
return Base::retError('参数错误:path 不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接传入路径,ManticoreFile 内部处理 URL 解析
|
||||||
|
$result = ManticoreFile::extractFileContentPaginated($path, $offset, $limit);
|
||||||
|
|
||||||
|
if (isset($result['error'])) {
|
||||||
|
return Base::retError($result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('success', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/file/search 搜索文件列表
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份(仅搜索文件名,AI 内容搜索请使用 api/search/file)
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup file
|
* @apiGroup file
|
||||||
* @apiName search
|
* @apiName search
|
||||||
*
|
*
|
||||||
* @apiParam {String} [link] 通过分享地址搜索(如:https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==)
|
* @apiParam {String} [link] 通过分享地址搜索(如:https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==)
|
||||||
* @apiParam {String} [key] 关键词
|
* @apiParam {String} [key] 关键词
|
||||||
|
* @apiParam {Number} [take] 获取数量(默认:50,最大:100)
|
||||||
*
|
*
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
@ -116,7 +195,7 @@ class FileController extends AbstractController
|
|||||||
$link = trim(Request::input('link'));
|
$link = trim(Request::input('link'));
|
||||||
$key = trim(Request::input('key'));
|
$key = trim(Request::input('key'));
|
||||||
$id = 0;
|
$id = 0;
|
||||||
$take = 50;
|
$take = Base::getPaginate(100, 50, 'take');
|
||||||
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
|
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
|
||||||
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
|
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
|
||||||
$take = 1;
|
$take = 1;
|
||||||
@ -124,13 +203,20 @@ class FileController extends AbstractController
|
|||||||
return Base::retSuccess('success', []);
|
return Base::retSuccess('success', []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索自己的
|
// 搜索自己的
|
||||||
$builder = File::whereUserid($user->userid);
|
$builder = File::whereUserid($user->userid);
|
||||||
if ($id) {
|
if ($id) {
|
||||||
$builder->where("id", $id);
|
$builder->where("id", $id);
|
||||||
}
|
}
|
||||||
if ($key) {
|
if ($key) {
|
||||||
$builder->where("name", "like", "%{$key}%");
|
if (!$id && Base::isNumber($key)) {
|
||||||
|
$builder->where(function ($query) use ($key) {
|
||||||
|
$query->where("id", $key)->orWhere("name", "like", "%{$key}%");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$builder->where("name", "like", "%{$key}%");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$array = $builder->take($take)->get()->toArray();
|
$array = $builder->take($take)->get()->toArray();
|
||||||
// 搜索共享的
|
// 搜索共享的
|
||||||
@ -149,7 +235,13 @@ class FileController extends AbstractController
|
|||||||
$builder->where("id", $id);
|
$builder->where("id", $id);
|
||||||
}
|
}
|
||||||
if ($key) {
|
if ($key) {
|
||||||
$builder->where("name", "like", "%{$key}%");
|
if (Base::isNumber($key)) {
|
||||||
|
$builder->where(function ($query) use ($key) {
|
||||||
|
$query->where("id", $key)->orWhere("name", "like", "%{$key}%");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$builder->where("name", "like", "%{$key}%");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$list = $builder->take($take)->get();
|
$list = $builder->take($take)->get();
|
||||||
if ($list->isNotEmpty()) {
|
if ($list->isNotEmpty()) {
|
||||||
@ -167,7 +259,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/add 04. 添加、修改文件(夹)
|
* @api {get} api/file/add 添加、修改文件(夹)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -193,8 +285,8 @@ class FileController extends AbstractController
|
|||||||
$pid = intval(Request::input('pid'));
|
$pid = intval(Request::input('pid'));
|
||||||
if (mb_strlen($name) < 2) {
|
if (mb_strlen($name) < 2) {
|
||||||
return Base::retError('文件名称不可以少于2个字');
|
return Base::retError('文件名称不可以少于2个字');
|
||||||
} elseif (mb_strlen($name) > 32) {
|
} elseif (mb_strlen($name) > 100) {
|
||||||
return Base::retError('文件名称最多只能设置32个字');
|
return Base::retError('文件名称最多只能设置100个字');
|
||||||
}
|
}
|
||||||
$tmpName = preg_replace("/[\\\\\/:*?\"<>|]/", '', $name);
|
$tmpName = preg_replace("/[\\\\\/:*?\"<>|]/", '', $name);
|
||||||
if ($tmpName != $name) {
|
if ($tmpName != $name) {
|
||||||
@ -276,7 +368,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/copy 05. 复制文件(夹)
|
* @api {get} api/file/copy 复制文件(夹)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -299,6 +391,7 @@ class FileController extends AbstractController
|
|||||||
//
|
//
|
||||||
$userid = $user->userid;
|
$userid = $user->userid;
|
||||||
if ($row->pid > 0) {
|
if ($row->pid > 0) {
|
||||||
|
File::permissionFind($row->pid, $user, 1);
|
||||||
$userid = intval(File::whereId($row->pid)->value('userid'));
|
$userid = intval(File::whereId($row->pid)->value('userid'));
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
@ -336,7 +429,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/move 06. 移动文件(夹)
|
* @api {get} api/file/move 移动文件(夹)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -383,7 +476,7 @@ class FileController extends AbstractController
|
|||||||
throw new ApiException("{$file->name} 内含有共享文件,无法移动到另一个共享文件夹内");
|
throw new ApiException("{$file->name} 内含有共享文件,无法移动到另一个共享文件夹内");
|
||||||
}
|
}
|
||||||
$file->userid = $toShareFile->userid;
|
$file->userid = $toShareFile->userid;
|
||||||
File::where('pids', 'LIKE', "%,{$file->id},%")->update(['userid' => $toShareFile->userid]);
|
$file->updateChildFilesUserid($toShareFile->userid);
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$tmpId = $pid;
|
$tmpId = $pid;
|
||||||
@ -395,7 +488,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$file->userid = $user->userid;
|
$file->userid = $user->userid;
|
||||||
File::where('pids', 'LIKE', "%,{$file->id},%")->update(['userid' => $user->userid]);
|
$file->updateChildFilesUserid($user->userid);
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$file->pid = $pid;
|
$file->pid = $pid;
|
||||||
@ -411,7 +504,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/remove 07. 删除文件(夹)
|
* @api {get} api/file/remove 删除文件(夹)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -450,7 +543,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/content 08. 获取文件内容
|
* @api {get} api/file/content 获取文件内容
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -498,6 +591,10 @@ class FileController extends AbstractController
|
|||||||
return Base::retError('参数错误');
|
return Base::retError('参数错误');
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
|
if ($down == 'no') {
|
||||||
|
File::isNeedInstallApp($file->type);
|
||||||
|
}
|
||||||
|
//
|
||||||
if ($only_update_at == 'yes') {
|
if ($only_update_at == 'yes') {
|
||||||
return Base::retSuccess('success', [
|
return Base::retSuccess('success', [
|
||||||
'id' => $file->id,
|
'id' => $file->id,
|
||||||
@ -510,6 +607,16 @@ class FileController extends AbstractController
|
|||||||
$builder->whereId($history_id);
|
$builder->whereId($history_id);
|
||||||
}
|
}
|
||||||
$content = $builder->orderByDesc('id')->first();
|
$content = $builder->orderByDesc('id')->first();
|
||||||
|
if (isset($user)) {
|
||||||
|
UserRecentItem::record(
|
||||||
|
$user->userid,
|
||||||
|
UserRecentItem::TYPE_FILE,
|
||||||
|
$file->id,
|
||||||
|
UserRecentItem::SOURCE_FILESYSTEM,
|
||||||
|
intval($file->pid)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ($down === 'preview') {
|
if ($down === 'preview') {
|
||||||
return Redirect::to(FileContent::formatPreview($file, $content?->content));
|
return Redirect::to(FileContent::formatPreview($file, $content?->content));
|
||||||
}
|
}
|
||||||
@ -517,7 +624,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/content/save 09. 保存文件内容
|
* @api {get} api/file/content/save 保存文件内容
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -557,7 +664,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$text = strip_tags($data['content']);
|
$text = strip_tags($data['content']);
|
||||||
if ($isRep == true) {
|
if ($isRep) {
|
||||||
$content = Base::array2json($data);
|
$content = Base::array2json($data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -572,10 +679,12 @@ class FileController extends AbstractController
|
|||||||
$contentArray = Base::json2array($content);
|
$contentArray = Base::json2array($content);
|
||||||
$contentString = $contentArray['xml'];
|
$contentString = $contentArray['xml'];
|
||||||
$file->ext = 'drawio';
|
$file->ext = 'drawio';
|
||||||
|
File::isNeedInstallApp($file->type);
|
||||||
break;
|
break;
|
||||||
case 'mind':
|
case 'mind':
|
||||||
$contentString = $content;
|
$contentString = $content;
|
||||||
$file->ext = 'mind';
|
$file->ext = 'mind';
|
||||||
|
File::isNeedInstallApp($file->type);
|
||||||
break;
|
break;
|
||||||
case 'txt':
|
case 'txt':
|
||||||
case 'code':
|
case 'code':
|
||||||
@ -610,9 +719,9 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/office/token 10. 获取token
|
* @api {get} api/file/office/token 获取token
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 用于生成office在线编辑的token
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup file
|
* @apiGroup file
|
||||||
* @apiName office__token
|
* @apiName office__token
|
||||||
@ -625,7 +734,7 @@ class FileController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function office__token()
|
public function office__token()
|
||||||
{
|
{
|
||||||
User::auth();
|
File::isNeedInstallApp('office');
|
||||||
//
|
//
|
||||||
$config = Request::input('config');
|
$config = Request::input('config');
|
||||||
$token = \Firebase\JWT\JWT::encode($config, env('APP_KEY') ,'HS256');
|
$token = \Firebase\JWT\JWT::encode($config, env('APP_KEY') ,'HS256');
|
||||||
@ -635,7 +744,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/content/office 11. 保存文件内容(office)
|
* @api {get} api/file/content/office 保存文件内容(office)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -652,6 +761,8 @@ class FileController extends AbstractController
|
|||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
//
|
//
|
||||||
|
File::isNeedInstallApp('office');
|
||||||
|
//
|
||||||
$id = intval(Request::input('id'));
|
$id = intval(Request::input('id'));
|
||||||
$status = intval(Request::input('status'));
|
$status = intval(Request::input('status'));
|
||||||
$key = Request::input('key');
|
$key = Request::input('key');
|
||||||
@ -661,7 +772,7 @@ class FileController extends AbstractController
|
|||||||
//
|
//
|
||||||
if ($status === 2) {
|
if ($status === 2) {
|
||||||
$parse = parse_url($url);
|
$parse = parse_url($url);
|
||||||
$from = 'http://' . env('APP_IPPR') . '.3' . $parse['path'] . '?' . $parse['query'];
|
$from = 'http://nginx' . $parse['path'] . '?' . $parse['query'];
|
||||||
$path = 'uploads/file/' . $file->type . '/' . date("Ym") . '/' . $file->id . '/' . $key;
|
$path = 'uploads/file/' . $file->type . '/' . date("Ym") . '/' . $file->id . '/' . $key;
|
||||||
$save = public_path($path);
|
$save = public_path($path);
|
||||||
Base::makeDir(dirname($save));
|
Base::makeDir(dirname($save));
|
||||||
@ -689,7 +800,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/content/upload 12. 保存文件内容(上传文件)
|
* @api {get} api/file/content/upload 保存文件内容(上传文件)
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -697,6 +808,9 @@ class FileController extends AbstractController
|
|||||||
* @apiName content__upload
|
* @apiName content__upload
|
||||||
*
|
*
|
||||||
* @apiParam {Number} [pid] 父级ID
|
* @apiParam {Number} [pid] 父级ID
|
||||||
|
* @apiParam {Number} [cover] 覆盖已存在的文件
|
||||||
|
* - 0:不覆盖,保留两者(默认)
|
||||||
|
* - 1:覆盖
|
||||||
* @apiParam {String} [files] 文件名
|
* @apiParam {String} [files] 文件名
|
||||||
*
|
*
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
@ -707,13 +821,24 @@ class FileController extends AbstractController
|
|||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
$pid = intval(Request::input('pid'));
|
$pid = intval(Request::input('pid'));
|
||||||
$webkitRelativePath = Request::input('webkitRelativePath');
|
// 同一用户往相同父目录上传时排队,避免并发导致数据库死锁
|
||||||
$data = (new File)->contentUpload($user, $pid, $webkitRelativePath);
|
try {
|
||||||
return Base::retSuccess($data['data']['name'] . ' 上传成功', $data['addItem']);
|
return Lock::withLock("file:upload:{$user->userid}:{$pid}", function () use ($user, $pid) {
|
||||||
|
$overwrite = intval(Request::input('cover'));
|
||||||
|
$webkitRelativePath = Request::input('webkitRelativePath');
|
||||||
|
$data = (new File)->contentUpload($user, $pid, $webkitRelativePath, $overwrite);
|
||||||
|
return Base::retSuccess($data['data']['name'] . ' 上传成功', $data['addItem']);
|
||||||
|
}, 120000, 120000);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if (str_contains($e->getMessage(), 'Failed to acquire lock')) {
|
||||||
|
throw new ApiException('上传繁忙,请稍后再试');
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/content/history 13. 获取内容历史
|
* @api {get} api/file/content/history 获取内容历史
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -745,7 +870,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/content/restore 14. 恢复文件历史
|
* @api {get} api/file/content/restore 恢复文件历史
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -768,6 +893,8 @@ class FileController extends AbstractController
|
|||||||
//
|
//
|
||||||
$file = File::permissionFind($id, $user);
|
$file = File::permissionFind($id, $user);
|
||||||
//
|
//
|
||||||
|
File::isNeedInstallApp($file->type);
|
||||||
|
//
|
||||||
$history = FileContent::whereFid($file->id)->whereId($history_id)->first();
|
$history = FileContent::whereFid($file->id)->whereId($history_id)->first();
|
||||||
if (empty($history)) {
|
if (empty($history)) {
|
||||||
return Base::retError('历史数据不存在或已被删除');
|
return Base::retError('历史数据不存在或已被删除');
|
||||||
@ -785,7 +912,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/share 15. 获取共享信息
|
* @api {get} api/file/share 获取共享信息
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -821,7 +948,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/share/update 16. 设置共享
|
* @api {get} api/file/share/update 设置共享
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -911,7 +1038,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/share/out 17. 退出共享
|
* @api {get} api/file/share/out 退出共享
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -945,7 +1072,7 @@ class FileController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/link 18. 获取链接
|
* @api {get} api/file/link 获取链接
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -956,6 +1083,9 @@ class FileController extends AbstractController
|
|||||||
* @apiParam {String} refresh 刷新链接
|
* @apiParam {String} refresh 刷新链接
|
||||||
* - no: 只获取(默认)
|
* - no: 只获取(默认)
|
||||||
* - yes: 刷新链接,之前的将失效
|
* - yes: 刷新链接,之前的将失效
|
||||||
|
* @apiParam {String} guest_access 是否允许游客访问
|
||||||
|
* - no: 不允许(默认)
|
||||||
|
* - yes: 允许游客访问
|
||||||
*
|
*
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
@ -967,15 +1097,22 @@ class FileController extends AbstractController
|
|||||||
//
|
//
|
||||||
$id = intval(Request::input('id'));
|
$id = intval(Request::input('id'));
|
||||||
$refresh = Request::input('refresh', 'no');
|
$refresh = Request::input('refresh', 'no');
|
||||||
|
$guestAccess = Request::input('guest_access', 'no');
|
||||||
//
|
//
|
||||||
$file = File::permissionFind($id, $user);
|
$file = File::permissionFind($id, $user);
|
||||||
|
|
||||||
|
// 更新文件的游客访问权限
|
||||||
|
$file->guest_access = $guestAccess === 'yes' ? 1 : 0;
|
||||||
|
$file->save();
|
||||||
|
|
||||||
$fileLink = $file->getShareLink($user->userid, $refresh == 'yes');
|
$fileLink = $file->getShareLink($user->userid, $refresh == 'yes');
|
||||||
|
$fileLink['guest_access'] = $file->guest_access;
|
||||||
//
|
//
|
||||||
return Base::retSuccess('success', $fileLink);
|
return Base::retSuccess('success', $fileLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/file/download/pack 19. 打包文件
|
* @api {get} api/file/download/pack 打包文件
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
@ -991,9 +1128,49 @@ class FileController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function download__pack()
|
public function download__pack()
|
||||||
{
|
{
|
||||||
|
if (Request::has('key')) {
|
||||||
|
$array = Down::cache_decode();
|
||||||
|
$file = $array['file'];
|
||||||
|
if (empty($file) || !file_exists(storage_path($file))) {
|
||||||
|
return Base::ajaxError("文件不存在!", [], 0, 403);
|
||||||
|
}
|
||||||
|
return Response::download(storage_path($file));
|
||||||
|
}
|
||||||
|
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
|
if ($user->isTemp()) {
|
||||||
|
return Base::retError('无法打包下载');
|
||||||
|
}
|
||||||
|
$setting = Base::setting('fileSetting');
|
||||||
|
switch ($setting['permission_pack_type']) {
|
||||||
|
case 'admin':
|
||||||
|
if (!$user->isAdmin()) {
|
||||||
|
return Base::retError('此功能仅管理员可用');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'appointAllow':
|
||||||
|
if (!in_array($user->userid, $setting['permission_pack_userids'])) {
|
||||||
|
return Base::retError('此功能仅指定用户可用');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'appointProhibit':
|
||||||
|
if (in_array($user->userid, $setting['permission_pack_userids'])) {
|
||||||
|
return Base::retError('此功能已禁止使用');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$ids = Request::input('ids');
|
$ids = Request::input('ids');
|
||||||
$downName = Request::input('name');
|
$fileName = Request::input('name');
|
||||||
|
$fileName = preg_replace("/[\/\\\:\*\?\"\<\>\|]/", "", $fileName);
|
||||||
|
if (empty($fileName)) {
|
||||||
|
$fileName = 'Package_' . $user->userid;
|
||||||
|
}
|
||||||
|
$fileName .= '_' . Timer::time() . '.zip';
|
||||||
|
|
||||||
|
$filePath = "temp/file/pack/" . date("Ym", Timer::time());
|
||||||
|
$zipFile = "app/" . $filePath . "/" . $fileName;
|
||||||
|
$zipPath = storage_path($zipFile);
|
||||||
|
|
||||||
if (!is_array($ids) || empty($ids)) {
|
if (!is_array($ids) || empty($ids)) {
|
||||||
return Base::retError('请选择下载的文件或文件夹');
|
return Base::retError('请选择下载的文件或文件夹');
|
||||||
@ -1001,9 +1178,12 @@ class FileController extends AbstractController
|
|||||||
if (count($ids) > 100) {
|
if (count($ids) > 100) {
|
||||||
return Base::retError('一次最多可以下载100个文件或文件夹');
|
return Base::retError('一次最多可以下载100个文件或文件夹');
|
||||||
}
|
}
|
||||||
if (count($ids) > 100) {
|
|
||||||
return Base::retError('一次最多可以下载100个文件或文件夹');
|
$botUser = User::botGetOrCreate('system-msg');
|
||||||
|
if (empty($botUser)) {
|
||||||
|
return Base::retError('系统机器人不存在');
|
||||||
}
|
}
|
||||||
|
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||||
|
|
||||||
$files = [];
|
$files = [];
|
||||||
$totalSize = 0;
|
$totalSize = 0;
|
||||||
@ -1017,25 +1197,30 @@ class FileController extends AbstractController
|
|||||||
return Base::retError('文件总大小已超过1GB,请分批下载');
|
return Base::retError('文件总大小已超过1GB,请分批下载');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$key = Down::cache_encode([
|
||||||
|
'file' => $zipFile,
|
||||||
|
]);
|
||||||
|
$fileUrl = Base::fillUrl('api/file/download/pack?key=' . $key);
|
||||||
|
|
||||||
$zip = new \ZipArchive();
|
$zip = new \ZipArchive();
|
||||||
$zipName = 'tmp/file/' . date("Ym") . '/' . $user->userid . '/' . $downName;
|
|
||||||
$zipPath = public_path($zipName);
|
|
||||||
Base::makeDir(dirname($zipPath));
|
Base::makeDir(dirname($zipPath));
|
||||||
|
|
||||||
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||||
return Base::retError('创建压缩文件失败');
|
return Base::retError('创建压缩文件失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
go(function() use ($user, $zip, $files, $downName, $zipName) {
|
$userid = $user->userid;
|
||||||
|
go(function () use ($userid, $zipPath, $fileUrl, $zip, $files, $fileName, $botUser, $dialog) {
|
||||||
Coroutine::sleep(0.1);
|
Coroutine::sleep(0.1);
|
||||||
// 压缩进度
|
// 压缩进度
|
||||||
$progress = 0;
|
$progress = 0;
|
||||||
$zip->registerProgressCallback(0.05, function($ratio) use ($downName, &$progress) {
|
$zip->registerProgressCallback(0.05, function ($ratio) use ($userid, $fileUrl, $fileName, &$progress) {
|
||||||
$progress = round($ratio * 100);
|
$progress = round($ratio * 100);
|
||||||
File::filePushMsg('compress', [
|
File::pushMsgSimple('compress', [
|
||||||
'name'=> $downName,
|
'name' => $fileName,
|
||||||
|
'url' => $fileUrl,
|
||||||
'progress' => $progress
|
'progress' => $progress
|
||||||
]);
|
], $userid);
|
||||||
});
|
});
|
||||||
//
|
//
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
@ -1044,52 +1229,24 @@ class FileController extends AbstractController
|
|||||||
$zip->close();
|
$zip->close();
|
||||||
//
|
//
|
||||||
if ($progress < 100) {
|
if ($progress < 100) {
|
||||||
File::filePushMsg('compress', [
|
File::pushMsgSimple('compress', [
|
||||||
'name'=> $downName,
|
'name' => $fileName,
|
||||||
|
'url' => $fileUrl,
|
||||||
'progress' => 100
|
'progress' => 100
|
||||||
]);
|
], $userid);
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$botUser = User::botGetOrCreate('system-msg');
|
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||||
if (empty($botUser)) {
|
'type' => 'file_download',
|
||||||
return;
|
'title' => '文件下载打包已完成',
|
||||||
}
|
'name' => $fileName,
|
||||||
if ($dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
|
'size' => filesize($zipPath),
|
||||||
$text = "<b>文件下载打包已完成。</b>";
|
'url' => $fileUrl,
|
||||||
$text .= "\n\n";
|
], $botUser->userid, false, false, true);
|
||||||
$text .= "文件名:{$downName}";
|
|
||||||
$text .= "\n";
|
|
||||||
$text .= "下载地址:".Base::fillUrl($zipName);
|
|
||||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return Base::retSuccess('success', [
|
||||||
return Base::retSuccess('success');
|
'name' => $fileName,
|
||||||
}
|
'url' => $fileUrl,
|
||||||
|
]);
|
||||||
/**
|
|
||||||
* @api {get} api/file/download/confirm 20. 确认下载
|
|
||||||
*
|
|
||||||
* @apiDescription 需要token身份
|
|
||||||
* @apiVersion 1.0.0
|
|
||||||
* @apiGroup file
|
|
||||||
* @apiName download__confirm
|
|
||||||
*
|
|
||||||
* @apiParam {String} [name] 下载文件名
|
|
||||||
*
|
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
|
||||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
|
||||||
* @apiSuccess {Object} data 返回数据
|
|
||||||
*/
|
|
||||||
public function download__confirm()
|
|
||||||
{
|
|
||||||
$user = User::auth();
|
|
||||||
$downName = Request::input('name');
|
|
||||||
$zipName = 'tmp/file/' . date("Ym") . '/' . $user->userid . '/' . $downName;
|
|
||||||
$zipPath = public_path($zipName);
|
|
||||||
if (!file_exists($zipPath)) {
|
|
||||||
abort(403, "The file does not exist.");
|
|
||||||
}
|
|
||||||
return response()->download($zipPath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2,15 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\UserBot;
|
use App\Models\UserBot;
|
||||||
use App\Models\UserCheckinMac;
|
|
||||||
use App\Models\UserCheckinRecord;
|
|
||||||
use App\Models\WebSocketDialog;
|
|
||||||
use App\Models\WebSocketDialogMsg;
|
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use Cache;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Request;
|
use Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,7 +65,9 @@ class PublicController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {post} 签到 - 路由器(openwrt)上报
|
* {post} 签到 - 上报
|
||||||
|
* - 1、路由器(openwrt)签到上报
|
||||||
|
* - 2、考勤机签到上报
|
||||||
*
|
*
|
||||||
* @apiParam {String} key
|
* @apiParam {String} key
|
||||||
* @apiParam {String} mac 使用逗号分割多个
|
* @apiParam {String} mac 使用逗号分割多个
|
||||||
@ -85,20 +80,30 @@ class PublicController extends AbstractController
|
|||||||
$key = trim(Request::input('key'));
|
$key = trim(Request::input('key'));
|
||||||
$mac = trim(Request::input('mac'));
|
$mac = trim(Request::input('mac'));
|
||||||
$time = intval(Request::input('time'));
|
$time = intval(Request::input('time'));
|
||||||
|
$type = trim(Request::input('type'));
|
||||||
//
|
//
|
||||||
$setting = Base::setting('checkinSetting');
|
$setting = Base::setting('checkinSetting');
|
||||||
if ($setting['open'] !== 'open') {
|
if ($setting['open'] !== 'open') {
|
||||||
return 'function off';
|
return 'function off';
|
||||||
}
|
}
|
||||||
if (!in_array('auto', $setting['modes'])) {
|
$alreadyTip = false;
|
||||||
return 'mode off';
|
if ($type === 'face') {
|
||||||
}
|
if (!in_array('face', $setting['modes'])) {
|
||||||
if ($key != $setting['key']) {
|
return 'mode off';
|
||||||
return 'key error';
|
}
|
||||||
}
|
if ($key != $setting['face_key']) {
|
||||||
if ($error = UserBot::checkinBotCheckin($mac, $time)) {
|
return 'key error';
|
||||||
return $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';
|
return 'success';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,14 +6,16 @@ use App\Exceptions\ApiException;
|
|||||||
use App\Models\AbstractModel;
|
use App\Models\AbstractModel;
|
||||||
use App\Models\ProjectTask;
|
use App\Models\ProjectTask;
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
|
use App\Models\ReportAnalysis;
|
||||||
|
use App\Models\ReportLink;
|
||||||
use App\Models\ReportReceive;
|
use App\Models\ReportReceive;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\WebSocketDialogMsg;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use App\Module\Doo;
|
use App\Module\Doo;
|
||||||
use App\Tasks\PushTask;
|
use App\Tasks\PushTask;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Request;
|
use Request;
|
||||||
@ -27,13 +29,15 @@ use Illuminate\Support\Facades\Validator;
|
|||||||
class ReportController extends AbstractController
|
class ReportController extends AbstractController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @api {get} api/report/my 01. 我发送的汇报
|
* @api {get} api/report/my 我发送的汇报
|
||||||
*
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup report
|
* @apiGroup report
|
||||||
* @apiName my
|
* @apiName my
|
||||||
*
|
*
|
||||||
* @apiParam {Object} [keys] 搜索条件
|
* @apiParam {Object} [keys] 搜索条件
|
||||||
|
* - keys.key: 关键词
|
||||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||||
* - keys.created_at: 汇报时间
|
* - keys.created_at: 汇报时间
|
||||||
* @apiParam {Number} [page] 当前页,默认:1
|
* @apiParam {Number} [page] 当前页,默认:1
|
||||||
@ -47,15 +51,31 @@ class ReportController extends AbstractController
|
|||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
//
|
//
|
||||||
$builder = Report::with(['receivesUser'])->whereUserid($user->userid);
|
$builder = Report::with(['receivesUser'])
|
||||||
|
->select(Report::LIST_FIELDS)
|
||||||
|
->whereUserid($user->userid);
|
||||||
$keys = Request::input('keys');
|
$keys = Request::input('keys');
|
||||||
if (is_array($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(function ($query) use ($keys) {
|
||||||
|
$query->where("id", intval($keys['key']))
|
||||||
|
->orWhere("title", "LIKE", "%{$keys['key']}%");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$builder->where("title", "LIKE", "%{$keys['key']}%");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
|
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
|
||||||
$builder->whereType($keys['type']);
|
$builder->whereType($keys['type']);
|
||||||
}
|
}
|
||||||
if (is_array($keys['created_at'])) {
|
if (is_array($keys['created_at'])) {
|
||||||
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', date('Y-m-d H:i:s', Base::dayTimeF($keys['created_at'][0])));
|
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', '<=', date('Y-m-d H:i:s', Base::dayTimeE($keys['created_at'][1])));
|
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', Base::newCarbon($keys['created_at'][1])->endOfDay());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
|
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
|
||||||
@ -63,15 +83,18 @@ class ReportController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/report/receive 02. 我接收的汇报
|
* @api {get} api/report/receive 我接收的汇报
|
||||||
*
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup report
|
* @apiGroup report
|
||||||
* @apiName receive
|
* @apiName receive
|
||||||
*
|
*
|
||||||
* @apiParam {Object} [keys] 搜索条件
|
* @apiParam {Object} [keys] 搜索条件
|
||||||
* - keys.key: 关键词
|
* - keys.key: 关键词
|
||||||
|
* - keys.department_id: 部门ID
|
||||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||||
|
* - keys.status: 状态,unread:未读,read:已读
|
||||||
* - keys.created_at: 汇报时间
|
* - keys.created_at: 汇报时间
|
||||||
* @apiParam {Number} [page] 当前页,默认:1
|
* @apiParam {Number} [page] 当前页,默认:1
|
||||||
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50
|
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50
|
||||||
@ -83,39 +106,59 @@ class ReportController extends AbstractController
|
|||||||
public function receive(): array
|
public function receive(): array
|
||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
$builder = Report::with(['receivesUser']);
|
$builder = Report::with(['receivesUser'])
|
||||||
|
->select(Report::LIST_FIELDS);
|
||||||
$builder->whereHas("receivesUser", function ($query) use ($user) {
|
$builder->whereHas("receivesUser", function ($query) use ($user) {
|
||||||
$query->where("report_receives.userid", $user->userid);
|
$query->where("report_receives.userid", $user->userid);
|
||||||
});
|
});
|
||||||
$keys = Request::input('keys');
|
$keys = Request::input('keys');
|
||||||
if (is_array($keys)) {
|
if (is_array($keys)) {
|
||||||
if ($keys['key']) {
|
if ($keys['key']) {
|
||||||
$builder->where(function($query) use ($keys) {
|
if (str_contains($keys['key'], '@')) {
|
||||||
$query->whereHas('sendUser', function ($q2) use ($keys) {
|
$builder->whereHas('sendUser', function ($q2) use ($keys) {
|
||||||
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
|
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
|
||||||
})->orWhere("title", "LIKE", "%{$keys['key']}%");
|
});
|
||||||
|
} elseif (Base::isNumber($keys['key'])) {
|
||||||
|
$builder->where(function ($query) use ($keys) {
|
||||||
|
$query->where("userid", intval($keys['key']))
|
||||||
|
->orWhere("id", intval($keys['key']))
|
||||||
|
->orWhere("title", "LIKE", "%{$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])) {
|
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
|
||||||
$builder->whereType($keys['type']);
|
$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 (is_array($keys['created_at'])) {
|
||||||
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', date('Y-m-d H:i:s', Base::dayTimeF($keys['created_at'][0])));
|
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', '<=', date('Y-m-d H:i:s', Base::dayTimeE($keys['created_at'][1])));
|
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', Base::newCarbon($keys['created_at'][1])->endOfDay());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
|
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
|
||||||
if ($list->items()) {
|
if ($list->items()) {
|
||||||
foreach ($list->items() as $item) {
|
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);
|
return Base::retSuccess('success', $list);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/report/store 03. 保存并发送工作汇报
|
* @api {get} api/report/store 保存并发送工作汇报
|
||||||
*
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup report
|
* @apiGroup report
|
||||||
* @apiName store
|
* @apiName store
|
||||||
@ -175,7 +218,7 @@ class ReportController extends AbstractController
|
|||||||
|
|
||||||
foreach ($input["receive"] as $userid) {
|
foreach ($input["receive"] as $userid) {
|
||||||
$input["receive_content"][] = [
|
$input["receive_content"][] = [
|
||||||
"receive_time" => Carbon::now()->toDateTimeString(),
|
"receive_at" => Carbon::now()->toDateTimeString(),
|
||||||
"userid" => $userid,
|
"userid" => $userid,
|
||||||
"read" => 0,
|
"read" => 0,
|
||||||
];
|
];
|
||||||
@ -191,7 +234,6 @@ class ReportController extends AbstractController
|
|||||||
$report->updateInstance([
|
$report->updateInstance([
|
||||||
"title" => $input["title"],
|
"title" => $input["title"],
|
||||||
"type" => $input["type"],
|
"type" => $input["type"],
|
||||||
"content" => htmlspecialchars($input["content"]),
|
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
// 生成唯一标识
|
// 生成唯一标识
|
||||||
@ -205,11 +247,25 @@ class ReportController extends AbstractController
|
|||||||
"title" => $input["title"],
|
"title" => $input["title"],
|
||||||
"type" => $input["type"],
|
"type" => $input["type"],
|
||||||
"userid" => $user->userid,
|
"userid" => $user->userid,
|
||||||
"content" => htmlspecialchars($input["content"]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
$report->save();
|
$report->save();
|
||||||
|
|
||||||
|
// 保存内容
|
||||||
|
$content = $input["content"];
|
||||||
|
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp);base64,(.*?)\"/s", $content, $matchs);
|
||||||
|
foreach ($matchs[2] as $key => $text) {
|
||||||
|
$tmpPath = "uploads/report/" . Carbon::parse($report->created_at)->format("Ym") . "/" . $report->id . "/attached/";
|
||||||
|
Base::makeDir(public_path($tmpPath));
|
||||||
|
$tmpPath .= md5($text) . "." . $matchs[1][$key];
|
||||||
|
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
|
||||||
|
$paramet = getimagesize(public_path($tmpPath));
|
||||||
|
$content = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($tmpPath) . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$report->content = htmlspecialchars($content);
|
||||||
|
$report->save();
|
||||||
|
|
||||||
// 删除关联
|
// 删除关联
|
||||||
$report->Receives()->delete();
|
$report->Receives()->delete();
|
||||||
if ($input["receive_content"]) {
|
if ($input["receive_content"]) {
|
||||||
@ -239,8 +295,9 @@ class ReportController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/report/template 04. 生成汇报模板
|
* @api {get} api/report/template 生成汇报模板
|
||||||
*
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup report
|
* @apiGroup report
|
||||||
* @apiName template
|
* @apiName template
|
||||||
@ -260,6 +317,7 @@ class ReportController extends AbstractController
|
|||||||
$offset = abs(intval(Request::input("offset", 0)));
|
$offset = abs(intval(Request::input("offset", 0)));
|
||||||
$id = intval(Request::input("offset", 0));
|
$id = intval(Request::input("offset", 0));
|
||||||
$now_dt = trim(Request::input("date")) ? Carbon::parse(Request::input("date")) : Carbon::now();
|
$now_dt = trim(Request::input("date")) ? Carbon::parse(Request::input("date")) : Carbon::now();
|
||||||
|
|
||||||
// 获取开始时间
|
// 获取开始时间
|
||||||
if ($type === Report::DAILY) {
|
if ($type === Report::DAILY) {
|
||||||
$start_time = Carbon::today();
|
$start_time = Carbon::today();
|
||||||
@ -281,9 +339,18 @@ class ReportController extends AbstractController
|
|||||||
$start_time->startOfWeek();
|
$start_time->startOfWeek();
|
||||||
$end_time = Carbon::instance($start_time)->endOfWeek();
|
$end_time = Carbon::instance($start_time)->endOfWeek();
|
||||||
}
|
}
|
||||||
|
// 周报时预计算下一周期时间范围(下周)
|
||||||
|
$next_start_time = null;
|
||||||
|
$next_end_time = null;
|
||||||
|
if ($type === Report::WEEKLY) {
|
||||||
|
$next_start_time = Carbon::instance($start_time)->copy()->addWeek();
|
||||||
|
$next_end_time = Carbon::instance($end_time)->copy()->addWeek();
|
||||||
|
}
|
||||||
|
|
||||||
// 生成唯一标识
|
// 生成唯一标识
|
||||||
$sign = Report::generateSign($type, 0, Carbon::instance($start_time));
|
$sign = Report::generateSign($type, 0, Carbon::instance($start_time));
|
||||||
$one = Report::whereSign($sign)->whereType($type)->first();
|
$one = Report::whereSign($sign)->whereType($type)->first();
|
||||||
|
|
||||||
// 如果已经提交了相关汇报
|
// 如果已经提交了相关汇报
|
||||||
if ($one && $id > 0) {
|
if ($one && $id > 0) {
|
||||||
return Base::retSuccess('success', [
|
return Base::retSuccess('success', [
|
||||||
@ -294,8 +361,16 @@ class ReportController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表格头部
|
||||||
|
$labels = [
|
||||||
|
Doo::translate('项目'),
|
||||||
|
Doo::translate('任务'),
|
||||||
|
Doo::translate('负责人'),
|
||||||
|
Doo::translate('备注'),
|
||||||
|
];
|
||||||
|
|
||||||
// 已完成的任务
|
// 已完成的任务
|
||||||
$completeContent = "";
|
$completeDatas = [];
|
||||||
$complete_task = ProjectTask::query()
|
$complete_task = ProjectTask::query()
|
||||||
->whereNotNull("complete_at")
|
->whereNotNull("complete_at")
|
||||||
->whereBetween("complete_at", [$start_time->toDateTimeString(), $end_time->toDateTimeString()])
|
->whereBetween("complete_at", [$start_time->toDateTimeString(), $end_time->toDateTimeString()])
|
||||||
@ -306,57 +381,109 @@ class ReportController extends AbstractController
|
|||||||
->get();
|
->get();
|
||||||
if ($complete_task->isNotEmpty()) {
|
if ($complete_task->isNotEmpty()) {
|
||||||
foreach ($complete_task as $task) {
|
foreach ($complete_task as $task) {
|
||||||
|
// 排除取消态任务:不将已取消任务计入“已完成工作”
|
||||||
|
if (ProjectTask::isCanceledFlowName($task->flow_item_name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$complete_at = Carbon::parse($task->complete_at);
|
$complete_at = Carbon::parse($task->complete_at);
|
||||||
$pre = $type == Report::WEEKLY ? ('<span>[' . Doo::translate('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</span> ') : '';
|
$remark = $type == Report::WEEKLY ? ('<div style="text-align:center">[' . Doo::translate('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</div>') : ' ';
|
||||||
$completeContent .= "<li>{$pre}[{$task->project->name}] {$task->name}</li>";
|
$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()
|
$unfinished_task = ProjectTask::buildUnfinishedTaskQuery($user->userid, $start_time, $end_time, true)->get();
|
||||||
->whereNull("complete_at")
|
|
||||||
->whereNotNull("start_at")
|
|
||||||
->where("end_at", "<", $end_time->toDateTimeString())
|
|
||||||
->whereHas("taskUser", function ($query) use ($user) {
|
|
||||||
$query->where("userid", $user->userid);
|
|
||||||
})
|
|
||||||
->orderByDesc("id")
|
|
||||||
->get();
|
|
||||||
if ($unfinished_task->isNotEmpty()) {
|
if ($unfinished_task->isNotEmpty()) {
|
||||||
foreach ($unfinished_task as $task) {
|
foreach ($unfinished_task as $task) {
|
||||||
empty($task->end_at) || $end_at = Carbon::parse($task->end_at);
|
empty($task->end_at) || $end_at = Carbon::parse($task->end_at);
|
||||||
$pre = (!empty($end_at) && $end_at->lt($now_dt)) ? '<span style="color:#ff0000;">[' . Doo::translate('超期') . ']</span> ' : '';
|
$remark = (!empty($end_at) && $end_at->lt($now_dt)) ? '<div style="color:#ff0000;text-align:center">[' . Doo::translate('超期') . ']</div>' : ' ';
|
||||||
$unfinishedContent .= "<li>{$pre}[{$task->project->name}] {$task->name}</li>";
|
$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) {
|
if ($type === Report::WEEKLY) {
|
||||||
$title = $user->nickname . "的周报[" . $start_time->format("m/d") . "-" . $end_time->format("m/d") . "]";
|
$title = $user->nickname . "的周报[" . $start_time->format("m/d") . "-" . $end_time->format("m/d") . "]";
|
||||||
$title .= "[" . $start_time->month . "月第" . $start_time->weekOfMonth . "周]";
|
$title .= "[" . $start_time->month . "月第" . $start_time->weekOfMonth . "周]";
|
||||||
|
$unfinishedTitle = '本周未完成的工作';
|
||||||
} else {
|
} else {
|
||||||
$title = $user->nickname . "的日报[" . $start_time->format("Y/m/d") . "]";
|
$title = $user->nickname . "的日报[" . $start_time->format("Y/m/d") . "]";
|
||||||
|
$unfinishedTitle = '今日未完成的工作';
|
||||||
}
|
}
|
||||||
|
$title = Doo::translate($title);
|
||||||
|
|
||||||
// 生成内容
|
// 生成内容
|
||||||
$content = '<h2>' . Doo::translate('已完成工作') . '</h2><ol>' .
|
$contents = [];
|
||||||
$completeContent . '</ol><h2>' .
|
$contents[] = '<h2>' . Doo::translate('已完成工作') . '</h2>';
|
||||||
Doo::translate('未完成的工作') . '</h2><ol>' .
|
$contents[] = view('report', [
|
||||||
$unfinishedContent . '</ol>';
|
'labels' => $labels,
|
||||||
|
'datas' => $completeDatas,
|
||||||
|
])->render();
|
||||||
|
|
||||||
|
$contents[] = '<p> </p>';
|
||||||
|
$contents[] = '<h2>' . Doo::translate($unfinishedTitle) . '</h2>';
|
||||||
|
$contents[] = view('report', [
|
||||||
|
'labels' => $labels,
|
||||||
|
'datas' => $unfinishedDatas,
|
||||||
|
])->render();
|
||||||
|
|
||||||
if ($type === Report::WEEKLY) {
|
if ($type === Report::WEEKLY) {
|
||||||
$content .= "<h2>" . Doo::translate("下周拟定计划") . "[" . $start_time->addWeek()->format("m/d") . "-" . $end_time->addWeek()->format("m/d") . "]</h2><ol><li> </li></ol>";
|
// 下周拟定计划:基于下周时间范围预生成候选任务
|
||||||
|
$nextPlanDatas = [];
|
||||||
|
if ($next_start_time && $next_end_time) {
|
||||||
|
$next_tasks = ProjectTask::buildUnfinishedTaskQuery($user->userid, $next_start_time, $next_end_time, false)->get();
|
||||||
|
if ($next_tasks->isNotEmpty()) {
|
||||||
|
foreach ($next_tasks as $task) {
|
||||||
|
$planTime = '-';
|
||||||
|
if ($task->start_at || $task->end_at) {
|
||||||
|
$startText = $task->start_at ? Carbon::parse($task->start_at)->format('Y-m-d H:i') : '';
|
||||||
|
$endText = $task->end_at ? Carbon::parse($task->end_at)->format('Y-m-d H:i') : '';
|
||||||
|
$planTime = trim($startText . ($endText ? (' ~ ' . $endText) : ''));
|
||||||
|
}
|
||||||
|
$nextPlanDatas[] = [
|
||||||
|
'[' . $task->project->name . '] ' . $task->name,
|
||||||
|
$planTime,
|
||||||
|
$task->taskUser->where("owner", 1)->map(function ($item) {
|
||||||
|
return User::userid2nickname($item->userid);
|
||||||
|
})->implode(", "),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$contents[] = '<p> </p>';
|
||||||
|
$contents[] = "<h2>" . Doo::translate("下周拟定计划") . "[" . $next_start_time->format("m/d") . "-" . $next_end_time->format("m/d") . "]</h2>";
|
||||||
|
$contents[] = view('report', [
|
||||||
|
'labels' => [
|
||||||
|
Doo::translate('计划描述'),
|
||||||
|
Doo::translate('计划时间'),
|
||||||
|
Doo::translate('负责人'),
|
||||||
|
],
|
||||||
|
'datas' => $nextPlanDatas,
|
||||||
|
])->render();
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
"time" => $start_time->toDateTimeString(),
|
"time" => $start_time->toDateTimeString(),
|
||||||
"sign" => $sign,
|
"sign" => $sign,
|
||||||
"title" => $title,
|
"title" => $title,
|
||||||
"content" => $content,
|
"content" => implode("", $contents),
|
||||||
"complete_task" => $complete_task,
|
|
||||||
"unfinished_task" => $unfinished_task,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($one) {
|
if ($one) {
|
||||||
$data['id'] = $one->id;
|
$data['id'] = $one->id;
|
||||||
}
|
}
|
||||||
@ -364,13 +491,15 @@ class ReportController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/report/detail 05. 报告详情
|
* @api {get} api/report/detail 报告详情
|
||||||
*
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup report
|
* @apiGroup report
|
||||||
* @apiName detail
|
* @apiName detail
|
||||||
*
|
*
|
||||||
* @apiParam {Number} [id] 报告id
|
* @apiParam {Number} [id] 报告ID
|
||||||
|
* @apiParam {String} [code] 报告分享代码,与ID二选一,优先ID
|
||||||
*
|
*
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
@ -379,30 +508,145 @@ class ReportController extends AbstractController
|
|||||||
public function detail(): array
|
public function detail(): array
|
||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
|
//
|
||||||
$id = intval(trim(Request::input("id")));
|
$id = intval(trim(Request::input("id")));
|
||||||
if (empty($id))
|
$code = trim(Request::input("code"));
|
||||||
|
//
|
||||||
|
if (empty($id) && empty($code)) {
|
||||||
return Base::retError("缺少ID参数");
|
return Base::retError("缺少ID参数");
|
||||||
|
}
|
||||||
$one = Report::getOne($id);
|
//
|
||||||
$one->type_val = $one->getRawOriginal("type");
|
if (!empty($id)) {
|
||||||
|
$one = Report::getOne($id);
|
||||||
// 标记为已读
|
$one->type_val = $one->getRawOriginal("type");
|
||||||
if (!empty($one->receivesUser)) {
|
// 标记为已读
|
||||||
foreach ($one->receivesUser as $item) {
|
if (!empty($one->receivesUser)) {
|
||||||
if ($item->userid === $user->userid && $item->pivot->read === 0) {
|
foreach ($one->receivesUser as $item) {
|
||||||
$one->receivesUser()->updateExistingPivot($user->userid, [
|
if ($item->userid === $user->userid && $item->pivot->read === 0) {
|
||||||
"read" => 1,
|
$one->receivesUser()->updateExistingPivot($user->userid, [
|
||||||
]);
|
"read" => 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
$link = ReportLink::whereCode($code)->first();
|
||||||
|
if (empty($link)) {
|
||||||
|
return Base::retError("报告不存在或已被删除");
|
||||||
|
}
|
||||||
|
$one = Report::getOne($link->rid);
|
||||||
|
$one->report_link = $link;
|
||||||
|
$link->increment("num");
|
||||||
|
}
|
||||||
|
$analysis = ReportAnalysis::query()
|
||||||
|
->whereRid($one->id)
|
||||||
|
->whereUserid($user->userid)
|
||||||
|
->first();
|
||||||
|
if ($analysis) {
|
||||||
|
$updatedAt = $analysis->updated_at ? $analysis->updated_at->toDateTimeString() : null;
|
||||||
|
$one->setAttribute('ai_analysis', [
|
||||||
|
'id' => $analysis->id,
|
||||||
|
'text' => $analysis->analysis_text,
|
||||||
|
'model' => $analysis->model,
|
||||||
|
'updated_at' => $updatedAt,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$one->setAttribute('ai_analysis', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Base::retSuccess("success", $one);
|
return Base::retSuccess("success", $one);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/report/mark 06. 标记已读/未读
|
* @api {post} api/report/analysave 保存工作汇报 AI 分析
|
||||||
*
|
*
|
||||||
|
* @apiDescription 需要token身份,仅支持报告提交人或接收人保存分析
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup report
|
||||||
|
* @apiName analysave
|
||||||
|
*
|
||||||
|
* @apiParam {Number} id 报告ID
|
||||||
|
* @apiParam {String} text 分析内容(Markdown)
|
||||||
|
* @apiParam {String} [model] 分析使用的模型标识(可选)
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
* @apiSuccess {Number} data.id 分析记录ID
|
||||||
|
* @apiSuccess {String} data.text 分析内容(Markdown)
|
||||||
|
* @apiSuccess {String} data.updated_at 最近更新时间
|
||||||
|
*/
|
||||||
|
public function analysave(): array
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$id = intval(Request::input("id"));
|
||||||
|
if ($id <= 0) {
|
||||||
|
return Base::retError("缺少ID参数");
|
||||||
|
}
|
||||||
|
$text = trim((string)Request::input('text', ''));
|
||||||
|
if ($text === '') {
|
||||||
|
return Base::retError("分析内容不能为空");
|
||||||
|
}
|
||||||
|
$model = trim((string)Request::input('model', ''));
|
||||||
|
|
||||||
|
$report = Report::getOne($id);
|
||||||
|
if (!$this->userCanAccessReport($report, $user)) {
|
||||||
|
return Base::retError("无权访问该工作汇报");
|
||||||
|
}
|
||||||
|
|
||||||
|
$analysis = ReportAnalysis::query()
|
||||||
|
->whereRid($report->id)
|
||||||
|
->whereUserid($user->userid)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$analysis) {
|
||||||
|
$analysis = ReportAnalysis::fillInstance([
|
||||||
|
'rid' => $report->id,
|
||||||
|
'userid' => $user->userid,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$viewerRole = $user->profession ?: (is_array($user->identity) && !empty($user->identity) ? implode('/', $user->identity) : null);
|
||||||
|
$focusMeta = null;
|
||||||
|
$focus = Request::input('focus');
|
||||||
|
if (is_array($focus)) {
|
||||||
|
$focusMeta = array_filter(array_map('trim', $focus));
|
||||||
|
} elseif (is_string($focus) && trim($focus) !== '') {
|
||||||
|
$focusMeta = [trim($focus)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = array_filter([
|
||||||
|
'viewer_role' => $viewerRole,
|
||||||
|
'viewer_name' => $user->nickname ?? null,
|
||||||
|
'focus' => $focusMeta,
|
||||||
|
], function ($value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
return !empty($value);
|
||||||
|
}
|
||||||
|
return $value !== null && $value !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
$analysis->updateInstance([
|
||||||
|
'model' => $model,
|
||||||
|
'analysis_text' => $text,
|
||||||
|
'meta' => $meta,
|
||||||
|
]);
|
||||||
|
$analysis->save();
|
||||||
|
|
||||||
|
$analysis->refresh();
|
||||||
|
|
||||||
|
return Base::retSuccess("success", [
|
||||||
|
'id' => $analysis->id,
|
||||||
|
'text' => $analysis->analysis_text,
|
||||||
|
'model' => $analysis->model,
|
||||||
|
'updated_at' => $analysis->updated_at ? $analysis->updated_at->toDateTimeString() : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/report/mark 标记已读/未读
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup report
|
* @apiGroup report
|
||||||
* @apiName mark
|
* @apiName mark
|
||||||
@ -443,8 +687,71 @@ class ReportController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人
|
* @api {get} api/report/share 分享报告到消息
|
||||||
*
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup report
|
||||||
|
* @apiName share
|
||||||
|
*
|
||||||
|
* @apiParam {Number} id 报告id(组)
|
||||||
|
* @apiParam {Array} dialogids 转发给的对话ID
|
||||||
|
* @apiParam {Array} userids 转发给的成员ID
|
||||||
|
* @apiParam {String} leave_message 转发留言
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function share()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
//
|
||||||
|
$id = Request::input('id');
|
||||||
|
$dialogids = Request::input('dialogids');
|
||||||
|
$userids = Request::input('userids');
|
||||||
|
$leave_message = Request::input('leave_message');
|
||||||
|
//
|
||||||
|
if (is_array($id)) {
|
||||||
|
if (count(Base::arrayRetainInt($id)) > 20) {
|
||||||
|
return Base::retError("最多只能操作20条数据");
|
||||||
|
}
|
||||||
|
$builder = Report::whereIn("id", Base::arrayRetainInt($id));
|
||||||
|
} else {
|
||||||
|
$builder = Report::whereId(intval($id));
|
||||||
|
}
|
||||||
|
$reportMsgs = [];
|
||||||
|
$builder ->chunkById(100, function ($list) use (&$reportMsgs, $user) {
|
||||||
|
/** @var Report $item */
|
||||||
|
foreach ($list as $item) {
|
||||||
|
$reportLink = ReportLink::generateLink($item->id, $user->userid);
|
||||||
|
$reportMsgs[] = "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/{$reportLink['code']}\" target=\"_blank\">%{$item->title}</a>";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (empty($reportMsgs)) {
|
||||||
|
return Base::retError("报告不存在或已被删除");
|
||||||
|
}
|
||||||
|
$reportTag = count($reportMsgs) > 1 ? 'li' : 'p';
|
||||||
|
$reportAttr = $reportTag === 'li' ? ' data-list="ordered"' : '';
|
||||||
|
$reportMsgs = array_map(function ($item) use ($reportAttr, $reportTag) {
|
||||||
|
return "<{$reportTag}{$reportAttr}>{$item}</{$reportTag}>";
|
||||||
|
}, $reportMsgs);
|
||||||
|
if ($reportTag === 'li') {
|
||||||
|
array_unshift($reportMsgs, "<ol>");
|
||||||
|
$reportMsgs[] = "</ol>";
|
||||||
|
}
|
||||||
|
if ($leave_message) {
|
||||||
|
$reportMsgs[] = "<p>{$leave_message}</p>";
|
||||||
|
}
|
||||||
|
$msgText = implode("", $reportMsgs);
|
||||||
|
//
|
||||||
|
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $msgText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/report/last_submitter 获取最后一次提交的接收人
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup report
|
* @apiGroup report
|
||||||
* @apiName last_submitter
|
* @apiName last_submitter
|
||||||
@ -460,8 +767,9 @@ class ReportController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/report/unread 08. 获取未读
|
* @api {get} api/report/unread 获取未读
|
||||||
*
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup report
|
* @apiGroup report
|
||||||
* @apiName unread
|
* @apiName unread
|
||||||
@ -474,15 +782,19 @@ class ReportController extends AbstractController
|
|||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
//
|
//
|
||||||
$data = Report::whereHas("Receives", function (Builder $query) use ($user) {
|
$total = Report::select('reports.id')
|
||||||
$query->where("userid", $user->userid)->where("read", 0);
|
->join('report_receives', 'report_receives.rid', '=', 'reports.id')
|
||||||
})->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
|
->where('report_receives.userid', $user->userid)
|
||||||
return Base::retSuccess("success", $data);
|
->where('report_receives.read', 0)
|
||||||
|
->count();
|
||||||
|
//
|
||||||
|
return Base::retSuccess("success", compact("total"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/report/read 09. 标记汇报已读,可批量
|
* @api {get} api/report/read 标记汇报已读,可批量
|
||||||
*
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup report
|
* @apiGroup report
|
||||||
* @apiName read
|
* @apiName read
|
||||||
@ -518,4 +830,22 @@ class ReportController extends AbstractController
|
|||||||
}
|
}
|
||||||
return Base::retSuccess("success", $data);
|
return Base::retSuccess("success", $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前用户是否有权限查看/分析指定工作汇报
|
||||||
|
* @param Report $report
|
||||||
|
* @param User $user
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function userCanAccessReport(Report $report, User $user): bool
|
||||||
|
{
|
||||||
|
if ($report->userid === $user->userid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReportReceive::query()
|
||||||
|
->whereRid($report->id)
|
||||||
|
->whereUserid($user->userid)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
619
app/Http/Controllers/Api/SearchController.php
Normal file
619
app/Http/Controllers/Api/SearchController.php
Normal file
@ -0,0 +1,619 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use Request;
|
||||||
|
use App\Models\File;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectTask;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\UserTag;
|
||||||
|
use App\Models\WebSocketDialog;
|
||||||
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Module\Base;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Manticore\ManticoreFile;
|
||||||
|
use App\Module\Manticore\ManticoreUser;
|
||||||
|
use App\Module\Manticore\ManticoreProject;
|
||||||
|
use App\Module\Manticore\ManticoreTask;
|
||||||
|
use App\Module\Manticore\ManticoreMsg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiDefine search
|
||||||
|
*
|
||||||
|
* 智能搜索
|
||||||
|
*/
|
||||||
|
class SearchController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @api {get} api/search/contact 搜索联系人
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份,优先使用 Manticore Search,未安装则使用 MySQL 搜索
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup search
|
||||||
|
* @apiName contact
|
||||||
|
*
|
||||||
|
* @apiParam {String} key 搜索关键词
|
||||||
|
* @apiParam {String} [search_type] 搜索类型(text/vector/hybrid,默认:hybrid,仅 Manticore 有效)
|
||||||
|
* @apiParam {Number} [take] 获取数量(默认:20,最大:50)
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function contact()
|
||||||
|
{
|
||||||
|
User::auth();
|
||||||
|
|
||||||
|
$key = trim(Request::input('key'));
|
||||||
|
$searchType = Request::input('search_type', 'hybrid');
|
||||||
|
$take = Base::getPaginate(50, 20, 'take');
|
||||||
|
|
||||||
|
if (empty($key)) {
|
||||||
|
return Base::retSuccess('success', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用 Manticore 搜索
|
||||||
|
if (Apps::isInstalled('search')) {
|
||||||
|
$results = ManticoreUser::search($key, $searchType, $take);
|
||||||
|
|
||||||
|
// 补充用户完整信息
|
||||||
|
$userids = array_column($results, 'userid');
|
||||||
|
if (!empty($userids)) {
|
||||||
|
$users = User::whereIn('userid', $userids)
|
||||||
|
->select(User::$basicField)
|
||||||
|
->get()
|
||||||
|
->keyBy('userid');
|
||||||
|
|
||||||
|
foreach ($results as &$item) {
|
||||||
|
$userData = $users->get($item['userid']);
|
||||||
|
if ($userData) {
|
||||||
|
// 标签直接从 Manticore 搜索结果获取(空格分隔的字符串转数组)
|
||||||
|
$tagsStr = $item['tags'] ?? '';
|
||||||
|
$searchTags = !empty($tagsStr) ? preg_split('/\s+/', trim($tagsStr)) : [];
|
||||||
|
|
||||||
|
$item = array_merge($userData->toArray(), [
|
||||||
|
'relevance' => $item['relevance'] ?? 0,
|
||||||
|
'introduction_preview' => $item['introduction_preview'] ?? null,
|
||||||
|
'search_tags' => $searchTags,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// MySQL 回退搜索
|
||||||
|
$results = $this->searchContactByMysql($key, $take);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('success', $results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL 回退搜索联系人
|
||||||
|
*
|
||||||
|
* @param string $key 搜索关键词
|
||||||
|
* @param int $take 获取数量
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function searchContactByMysql(string $key, int $take): array
|
||||||
|
{
|
||||||
|
$users = User::select(User::$basicField)
|
||||||
|
->where('bot', 0)
|
||||||
|
->whereNull('disable_at')
|
||||||
|
->searchByKeyword($key)
|
||||||
|
->orderByDesc('line_at')
|
||||||
|
->take($take)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// 获取用户标签
|
||||||
|
$userids = $users->pluck('userid')->toArray();
|
||||||
|
$userTags = $this->getUserTagsMap($userids);
|
||||||
|
|
||||||
|
return $users->map(function ($user) use ($userTags) {
|
||||||
|
return array_merge($user->toArray(), [
|
||||||
|
'relevance' => 0,
|
||||||
|
'introduction_preview' => null,
|
||||||
|
'search_tags' => $userTags[$user->userid] ?? [],
|
||||||
|
]);
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/search/project 搜索项目
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份,优先使用 Manticore Search,未安装则使用 MySQL 搜索
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup search
|
||||||
|
* @apiName project
|
||||||
|
*
|
||||||
|
* @apiParam {String} key 搜索关键词
|
||||||
|
* @apiParam {String} [search_type] 搜索类型(text/vector/hybrid,默认:hybrid,仅 Manticore 有效)
|
||||||
|
* @apiParam {Number} [take] 获取数量(默认:20,最大:50)
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function project()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
|
||||||
|
$key = trim(Request::input('key'));
|
||||||
|
$searchType = Request::input('search_type', 'hybrid');
|
||||||
|
$take = Base::getPaginate(50, 20, 'take');
|
||||||
|
|
||||||
|
if (empty($key)) {
|
||||||
|
return Base::retSuccess('success', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用 Manticore 搜索
|
||||||
|
if (Apps::isInstalled('search')) {
|
||||||
|
$results = ManticoreProject::search($user->userid, $key, $searchType, $take);
|
||||||
|
|
||||||
|
// 补充项目完整信息
|
||||||
|
$projectIds = array_column($results, 'project_id');
|
||||||
|
if (!empty($projectIds)) {
|
||||||
|
$projects = Project::whereIn('id', $projectIds)
|
||||||
|
->get()
|
||||||
|
->keyBy('id');
|
||||||
|
|
||||||
|
foreach ($results as &$item) {
|
||||||
|
$projectData = $projects->get($item['project_id']);
|
||||||
|
if ($projectData) {
|
||||||
|
$item = array_merge($projectData->toArray(), [
|
||||||
|
'relevance' => $item['relevance'] ?? 0,
|
||||||
|
'desc_preview' => $item['desc_preview'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// MySQL 回退搜索
|
||||||
|
$results = $this->searchProjectByMysql($user->userid, $key, $take);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('success', $results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL 回退搜索项目
|
||||||
|
*
|
||||||
|
* @param int $userid 用户ID
|
||||||
|
* @param string $key 搜索关键词
|
||||||
|
* @param int $take 获取数量
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function searchProjectByMysql(int $userid, string $key, int $take): array
|
||||||
|
{
|
||||||
|
$projects = Project::authData()
|
||||||
|
->whereNull('projects.archived_at')
|
||||||
|
->searchByKeyword($key)
|
||||||
|
->orderByDesc('projects.id')
|
||||||
|
->take($take)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $projects->map(function ($project) {
|
||||||
|
$array = $project->toArray();
|
||||||
|
$array['relevance'] = 0;
|
||||||
|
$array['desc_preview'] = null;
|
||||||
|
return $array;
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/search/task 搜索任务
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份,优先使用 Manticore Search,未安装则使用 MySQL 搜索
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup search
|
||||||
|
* @apiName task
|
||||||
|
*
|
||||||
|
* @apiParam {String} key 搜索关键词
|
||||||
|
* @apiParam {String} [search_type] 搜索类型(text/vector/hybrid,默认:hybrid,仅 Manticore 有效)
|
||||||
|
* @apiParam {Number} [take] 获取数量(默认:20,最大:50)
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function task()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
|
||||||
|
$key = trim(Request::input('key'));
|
||||||
|
$searchType = Request::input('search_type', 'hybrid');
|
||||||
|
$take = Base::getPaginate(50, 20, 'take');
|
||||||
|
|
||||||
|
if (empty($key)) {
|
||||||
|
return Base::retSuccess('success', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用 Manticore 搜索
|
||||||
|
if (Apps::isInstalled('search')) {
|
||||||
|
$results = ManticoreTask::search($user->userid, $key, $searchType, $take);
|
||||||
|
|
||||||
|
// 补充任务完整信息
|
||||||
|
$taskIds = array_column($results, 'task_id');
|
||||||
|
if (!empty($taskIds)) {
|
||||||
|
$tasks = ProjectTask::with(['taskUser', 'taskTag'])
|
||||||
|
->whereIn('id', $taskIds)
|
||||||
|
->get()
|
||||||
|
->keyBy('id');
|
||||||
|
|
||||||
|
foreach ($results as &$item) {
|
||||||
|
$taskData = $tasks->get($item['task_id']);
|
||||||
|
if ($taskData) {
|
||||||
|
$item = array_merge($taskData->toArray(), [
|
||||||
|
'relevance' => $item['relevance'] ?? 0,
|
||||||
|
'desc_preview' => $item['desc_preview'] ?? null,
|
||||||
|
'content_preview' => $item['content_preview'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// MySQL 回退搜索
|
||||||
|
$results = $this->searchTaskByMysql($user->userid, $key, $take);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('success', $results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL 回退搜索任务
|
||||||
|
*
|
||||||
|
* @param int $userid 用户ID
|
||||||
|
* @param string $key 搜索关键词
|
||||||
|
* @param int $take 获取数量
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function searchTaskByMysql(int $userid, string $key, int $take): array
|
||||||
|
{
|
||||||
|
$tasks = ProjectTask::with(['taskUser', 'taskTag'])
|
||||||
|
->whereIn('project_tasks.project_id', function ($query) use ($userid) {
|
||||||
|
$query->select('project_id')
|
||||||
|
->from('project_users')
|
||||||
|
->where('userid', $userid);
|
||||||
|
})
|
||||||
|
->whereNull('project_tasks.archived_at')
|
||||||
|
->whereNull('project_tasks.deleted_at')
|
||||||
|
->searchByKeyword($key)
|
||||||
|
->orderByDesc('project_tasks.id')
|
||||||
|
->take($take)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $tasks->map(function ($task) {
|
||||||
|
$array = $task->toArray();
|
||||||
|
$array['relevance'] = 0;
|
||||||
|
$array['desc_preview'] = null;
|
||||||
|
$array['content_preview'] = null;
|
||||||
|
return $array;
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/search/file 搜索文件
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份,优先使用 Manticore Search,未安装则使用 MySQL 搜索
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup search
|
||||||
|
* @apiName file
|
||||||
|
*
|
||||||
|
* @apiParam {String} key 搜索关键词
|
||||||
|
* @apiParam {String} [search_type] 搜索类型(text/vector/hybrid,默认:hybrid,仅 Manticore 有效)
|
||||||
|
* @apiParam {Number} [take] 获取数量(默认:20,最大:50)
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function file()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
|
||||||
|
$key = trim(Request::input('key'));
|
||||||
|
$searchType = Request::input('search_type', 'hybrid');
|
||||||
|
$take = Base::getPaginate(50, 20, 'take');
|
||||||
|
|
||||||
|
if (empty($key)) {
|
||||||
|
return Base::retSuccess('success', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用 Manticore 搜索
|
||||||
|
if (Apps::isInstalled('search')) {
|
||||||
|
$results = ManticoreFile::search($user->userid, $key, $searchType, 0, $take);
|
||||||
|
|
||||||
|
// 补充文件完整信息
|
||||||
|
$fileIds = array_column($results, 'file_id');
|
||||||
|
if (!empty($fileIds)) {
|
||||||
|
$files = File::whereIn('id', $fileIds)
|
||||||
|
->get()
|
||||||
|
->keyBy('id');
|
||||||
|
|
||||||
|
$formattedResults = [];
|
||||||
|
foreach ($results as $item) {
|
||||||
|
$fileData = $files->get($item['file_id']);
|
||||||
|
if ($fileData) {
|
||||||
|
$formattedResults[] = array_merge($fileData->toArray(), [
|
||||||
|
'relevance' => $item['relevance'] ?? 0,
|
||||||
|
'content_preview' => $item['content_preview'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Base::retSuccess('success', $formattedResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('success', []);
|
||||||
|
} else {
|
||||||
|
// MySQL 回退搜索
|
||||||
|
$results = $this->searchFileByMysql($user->userid, $key, $take);
|
||||||
|
return Base::retSuccess('success', $results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL 回退搜索文件
|
||||||
|
*
|
||||||
|
* @param int $userid 用户ID
|
||||||
|
* @param string $key 搜索关键词
|
||||||
|
* @param int $take 获取数量
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function searchFileByMysql(int $userid, string $key, int $take): array
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
// 搜索用户自己的文件
|
||||||
|
$ownFiles = File::where('userid', $userid)
|
||||||
|
->searchByKeyword($key)
|
||||||
|
->take($take)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($ownFiles as $file) {
|
||||||
|
$results[] = array_merge($file->toArray(), [
|
||||||
|
'relevance' => 0,
|
||||||
|
'content_preview' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索共享给用户的文件
|
||||||
|
$remaining = $take - count($results);
|
||||||
|
if ($remaining > 0) {
|
||||||
|
$sharedFiles = File::sharedToUser($userid)
|
||||||
|
->searchByKeyword($key)
|
||||||
|
->take($remaining)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($sharedFiles as $file) {
|
||||||
|
$temp = $file->toArray();
|
||||||
|
if ($file->pshare === $file->id) {
|
||||||
|
$temp['pid'] = 0;
|
||||||
|
}
|
||||||
|
$temp['relevance'] = 0;
|
||||||
|
$temp['content_preview'] = null;
|
||||||
|
$results[] = $temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/search/message 搜索消息
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份,优先使用 Manticore Search,未安装则使用 MySQL 搜索
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup search
|
||||||
|
* @apiName message
|
||||||
|
*
|
||||||
|
* @apiParam {String} key 搜索关键词
|
||||||
|
* @apiParam {String} [search_type] 搜索类型(text/vector/hybrid,默认:hybrid,仅 Manticore 有效)
|
||||||
|
* @apiParam {Number} [take] 获取数量(默认:20,最大:50)
|
||||||
|
* @apiParam {String} [mode] 返回模式(message/position/dialog,默认:message)
|
||||||
|
* - message: 返回消息详细信息
|
||||||
|
* - position: 只返回消息ID
|
||||||
|
* - dialog: 返回对话级数据
|
||||||
|
* @apiParam {Number} [dialog_id] 对话ID(筛选指定对话内的消息)
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function message()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
|
||||||
|
$key = trim(Request::input('key'));
|
||||||
|
$searchType = Request::input('search_type', 'hybrid');
|
||||||
|
$take = Base::getPaginate(50, 20, 'take');
|
||||||
|
$mode = Request::input('mode', 'message');
|
||||||
|
$dialogId = intval(Request::input('dialog_id', 0));
|
||||||
|
|
||||||
|
// 验证 mode 参数
|
||||||
|
if (!in_array($mode, ['message', 'position', 'dialog'])) {
|
||||||
|
$mode = 'message';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($key)) {
|
||||||
|
return Base::retSuccess('success', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果指定了 dialog_id,需要验证用户有权限访问该对话
|
||||||
|
if ($dialogId > 0) {
|
||||||
|
WebSocketDialog::checkDialog($dialogId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用 Manticore 搜索
|
||||||
|
if (Apps::isInstalled('search')) {
|
||||||
|
$results = ManticoreMsg::search($user->userid, $key, $searchType, 0, $take, $dialogId);
|
||||||
|
} else {
|
||||||
|
// MySQL 回退搜索
|
||||||
|
$results = $this->searchMessageByMysql($user->userid, $key, $take, $dialogId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 mode 返回不同格式的数据
|
||||||
|
return $this->formatMessageResults($results, $mode, $user->userid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL 回退搜索消息
|
||||||
|
*
|
||||||
|
* @param int $userid 用户ID
|
||||||
|
* @param string $key 搜索关键词
|
||||||
|
* @param int $take 获取数量
|
||||||
|
* @param int $dialogId 对话ID(0表示不限制)
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function searchMessageByMysql(int $userid, string $key, int $take, int $dialogId = 0): array
|
||||||
|
{
|
||||||
|
$builder = WebSocketDialogMsg::select([
|
||||||
|
'id as msg_id',
|
||||||
|
'dialog_id',
|
||||||
|
'userid',
|
||||||
|
'type',
|
||||||
|
'msg',
|
||||||
|
'created_at',
|
||||||
|
])
|
||||||
|
->accessibleByUser($userid)
|
||||||
|
->where('bot', 0)
|
||||||
|
->searchByKeyword($key);
|
||||||
|
|
||||||
|
if ($dialogId > 0) {
|
||||||
|
$builder->where('dialog_id', $dialogId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $builder->orderByDesc('id')
|
||||||
|
->limit($take)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $items->map(function ($item) {
|
||||||
|
return [
|
||||||
|
'msg_id' => $item->msg_id,
|
||||||
|
'dialog_id' => $item->dialog_id,
|
||||||
|
'userid' => $item->userid,
|
||||||
|
'type' => $item->type,
|
||||||
|
'msg' => $item->msg,
|
||||||
|
'created_at' => $item->created_at,
|
||||||
|
'relevance' => 0,
|
||||||
|
'content_preview' => null,
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化消息搜索结果
|
||||||
|
*
|
||||||
|
* @param array $results 搜索结果
|
||||||
|
* @param string $mode 返回模式
|
||||||
|
* @param int $userid 用户ID
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
private function formatMessageResults(array $results, string $mode, int $userid)
|
||||||
|
{
|
||||||
|
switch ($mode) {
|
||||||
|
case 'position':
|
||||||
|
// 只返回消息ID
|
||||||
|
$data = array_column($results, 'msg_id');
|
||||||
|
return Base::retSuccess('success', compact('data'));
|
||||||
|
|
||||||
|
case 'dialog':
|
||||||
|
// 返回对话级数据
|
||||||
|
$list = [];
|
||||||
|
$seenDialogs = [];
|
||||||
|
foreach ($results as $item) {
|
||||||
|
$dialogIdFromResult = $item['dialog_id'];
|
||||||
|
// 每个对话只返回一次
|
||||||
|
if (isset($seenDialogs[$dialogIdFromResult])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seenDialogs[$dialogIdFromResult] = true;
|
||||||
|
|
||||||
|
if ($dialog = WebSocketDialog::find($dialogIdFromResult)) {
|
||||||
|
$dialogData = array_merge($dialog->toArray(), [
|
||||||
|
'search_msg_id' => $item['msg_id'],
|
||||||
|
]);
|
||||||
|
$list[] = WebSocketDialog::synthesizeData($dialogData, $userid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Base::retSuccess('success', ['data' => $list]);
|
||||||
|
|
||||||
|
case 'message':
|
||||||
|
default:
|
||||||
|
// 返回消息详细信息(默认行为)
|
||||||
|
$msgIds = array_column($results, 'msg_id');
|
||||||
|
if (!empty($msgIds)) {
|
||||||
|
$msgs = WebSocketDialogMsg::whereIn('id', $msgIds)
|
||||||
|
->with(['user' => function ($query) {
|
||||||
|
$query->select(User::$basicField);
|
||||||
|
}])
|
||||||
|
->get()
|
||||||
|
->keyBy('id');
|
||||||
|
|
||||||
|
// 创建结果映射以保持原始顺序和额外字段
|
||||||
|
$resultsMap = [];
|
||||||
|
foreach ($results as $item) {
|
||||||
|
$resultsMap[$item['msg_id']] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formattedResults = [];
|
||||||
|
foreach ($msgIds as $msgId) {
|
||||||
|
$msgData = $msgs->get($msgId);
|
||||||
|
$originalItem = $resultsMap[$msgId] ?? [];
|
||||||
|
if ($msgData) {
|
||||||
|
$formattedResults[] = [
|
||||||
|
'id' => $msgData->id,
|
||||||
|
'msg_id' => $msgData->id,
|
||||||
|
'dialog_id' => $msgData->dialog_id,
|
||||||
|
'userid' => $msgData->userid,
|
||||||
|
'type' => $msgData->type,
|
||||||
|
'msg' => $msgData->msg,
|
||||||
|
'created_at' => $msgData->created_at,
|
||||||
|
'user' => $msgData->user,
|
||||||
|
'relevance' => $originalItem['relevance'] ?? 0,
|
||||||
|
'content_preview' => $originalItem['content_preview'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Base::retSuccess('success', $formattedResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('success', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取用户标签映射
|
||||||
|
*
|
||||||
|
* @param array $userids 用户ID数组
|
||||||
|
* @return array 用户ID => 标签名称数组的映射
|
||||||
|
*/
|
||||||
|
private function getUserTagsMap(array $userids): array
|
||||||
|
{
|
||||||
|
if (empty($userids)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有用户的标签(带认可数)
|
||||||
|
$tags = UserTag::whereIn('user_id', $userids)
|
||||||
|
->withCount('recognitions')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// 按用户分组,每个用户取 Top 10 标签
|
||||||
|
$result = [];
|
||||||
|
foreach ($userids as $userid) {
|
||||||
|
$result[$userid] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$userTags = $tags->groupBy('user_id');
|
||||||
|
foreach ($userTags as $userid => $tagCollection) {
|
||||||
|
$result[$userid] = $tagCollection
|
||||||
|
->sortByDesc('recognitions_count')
|
||||||
|
->take(10)
|
||||||
|
->pluck('name')
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
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
378
app/Http/Controllers/Api/apidoc.md
Normal file
378
app/Http/Controllers/Api/apidoc.md
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
# apiDoc 参数标签说明(完整速查)
|
||||||
|
|
||||||
|
apiDoc 使用内联注释为 RESTful API 自动生成文档。
|
||||||
|
以下为所有官方支持的参数与其说明。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @api
|
||||||
|
**定义 API 方法的基本信息**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@api {method} path title
|
||||||
|
```
|
||||||
|
|
||||||
|
- **method**:请求方法,如 `GET`、`POST`、`PUT`、`DELETE` 等
|
||||||
|
- **path**:请求路径,例如 `/user/:id`
|
||||||
|
- **title**:简短标题(显示在文档中)
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@api {get} /user/:id Get user info
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiBody
|
||||||
|
**定义请求体参数**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiBody [{type}] [field=defaultValue] [description]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `{type}` 参数类型(如 String, Number, Object, String[])
|
||||||
|
- `[field]` 可选字段(方括号表示可选)
|
||||||
|
- `=defaultValue` 默认值
|
||||||
|
- `description` 参数说明
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiBody {String} lastname Mandatory Lastname.
|
||||||
|
@apiBody {Object} [address] Optional address object.
|
||||||
|
@apiBody {String} [address[city]] Optional city.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiDefine
|
||||||
|
**定义可复用的文档块**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiDefine name [title] [description]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `name`:唯一标识
|
||||||
|
- `title`:简短标题
|
||||||
|
- `description`:多行描述
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiDefine MyError
|
||||||
|
@apiError UserNotFound The <code>id</code> of the User was not found.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiDeprecated
|
||||||
|
**标记接口为弃用状态**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiDeprecated [text]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `text`:提示文本,可带链接到新方法
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiDeprecated use now (#User:GetDetails)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiDescription
|
||||||
|
**描述接口详细说明**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiDescription text
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiDescription This is the Description.
|
||||||
|
It is multiline capable.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiError
|
||||||
|
**定义错误返回参数**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiError [(group)] [{type}] field [description]
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiError UserNotFound The id of the User was not found.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiErrorExample
|
||||||
|
**定义错误返回示例**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiErrorExample [{type}] [title]
|
||||||
|
example
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiErrorExample {json} Error-Response:
|
||||||
|
HTTP/1.1 404 Not Found
|
||||||
|
{ "error": "UserNotFound" }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiExample
|
||||||
|
**定义接口使用示例**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiExample [{type}] title
|
||||||
|
example
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiExample {curl} Example usage:
|
||||||
|
curl -i http://localhost/user/4711
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiGroup
|
||||||
|
**定义所属分组**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiGroup name
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiGroup User
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiHeader
|
||||||
|
**定义请求头参数**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiHeader [(group)] [{type}] [field=defaultValue] [description]
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiHeader {String} access-key Users unique access-key.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiHeaderExample
|
||||||
|
**定义请求头示例**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiHeaderExample [{type}] [title]
|
||||||
|
example
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiHeaderExample {json} Header-Example:
|
||||||
|
{
|
||||||
|
"Accept-Encoding": "gzip, deflate"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiIgnore
|
||||||
|
**忽略当前文档块**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiIgnore [hint]
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiIgnore Not finished method
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiName
|
||||||
|
**定义接口唯一名称**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiName name
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiName GetUser
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiParam
|
||||||
|
**定义请求参数**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiParam [(group)] [{type}] [field=defaultValue] [description]
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiParam {Number} id Users unique ID.
|
||||||
|
@apiParam {String} [firstname] Optional firstname.
|
||||||
|
@apiParam {String} country="DE" Mandatory with default.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiParamExample
|
||||||
|
**定义参数请求示例**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiParamExample [{type}] [title]
|
||||||
|
example
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiParamExample {json} Request-Example:
|
||||||
|
{ "id": 4711 }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiPermission
|
||||||
|
**定义权限要求**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiPermission name
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiPermission admin
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiPrivate
|
||||||
|
**标记接口为私有(可过滤)**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiPrivate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiQuery
|
||||||
|
**定义查询参数(?query)**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiQuery [{type}] [field=defaultValue] [description]
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiQuery {Number} id Users unique ID.
|
||||||
|
@apiQuery {String} [sort="asc"] Sort order.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiSampleRequest
|
||||||
|
**定义接口测试请求 URL**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiSampleRequest url
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiSampleRequest http://test.github.com/some_path/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiSuccess
|
||||||
|
**定义成功返回参数**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiSuccess [(group)] [{type}] field [description]
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiSuccess {String} firstname Firstname of the User.
|
||||||
|
@apiSuccess {String} lastname Lastname of the User.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiSuccessExample
|
||||||
|
**定义成功返回示例**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiSuccessExample [{type}] [title]
|
||||||
|
example
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiSuccessExample {json} Success-Response:
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
{ "firstname": "John", "lastname": "Doe" }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiUse
|
||||||
|
**引用定义块(@apiDefine)**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiUse name
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiDefine MySuccess
|
||||||
|
@apiSuccess {String} firstname User firstname.
|
||||||
|
|
||||||
|
@apiUse MySuccess
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## @apiVersion
|
||||||
|
**定义接口版本**
|
||||||
|
|
||||||
|
```js
|
||||||
|
@apiVersion version
|
||||||
|
```
|
||||||
|
|
||||||
|
📘 示例:
|
||||||
|
```js
|
||||||
|
@apiVersion 1.6.2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 附录:常用标签速查表
|
||||||
|
|
||||||
|
| 标签 | 作用 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `@api` | 定义接口 | `@api {get} /user/:id` |
|
||||||
|
| `@apiName` | 唯一名称 | `@apiName GetUser` |
|
||||||
|
| `@apiGroup` | 所属分组 | `@apiGroup User` |
|
||||||
|
| `@apiParam` | 请求参数 | `@apiParam {Number} id Users unique ID.` |
|
||||||
|
| `@apiBody` | 请求体参数 | `@apiBody {String} name Username.` |
|
||||||
|
| `@apiQuery` | 查询参数 | `@apiQuery {String} keyword Search term.` |
|
||||||
|
| `@apiHeader` | Header 参数 | `@apiHeader {String} token Auth token.` |
|
||||||
|
| `@apiSuccess` | 成功返回字段 | `@apiSuccess {String} name Username.` |
|
||||||
|
| `@apiError` | 错误返回字段 | `@apiError NotFound User not found.` |
|
||||||
|
| `@apiVersion` | 版本号 | `@apiVersion 1.0.0` |
|
||||||
@ -1,89 +1,137 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 给apidoc项目增加顺序编号
|
* 给apidoc项目增加顺序编号 / 支持恢复
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||||
|
|
||||||
$path = dirname(__FILE__). '/';
|
const NUMBER_WIDTH = 2;
|
||||||
$lists = scandir($path);
|
|
||||||
//
|
|
||||||
foreach ($lists AS $item) {
|
|
||||||
$fillPath = $path . $item;
|
|
||||||
if (str_ends_with($fillPath, 'Controller.php')) {
|
|
||||||
$content = file_get_contents($fillPath);
|
|
||||||
preg_match_all("/\* @api \{(.+?)\} (.*?)\n/i", $content, $matchs);
|
|
||||||
$i = 1;
|
|
||||||
foreach ($matchs[2] AS $key=>$text) {
|
|
||||||
if (in_array(strtolower($matchs[1][$key]), array('get', 'post'))) {
|
|
||||||
$expl = explode(" ", __sRemove($text));
|
|
||||||
$end = $expl[1];
|
|
||||||
if ($expl[2]) {
|
|
||||||
$end = '';
|
|
||||||
foreach ($expl AS $k=>$v) { if ($k >= 2) { $end.= " ".$v; } }
|
|
||||||
}
|
|
||||||
$newtext = "* @api {".$matchs[1][$key]."} ".$expl[0]." ".__zeroFill($i, 2).". ".trim($end);
|
|
||||||
$content = str_replace("* @api {".$matchs[1][$key]."} ".$text, $newtext, $content);
|
|
||||||
$i++;
|
|
||||||
//
|
|
||||||
echo $newtext;
|
|
||||||
echo "\r\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($i > 1) {
|
|
||||||
file_put_contents($fillPath, $content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
echo "Success \n";
|
|
||||||
|
|
||||||
/** ************************************************************** */
|
$isRestore = isset($argv[1]) && strtolower($argv[1]) === 'restore';
|
||||||
/** ************************************************************** */
|
|
||||||
/** ************************************************************** */
|
|
||||||
|
|
||||||
/**
|
$basePath = dirname(__FILE__) . '/';
|
||||||
* 替换所有空格
|
$controllerFiles = glob($basePath . '*Controller.php');
|
||||||
* @param $str
|
|
||||||
* @return mixed
|
if (!$controllerFiles) {
|
||||||
*/
|
echo "No Controller.php files found\n";
|
||||||
function __sRemove($str) {
|
exit(0);
|
||||||
$str = str_replace(" ", " ", $str);
|
|
||||||
if (__strExists($str, " ")) {
|
|
||||||
return __sRemove($str);
|
|
||||||
}
|
|
||||||
return $str;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($controllerFiles as $filePath) {
|
||||||
|
$original = file_get_contents($filePath);
|
||||||
|
[$updated, $linesChanged] = processFile($original, $isRestore);
|
||||||
|
|
||||||
|
if (count($linesChanged) === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($filePath, $updated);
|
||||||
|
|
||||||
|
foreach ($linesChanged as $line) {
|
||||||
|
echo $line . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $isRestore ? "Restore Success \n" : "Success \n";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否包含字符
|
* 处理单个文件内容
|
||||||
* @param $string
|
*
|
||||||
* @param $find
|
* @param string $content
|
||||||
* @return bool
|
* @param bool $restore
|
||||||
|
* @return array{string, array<int, string>}
|
||||||
*/
|
*/
|
||||||
function __strExists($string, $find)
|
function processFile(string $content, bool $restore): array
|
||||||
{
|
{
|
||||||
return str_contains($string, $find);
|
$lineChanges = [];
|
||||||
|
$counter = 1;
|
||||||
|
|
||||||
|
$pattern = '/\* @api \{([^\}]+)\}\s+([^\s]+)([^\r\n]*)(\r?\n)/';
|
||||||
|
|
||||||
|
$updated = preg_replace_callback(
|
||||||
|
$pattern,
|
||||||
|
function (array $matches) use ($restore, &$counter, &$lineChanges) {
|
||||||
|
$method = trim($matches[1]);
|
||||||
|
if (!in_array(strtolower($method), ['get', 'post'], true)) {
|
||||||
|
return $matches[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$endpoint = trim($matches[2]);
|
||||||
|
$suffix = normalizeDescription(stripExistingNumbering($matches[3]));
|
||||||
|
|
||||||
|
if (!$restore) {
|
||||||
|
$numberedSuffix = formatNumber($counter) . '.';
|
||||||
|
if ($suffix !== '') {
|
||||||
|
$numberedSuffix .= ' ' . $suffix;
|
||||||
|
}
|
||||||
|
$counter++;
|
||||||
|
} else {
|
||||||
|
$numberedSuffix = $suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newLine = renderAnnotation($method, $endpoint, $numberedSuffix);
|
||||||
|
|
||||||
|
if ($newLine !== rtrim($matches[0], "\r\n")) {
|
||||||
|
$lineChanges[] = $newLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $newLine . $matches[4];
|
||||||
|
},
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($updated === null) {
|
||||||
|
return [$content, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$updated, $lineChanges];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $str 补零
|
* 生成格式化后的注释行
|
||||||
* @param int $length
|
|
||||||
* @param int $after
|
|
||||||
* @return bool|string
|
|
||||||
*/
|
*/
|
||||||
function __zeroFill($str, $length = 0, $after = 1) {
|
function renderAnnotation(string $method, string $endpoint, string $suffix = ''): string
|
||||||
if (strlen($str) >= $length) {
|
{
|
||||||
return $str;
|
$line = "* @api {" . $method . "} " . $endpoint;
|
||||||
|
|
||||||
|
if ($suffix !== '') {
|
||||||
|
if ($suffix[0] !== ' ') {
|
||||||
|
$line .= ' ';
|
||||||
|
}
|
||||||
|
$line .= $suffix;
|
||||||
}
|
}
|
||||||
$_str = '';
|
|
||||||
for ($i = 0; $i < $length; $i++) {
|
return $line;
|
||||||
$_str .= '0';
|
}
|
||||||
}
|
|
||||||
if ($after) {
|
/**
|
||||||
$_ret = substr($_str . $str, $length * -1);
|
* 移除已有编号部分
|
||||||
} else {
|
*/
|
||||||
$_ret = substr($str . $_str, 0, $length);
|
function stripExistingNumbering(string $text): string
|
||||||
}
|
{
|
||||||
return $_ret;
|
$trimmed = ltrim($text);
|
||||||
|
$pattern = '/^\d+\.\s*/';
|
||||||
|
return preg_replace($pattern, '', $trimmed) ?? $trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩多余空格
|
||||||
|
*/
|
||||||
|
function normalizeDescription(string $text): string
|
||||||
|
{
|
||||||
|
$text = trim($text);
|
||||||
|
if ($text === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_replace('/\s+/', ' ', $text) ?? $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成固定宽度的数字
|
||||||
|
*/
|
||||||
|
function formatNumber(int $number): string
|
||||||
|
{
|
||||||
|
return str_pad((string) $number, NUMBER_WIDTH, '0', STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,20 +8,25 @@ use Request;
|
|||||||
use Redirect;
|
use Redirect;
|
||||||
use Response;
|
use Response;
|
||||||
use App\Models\File;
|
use App\Models\File;
|
||||||
|
use App\Module\Doo;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use App\Tasks\LoopTask;
|
|
||||||
use App\Module\Extranet;
|
use App\Module\Extranet;
|
||||||
use App\Tasks\AppPushTask;
|
|
||||||
use App\Module\RandomColor;
|
use App\Module\RandomColor;
|
||||||
|
use App\Tasks\LoopTask;
|
||||||
|
use App\Tasks\AppPushTask;
|
||||||
use App\Tasks\JokeSoupTask;
|
use App\Tasks\JokeSoupTask;
|
||||||
use App\Tasks\DeleteTmpTask;
|
use App\Tasks\DeleteTmpTask;
|
||||||
use App\Tasks\EmailNoticeTask;
|
use App\Tasks\EmailNoticeTask;
|
||||||
use App\Tasks\AutoArchivedTask;
|
use App\Tasks\AutoArchivedTask;
|
||||||
use App\Tasks\DeleteBotMsgTask;
|
use App\Tasks\DeleteBotMsgTask;
|
||||||
use App\Tasks\CheckinRemindTask;
|
use App\Tasks\CheckinRemindTask;
|
||||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
use App\Tasks\CloseMeetingRoomTask;
|
||||||
|
use App\Tasks\ManticoreSyncTask;
|
||||||
use App\Tasks\UnclaimedTaskRemindTask;
|
use App\Tasks\UnclaimedTaskRemindTask;
|
||||||
use LasseRafn\InitialAvatarGenerator\InitialAvatar;
|
use App\Tasks\TodoRemindTask;
|
||||||
|
use App\Tasks\AiTaskLoopTask;
|
||||||
|
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||||
|
use Laravolt\Avatar\Avatar;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,9 +42,8 @@ class IndexController extends InvokeController
|
|||||||
if ($action) {
|
if ($action) {
|
||||||
$app .= "__" . $action;
|
$app .= "__" . $action;
|
||||||
}
|
}
|
||||||
if ($app === 'manifest.txt') {
|
if ($app == 'default') {
|
||||||
$app = 'manifest';
|
return '';
|
||||||
$child = 'txt';
|
|
||||||
}
|
}
|
||||||
if (!method_exists($this, $app)) {
|
if (!method_exists($this, $app)) {
|
||||||
$app = method_exists($this, $method) ? $method : 'main';
|
$app = method_exists($this, $method) ? $method : 'main';
|
||||||
@ -59,58 +63,21 @@ class IndexController extends InvokeController
|
|||||||
$array = Base::json2array(file_get_contents($hotFile));
|
$array = Base::json2array(file_get_contents($hotFile));
|
||||||
$style = null;
|
$style = null;
|
||||||
$script = preg_replace("/^(\/\/(.*?))(:\d+)?\//i", "$1:" . $array['APP_DEV_PORT'] . "/", asset_main("resources/assets/js/app.js"));
|
$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 {
|
} else {
|
||||||
$array = Base::json2array(file_get_contents($manifestFile));
|
$array = Base::json2array(file_get_contents($manifestFile));
|
||||||
$style = asset_main($array['resources/assets/js/app.js']['css'][0]);
|
$style = asset_main($array['resources/assets/js/app.js']['css'][0]);
|
||||||
$script = asset_main($array['resources/assets/js/app.js']['file']);
|
$script = asset_main($array['resources/assets/js/app.js']['file']);
|
||||||
}
|
}
|
||||||
return response()->view('main', [
|
return response()->view('main', [
|
||||||
|
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
|
||||||
'version' => Base::getVersion(),
|
'version' => Base::getVersion(),
|
||||||
'style' => $style,
|
'style' => $style,
|
||||||
'script' => $script,
|
'script' => $script,
|
||||||
])->header('Link', "<" . url('manifest.txt') . ">; rel=\"prefetch\"");
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manifest
|
|
||||||
* @param $child
|
|
||||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|string
|
|
||||||
*/
|
|
||||||
public function manifest($child = '')
|
|
||||||
{
|
|
||||||
if (empty($child)) {
|
|
||||||
$murl = url('manifest.txt');
|
|
||||||
return response($murl)->header('Link', "<{$murl}>; rel=\"prefetch\"");
|
|
||||||
}
|
|
||||||
$array = [
|
|
||||||
"office/web-apps/apps/api/documents/api.js?hash=" . Base::getVersion(),
|
|
||||||
"office/7.5.1-23/web-apps/vendor/requirejs/require.js",
|
|
||||||
"office/7.5.1-23/web-apps/apps/api/documents/api.js",
|
|
||||||
"office/7.5.1-23/sdkjs/common/AllFonts.js",
|
|
||||||
"office/7.5.1-23/web-apps/vendor/xregexp/xregexp-all-min.js",
|
|
||||||
"office/7.5.1-23/web-apps/vendor/sockjs/sockjs.min.js",
|
|
||||||
"office/7.5.1-23/web-apps/vendor/jszip/jszip.min.js",
|
|
||||||
"office/7.5.1-23/web-apps/vendor/jszip-utils/jszip-utils.min.js",
|
|
||||||
"office/7.5.1-23/sdkjs/common/libfont/wasm/fonts.js",
|
|
||||||
"office/7.5.1-23/sdkjs/common/Charts/ChartStyles.js",
|
|
||||||
"office/7.5.1-23/sdkjs/slide/themes//themes.js",
|
|
||||||
|
|
||||||
"office/7.5.1-23/web-apps/apps/presentationeditor/main/app.js",
|
|
||||||
"office/7.5.1-23/sdkjs/slide/sdk-all-min.js",
|
|
||||||
"office/7.5.1-23/sdkjs/slide/sdk-all.js",
|
|
||||||
|
|
||||||
"office/7.5.1-23/web-apps/apps/documenteditor/main/app.js",
|
|
||||||
"office/7.5.1-23/sdkjs/word/sdk-all-min.js",
|
|
||||||
"office/7.5.1-23/sdkjs/word/sdk-all.js",
|
|
||||||
|
|
||||||
"office/7.5.1-23/web-apps/apps/spreadsheeteditor/main/app.js",
|
|
||||||
"office/7.5.1-23/sdkjs/cell/sdk-all-min.js",
|
|
||||||
"office/7.5.1-23/sdkjs/cell/sdk-all.js",
|
|
||||||
];
|
|
||||||
foreach ($array as &$item) {
|
|
||||||
$item = url($item);
|
|
||||||
}
|
|
||||||
return implode(PHP_EOL, $array);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,9 +89,18 @@ class IndexController extends InvokeController
|
|||||||
return Redirect::to(Base::fillUrl('api/system/version'), 301);
|
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
|
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||||
*/
|
*/
|
||||||
public function avatar()
|
public function avatar()
|
||||||
{
|
{
|
||||||
@ -132,34 +108,120 @@ class IndexController extends InvokeController
|
|||||||
if ($segment && preg_match('/.*?\.png$/i', $segment)) {
|
if ($segment && preg_match('/.*?\.png$/i', $segment)) {
|
||||||
$name = substr($segment, 0, -4);
|
$name = substr($segment, 0, -4);
|
||||||
} else {
|
} else {
|
||||||
$name = Request::input('name', 'H');
|
$name = Request::input('name', 'D');
|
||||||
}
|
}
|
||||||
$size = Request::input('size', 128);
|
$size = Request::input('size', 128);
|
||||||
$color = Request::input('color');
|
$color = Request::input('color');
|
||||||
$background = Request::input('background');
|
$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)) {
|
if (preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', $name)) {
|
||||||
$name = mb_substr($name, mb_strlen($name) - 2);
|
$name = mb_substr($name, mb_strlen($name) - 2);
|
||||||
}
|
}
|
||||||
|
if (empty($name)) {
|
||||||
|
$name = 'D';
|
||||||
|
}
|
||||||
if (empty($color)) {
|
if (empty($color)) {
|
||||||
$color = '#ffffff';
|
$color = '#ffffff';
|
||||||
$cacheKey = "avatarBackgroundColor::" . md5($name);
|
$cacheKey = "avatarBackgroundColor::" . md5($name);
|
||||||
$background = Cache::rememberForever($cacheKey, function() {
|
$background = Cache::rememberForever($cacheKey, function () {
|
||||||
return RandomColor::one(['luminosity' => 'dark']);
|
return RandomColor::one(['luminosity' => 'dark']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$avatar = new InitialAvatar();
|
$path = public_path('uploads/tmp/avatar/' . substr(md5($name), 0, 2));
|
||||||
$content = $avatar->name($name)
|
$file = Base::joinPath($path, md5($name) . '.png');
|
||||||
->size($size)
|
if (file_exists($file)) {
|
||||||
->color($color)
|
return response()->file($file, [
|
||||||
->background($background)
|
'Pragma' => 'public',
|
||||||
->fontSize(0.35)
|
'Cache-Control' => 'max-age=1814400',
|
||||||
->autoFont()
|
'Content-type' => 'image/png',
|
||||||
->generate()
|
'Expires' => gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400),
|
||||||
->stream('png', 100);
|
]);
|
||||||
|
}
|
||||||
|
Base::makeDir($path);
|
||||||
//
|
//
|
||||||
return response($content)
|
$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('Pragma', 'public')
|
||||||
->header('Cache-Control', 'max-age=1814400')
|
->header('Cache-Control', 'max-age=1814400')
|
||||||
->header('Content-type', 'image/png')
|
->header('Content-type', 'image/png')
|
||||||
@ -192,11 +254,13 @@ class IndexController extends InvokeController
|
|||||||
// App推送
|
// App推送
|
||||||
Task::deliver(new AppPushTask());
|
Task::deliver(new AppPushTask());
|
||||||
// 删除过期的临时表数据
|
// 删除过期的临时表数据
|
||||||
Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1));
|
Task::deliver(new DeleteTmpTask('tmp_msgs', 1));
|
||||||
Task::deliver(new DeleteTmpTask('task_worker', 12));
|
|
||||||
Task::deliver(new DeleteTmpTask('tmp'));
|
Task::deliver(new DeleteTmpTask('tmp'));
|
||||||
|
Task::deliver(new DeleteTmpTask('task_worker', 12));
|
||||||
Task::deliver(new DeleteTmpTask('file'));
|
Task::deliver(new DeleteTmpTask('file'));
|
||||||
Task::deliver(new DeleteTmpTask('file_pack'));
|
Task::deliver(new DeleteTmpTask('tmp_file', 24));
|
||||||
|
Task::deliver(new DeleteTmpTask('user_device', 24));
|
||||||
|
Task::deliver(new DeleteTmpTask('umeng_log', 24 * 3));
|
||||||
// 删除机器人消息
|
// 删除机器人消息
|
||||||
Task::deliver(new DeleteBotMsgTask());
|
Task::deliver(new DeleteBotMsgTask());
|
||||||
// 周期任务
|
// 周期任务
|
||||||
@ -207,6 +271,14 @@ class IndexController extends InvokeController
|
|||||||
Task::deliver(new JokeSoupTask());
|
Task::deliver(new JokeSoupTask());
|
||||||
// 未领取任务通知
|
// 未领取任务通知
|
||||||
Task::deliver(new UnclaimedTaskRemindTask());
|
Task::deliver(new UnclaimedTaskRemindTask());
|
||||||
|
// 待办提醒
|
||||||
|
Task::deliver(new TodoRemindTask());
|
||||||
|
// 关闭会议室
|
||||||
|
Task::deliver(new CloseMeetingRoomTask());
|
||||||
|
// Manticore Search 同步
|
||||||
|
Task::deliver(new ManticoreSyncTask());
|
||||||
|
// AI 任务建议
|
||||||
|
Task::deliver(new AiTaskLoopTask());
|
||||||
|
|
||||||
return "success";
|
return "success";
|
||||||
}
|
}
|
||||||
@ -222,80 +294,127 @@ class IndexController extends InvokeController
|
|||||||
if (strtolower($name) === 'latest') {
|
if (strtolower($name) === 'latest') {
|
||||||
$name = $latestVersion;
|
$name = $latestVersion;
|
||||||
}
|
}
|
||||||
// 上传
|
|
||||||
|
// 上传(header 中包含 publish-version)
|
||||||
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
|
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
|
||||||
|
// 判断密钥
|
||||||
$publishKey = Request::header('publish-key');
|
$publishKey = Request::header('publish-key');
|
||||||
if ($publishKey !== env('APP_KEY')) {
|
if ($publishKey !== env('APP_KEY')) {
|
||||||
return Base::retError("key error");
|
return Base::retError("key error");
|
||||||
}
|
}
|
||||||
if (version_compare($publishVersion, $latestVersion) > -1) { // 限制上传版本必须 ≥ 当前版本
|
// 判断版本
|
||||||
$publishPath = "uploads/desktop/{$publishVersion}/";
|
$action = Request::get('action');
|
||||||
$res = Base::upload([
|
$draftPath = "uploads/desktop-draft/{$publishVersion}/";
|
||||||
"file" => Request::file('file'),
|
if ($action === 'release') {
|
||||||
"type" => 'desktop',
|
// 将草稿版本发布为正式版本
|
||||||
"path" => $publishPath,
|
$draftPath = public_path($draftPath);
|
||||||
"fileName" => true
|
$releasePath = public_path("uploads/desktop/{$publishVersion}/");
|
||||||
]);
|
if (!file_exists($draftPath)) {
|
||||||
if (Base::isSuccess($res)) {
|
return Base::retError("draft version not exists");
|
||||||
file_put_contents($latestFile, $publishVersion);
|
|
||||||
}
|
}
|
||||||
return $res;
|
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,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
// 列表
|
|
||||||
if (preg_match("/^\d+\.\d+\.\d+$/", $name)) {
|
// 列表(访问路径 desktop/publish/{version})
|
||||||
$path = "uploads/desktop/{$name}";
|
if (preg_match("/^v*(\d+\.\d+\.\d+)$/", $name, $match)) {
|
||||||
$dirPath = public_path($path);
|
$paths = [
|
||||||
$lists = Base::readDir($dirPath);
|
"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 = [];
|
$files = [];
|
||||||
foreach ($lists as $file) {
|
foreach ($lists as $file) {
|
||||||
if (str_ends_with($file, '.yml') || str_ends_with($file, '.yaml')) {
|
if (preg_match('/\.(zip|yml|yaml|blockmap)$/i', $file) || str_ends_with($file, '-win.exe')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$fileName = Base::leftDelete($file, $dirPath);
|
$fileName = basename($file, $dirPath);
|
||||||
|
$fileSize = filesize($file);
|
||||||
$files[] = [
|
$files[] = [
|
||||||
'name' => substr($fileName, 1),
|
'name' => $fileName,
|
||||||
'time' => date("Y-m-d H:i:s", filemtime($file)),
|
'time' => date("Y-m-d H:i:s", filemtime($file)),
|
||||||
'size' => Base::readableBytes(filesize($file)),
|
'size' => $fileSize > 0 ? Base::readableBytes($fileSize) : 0,
|
||||||
'url' => Base::fillUrl($path . $fileName),
|
'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}"),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$path = "uploads/android";
|
return view('desktop', [
|
||||||
$dirPath = public_path($path);
|
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
|
||||||
$lists = Base::readDir($dirPath);
|
'version' => $match[1],
|
||||||
$apkFile = null;
|
'files' => $files,
|
||||||
foreach ($lists as $file) {
|
'is_draft' => $isDraft,
|
||||||
if (!str_ends_with($file, '.apk')) {
|
'latest_version' => $latestVersion,
|
||||||
continue;
|
'other_version' => array_reverse($otherVersion),
|
||||||
}
|
]);
|
||||||
if ($apkFile && strtotime($apkFile['time']) > filemtime($file)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$fileName = Base::leftDelete($file, $dirPath);
|
|
||||||
$apkFile = [
|
|
||||||
'name' => substr($fileName, 1),
|
|
||||||
'time' => date("Y-m-d H:i:s", filemtime($file)),
|
|
||||||
'size' => Base::readableBytes(filesize($file)),
|
|
||||||
'url' => Base::fillUrl($path . $fileName),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if ($apkFile) {
|
|
||||||
$files = array_merge([$apkFile], $files);
|
|
||||||
}
|
|
||||||
return view('desktop', ['version' => $name, 'files' => $files]);
|
|
||||||
}
|
}
|
||||||
// 下载
|
|
||||||
if ($name && file_exists($latestFile)) {
|
// 下载(Latest 版本内的文件,访问路径 desktop/publish/{fileName})
|
||||||
$publishVersion = file_get_contents($latestFile);
|
if ($name) {
|
||||||
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
|
$filePath = public_path("uploads/desktop/{$latestVersion}/{$name}");
|
||||||
$filePath = public_path("uploads/desktop/{$publishVersion}/{$name}");
|
if (file_exists($filePath)) {
|
||||||
if (file_exists($filePath)) {
|
return Response::download($filePath);
|
||||||
return Response::download($filePath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return abort(404);
|
|
||||||
|
// 404
|
||||||
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -321,125 +440,77 @@ class IndexController extends InvokeController
|
|||||||
$data = parse_url($key);
|
$data = parse_url($key);
|
||||||
$path = Arr::get($data, 'path');
|
$path = Arr::get($data, 'path');
|
||||||
$file = public_path($path);
|
$file = public_path($path);
|
||||||
|
// 防止 ../ 穿越获取到系统文件
|
||||||
|
abort_if(!str_starts_with(realpath($file), public_path()), 404);
|
||||||
|
// 如果文件不存在,直接返回 404
|
||||||
|
abort_if(!file_exists($file), 404);
|
||||||
//
|
//
|
||||||
if (file_exists($file)) {
|
parse_str($data['query'], $query);
|
||||||
parse_str($data['query'], $query);
|
$name = Arr::get($query, 'name');
|
||||||
$name = Arr::get($query, 'name');
|
$ext = strtolower(Arr::get($query, 'ext'));
|
||||||
$ext = strtolower(Arr::get($query, 'ext'));
|
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
|
||||||
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
|
if ($ext === 'pdf') {
|
||||||
if ($ext === 'pdf'
|
// 文件超过 10m 不支持在线预览,提示下载
|
||||||
&& (str_contains($userAgent, 'electron') || str_contains($userAgent, 'chrome'))) {
|
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, [
|
return Response::download($file, $name, [
|
||||||
'Content-Type' => 'application/pdf'
|
'Content-Type' => 'application/pdf'
|
||||||
], 'inline');
|
], 'inline');
|
||||||
}
|
}
|
||||||
//
|
// EEUI App 直接在线预览查看
|
||||||
if (in_array($ext, File::localExt)) {
|
if (Base::isEEUIApp() && Base::judgeClientVersion("0.34.47")) {
|
||||||
$url = Base::fillUrl($path);
|
if ($browser === 'safari-mobile') {
|
||||||
} else {
|
$redirectUrl = Base::fillUrl($path);
|
||||||
$url = 'http://' . env('APP_IPPR') . '.3/' . $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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($ext !== 'pdf') {
|
|
||||||
$url = Base::urlAddparameter($url, [
|
|
||||||
'fullfilename' => $name . '.' . $ext
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
$toUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
|
|
||||||
return Redirect::to($toUrl, 301);
|
|
||||||
}
|
}
|
||||||
return abort(404);
|
//
|
||||||
}
|
if (in_array($ext, File::localExt)) {
|
||||||
|
$url = Base::fillUrl($path);
|
||||||
/**
|
} else {
|
||||||
* 设置语言和皮肤
|
$url = 'http://nginx/' . $path;
|
||||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
|
}
|
||||||
*/
|
$url = Base::urlAddparameter($url, [
|
||||||
public function setting__theme_language()
|
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext
|
||||||
{
|
|
||||||
return view('setting', [
|
|
||||||
'theme' => Request::input('theme'),
|
|
||||||
'language' => Request::input('language')
|
|
||||||
]);
|
]);
|
||||||
}
|
$redirectUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
|
||||||
|
return Redirect::to($redirectUrl, 301);
|
||||||
/**
|
|
||||||
* 提取所有中文
|
|
||||||
* @return array|string
|
|
||||||
*/
|
|
||||||
public function allcn()
|
|
||||||
{
|
|
||||||
if (!Base::is_internal_ip(Base::getIp())) {
|
|
||||||
// 限制内网访问
|
|
||||||
return "Forbidden Access";
|
|
||||||
}
|
|
||||||
$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, '"'), "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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, ".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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return implode("\n", array_values($array));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use App\Tasks\IhttpTask;
|
|
||||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||||
@ -32,24 +29,7 @@ class InvokeController extends BaseController
|
|||||||
$msg = "404 not found (" . str_replace("__", "/", $app) . ").";
|
$msg = "404 not found (" . str_replace("__", "/", $app) . ").";
|
||||||
return Base::ajaxError($msg);
|
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);
|
$res = $this->__before($method, $action);
|
||||||
if ($res === true || Base::isSuccess($res)) {
|
if ($res === true || Base::isSuccess($res)) {
|
||||||
return $this->$app();
|
return $this->$app();
|
||||||
|
|||||||
@ -10,14 +10,19 @@ class TrustProxies extends Middleware
|
|||||||
/**
|
/**
|
||||||
* The trusted proxies for this application.
|
* The trusted proxies for this application.
|
||||||
*
|
*
|
||||||
|
* PHP(Swoole)只在内网被 nginx 访问,外部无法直连,故信任内网代理。
|
||||||
|
*
|
||||||
* @var array|string|null
|
* @var array|string|null
|
||||||
*/
|
*/
|
||||||
protected $proxies;
|
protected $proxies = '*';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The headers that should be used to detect proxies.
|
* The headers that should be used to detect proxies.
|
||||||
*
|
*
|
||||||
|
* 只采信 X-Forwarded-Proto:nginx 已用 $the_scheme 覆盖该头(值由 nginx 控制),
|
||||||
|
* 据此让 url() 实时跟随 https;host/for 一律不信,避免 Host 注入与 IP 伪造。
|
||||||
|
*
|
||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB;
|
protected $headers = Request::HEADER_X_FORWARDED_PROTO;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,10 @@ namespace App\Http\Middleware;
|
|||||||
|
|
||||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||||
|
|
||||||
|
use App\Module\Base;
|
||||||
use App\Module\Doo;
|
use App\Module\Doo;
|
||||||
|
use App\Services\RequestContext;
|
||||||
|
use Cache;
|
||||||
use Closure;
|
use Closure;
|
||||||
|
|
||||||
class WebApi
|
class WebApi
|
||||||
@ -18,11 +21,23 @@ class WebApi
|
|||||||
*/
|
*/
|
||||||
public function handle($request, Closure $next)
|
public function handle($request, Closure $next)
|
||||||
{
|
{
|
||||||
global $_A;
|
// 记录请求信息
|
||||||
$_A = [];
|
RequestContext::set('start_time', microtime(true));
|
||||||
|
RequestContext::set('header_language', $request->header('language'));
|
||||||
|
|
||||||
|
// 更新请求的基本URL
|
||||||
|
RequestContext::updateBaseUrl($request);
|
||||||
|
|
||||||
|
// 加载Doo类
|
||||||
Doo::load();
|
Doo::load();
|
||||||
|
|
||||||
|
// 记录 PC 端活跃时间
|
||||||
|
$userid = Doo::userId();
|
||||||
|
if ($userid > 0 && Base::isPc()) {
|
||||||
|
Cache::put("user_pc_active:{$userid}", time(), 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密请求内容
|
||||||
$encrypt = Doo::pgpParseStr($request->header('encrypt'));
|
$encrypt = Doo::pgpParseStr($request->header('encrypt'));
|
||||||
if ($request->isMethod('post')) {
|
if ($request->isMethod('post')) {
|
||||||
$version = $request->header('version');
|
$version = $request->header('version');
|
||||||
@ -41,12 +56,7 @@ class WebApi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = $next($request);
|
$response = $next($request);
|
||||||
|
|
||||||
// 加密返回内容
|
// 加密返回内容
|
||||||
@ -57,6 +67,16 @@ class WebApi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 返回响应
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function terminate()
|
||||||
|
{
|
||||||
|
// 请求结束后清理上下文
|
||||||
|
RequestContext::clean();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Ldap;
|
namespace App\Ldap;
|
||||||
|
|
||||||
|
use App\Exceptions\ApiException;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
|
use App\Services\RequestContext;
|
||||||
use LdapRecord\Configuration\ConfigurationException;
|
use LdapRecord\Configuration\ConfigurationException;
|
||||||
use LdapRecord\Container;
|
use LdapRecord\Container;
|
||||||
use LdapRecord\LdapRecordException;
|
use LdapRecord\LdapRecordException;
|
||||||
@ -11,20 +13,18 @@ use LdapRecord\Models\Model;
|
|||||||
|
|
||||||
class LdapUser extends Model
|
class LdapUser extends Model
|
||||||
{
|
{
|
||||||
protected static $init = null;
|
|
||||||
/**
|
/**
|
||||||
* The object classes of the LDAP model.
|
* The object classes of the LDAP model.
|
||||||
*
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
public static $objectClasses = [
|
public static $objectClasses = [
|
||||||
'inetOrgPerson',
|
|
||||||
'organizationalPerson',
|
|
||||||
'person',
|
'person',
|
||||||
'top',
|
'top',
|
||||||
'posixAccount',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static $emailAttrs = ['mail', 'cn', 'uid', 'userPrincipalName'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return mixed|null
|
* @return mixed|null
|
||||||
*/
|
*/
|
||||||
@ -68,19 +68,29 @@ class LdapUser extends Model
|
|||||||
return Base::settingFind('thirdAccessSetting', 'ldap_sync_local') === 'open';
|
return Base::settingFind('thirdAccessSetting', 'ldap_sync_local') === 'open';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取登录属性名
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function getLoginAttr(): string
|
||||||
|
{
|
||||||
|
$attr = Base::settingFind('thirdAccessSetting', 'ldap_login_attr');
|
||||||
|
return in_array($attr, ['cn', 'uid', 'mail', 'sAMAccountName', 'userPrincipalName']) ? $attr : 'cn';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化配置
|
* 初始化配置
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function initConfig()
|
public static function initConfig()
|
||||||
{
|
{
|
||||||
if (is_bool(self::$init)) {
|
if (RequestContext::has('ldap_init')) {
|
||||||
return self::$init;
|
return RequestContext::get('ldap_init');
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$setting = Base::setting('thirdAccessSetting');
|
$setting = Base::setting('thirdAccessSetting');
|
||||||
if ($setting['ldap_open'] !== 'open') {
|
if ($setting['ldap_open'] !== 'open') {
|
||||||
return self::$init = false;
|
return RequestContext::save('ldap_init', false);
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$connection = Container::getDefaultConnection();
|
$connection = Container::getDefaultConnection();
|
||||||
@ -92,15 +102,15 @@ class LdapUser extends Model
|
|||||||
"username" => $setting['ldap_user_dn'],
|
"username" => $setting['ldap_user_dn'],
|
||||||
"password" => $setting['ldap_password'],
|
"password" => $setting['ldap_password'],
|
||||||
]);
|
]);
|
||||||
return self::$init = true;
|
return RequestContext::save('ldap_init', true);
|
||||||
} catch (ConfigurationException $e) {
|
} catch (ConfigurationException $e) {
|
||||||
info($e->getMessage());
|
info($e->getMessage());
|
||||||
return self::$init = false;
|
return RequestContext::save('ldap_init', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取
|
* 通过管理员绑定搜索用户,然后用用户 DN 做 Bind 认证
|
||||||
* @param $username
|
* @param $username
|
||||||
* @param $password
|
* @param $password
|
||||||
* @return Model|null
|
* @return Model|null
|
||||||
@ -111,16 +121,68 @@ class LdapUser extends Model
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return self::static()
|
$loginAttr = self::getLoginAttr();
|
||||||
->where([
|
$row = self::static()
|
||||||
'cn' => $username,
|
->whereRaw($loginAttr, '=', $username)
|
||||||
'userPassword' => $password
|
->first();
|
||||||
])->first();
|
if (!$row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$connection = Container::getDefaultConnection();
|
||||||
|
if (!$connection->auth()->attempt($row->getDn(), $password)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Swoole 下连接共享,必须恢复管理员绑定
|
||||||
|
$connection->auth()->attempt(
|
||||||
|
$connection->getConfiguration()->get('username'),
|
||||||
|
$connection->getConfiguration()->get('password')
|
||||||
|
);
|
||||||
|
return $row;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
info("[LDAP] auth fail: " . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过邮箱查找 LDAP 用户
|
||||||
|
* @param $email
|
||||||
|
* @return Model|null
|
||||||
|
*/
|
||||||
|
public static function findByEmail($email): ?Model
|
||||||
|
{
|
||||||
|
if (!self::initConfig()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
foreach (self::$emailAttrs as $attr) {
|
||||||
|
$row = self::static()->whereRaw($attr, '=', $email)->first();
|
||||||
|
if ($row) {
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (\Exception) {
|
} catch (\Exception) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的邮箱(从 LDAP 记录中提取)
|
||||||
|
* @param Model $row
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public static function getUserEmail(Model $row): ?string
|
||||||
|
{
|
||||||
|
foreach (self::$emailAttrs as $attr) {
|
||||||
|
$val = $row->getFirstAttribute($attr);
|
||||||
|
if ($val && Base::isEmail($val)) {
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录
|
* 登录
|
||||||
* @param $username
|
* @param $username
|
||||||
@ -138,7 +200,18 @@ class LdapUser extends Model
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (empty($user)) {
|
if (empty($user)) {
|
||||||
$user = User::reg($username, $password);
|
$email = self::getUserEmail($row);
|
||||||
|
if (empty($email)) {
|
||||||
|
throw new ApiException('LDAP 用户缺少邮箱属性,请联系管理员配置');
|
||||||
|
}
|
||||||
|
$user = User::whereEmail($email)->first();
|
||||||
|
if (empty($user)) {
|
||||||
|
// LDAP 用户通过 LDAP 认证,本地密码用随机值以满足密码策略
|
||||||
|
$localPassword = Base::generatePassword(16) . 'Aa1!';
|
||||||
|
$user = User::reg($email, $localPassword);
|
||||||
|
} elseif (!$user->isLdap()) {
|
||||||
|
info("[LDAP] merged with existing local account: userid={$user->userid}, email={$email}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($user) {
|
if ($user) {
|
||||||
$userimg = $row->getPhoto();
|
$userimg = $row->getPhoto();
|
||||||
@ -173,7 +246,7 @@ class LdapUser extends Model
|
|||||||
}
|
}
|
||||||
//
|
//
|
||||||
if (self::isSyncLocal()) {
|
if (self::isSyncLocal()) {
|
||||||
$row = self::userFirst($user->email, $password);
|
$row = self::findByEmail($user->email);
|
||||||
if ($row) {
|
if ($row) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -184,17 +257,18 @@ class LdapUser extends Model
|
|||||||
} else {
|
} else {
|
||||||
$userimg = '';
|
$userimg = '';
|
||||||
}
|
}
|
||||||
self::static()->create([
|
$attrs = [
|
||||||
'cn' => $user->email,
|
'cn' => $user->email,
|
||||||
'gidNumber' => 0,
|
|
||||||
'homeDirectory' => '/home/ldap/dootask/' . env("APP_NAME"),
|
|
||||||
'sn' => $user->email,
|
'sn' => $user->email,
|
||||||
'uid' => $user->email,
|
'uid' => $user->email,
|
||||||
'uidNumber' => $user->userid,
|
|
||||||
'userPassword' => $password,
|
'userPassword' => $password,
|
||||||
'displayName' => $user->nickname,
|
'displayName' => $user->nickname,
|
||||||
'jpegPhoto' => $userimg,
|
'mail' => $user->email,
|
||||||
]);
|
];
|
||||||
|
if ($userimg) {
|
||||||
|
$attrs['jpegPhoto'] = $userimg;
|
||||||
|
}
|
||||||
|
self::static()->create($attrs);
|
||||||
$user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['ldap']), ['ldap']));
|
$user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['ldap']), ['ldap']));
|
||||||
$user->save();
|
$user->save();
|
||||||
} catch (LdapRecordException $e) {
|
} catch (LdapRecordException $e) {
|
||||||
@ -205,11 +279,11 @@ class LdapUser extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新
|
* 更新
|
||||||
* @param $username
|
* @param $email
|
||||||
* @param $array
|
* @param $array
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function userUpdate($username, $array)
|
public static function userUpdate($email, $array)
|
||||||
{
|
{
|
||||||
if (empty($array)) {
|
if (empty($array)) {
|
||||||
return;
|
return;
|
||||||
@ -218,10 +292,7 @@ class LdapUser extends Model
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$row = self::static()
|
$row = self::findByEmail($email);
|
||||||
->where([
|
|
||||||
'cn' => $username,
|
|
||||||
])->first();
|
|
||||||
$row?->update($array);
|
$row?->update($array);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
info("[LDAP] update fail: " . $e->getMessage());
|
info("[LDAP] update fail: " . $e->getMessage());
|
||||||
@ -230,19 +301,16 @@ class LdapUser extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除
|
* 删除
|
||||||
* @param $username
|
* @param $email
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function userDelete($username)
|
public static function userDelete($email)
|
||||||
{
|
{
|
||||||
if (!self::initConfig()) {
|
if (!self::initConfig()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$row = self::static()
|
$row = self::findByEmail($email);
|
||||||
->where([
|
|
||||||
'cn' => $username,
|
|
||||||
])->first();
|
|
||||||
$row?->delete();
|
$row?->delete();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
info("[LDAP] delete fail: " . $e->getMessage());
|
info("[LDAP] delete fail: " . $e->getMessage());
|
||||||
|
|||||||
@ -20,9 +20,7 @@ use Illuminate\Support\Facades\DB;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelAppend()
|
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelAppend()
|
||||||
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelHidden()
|
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelHidden()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|static with($relations)
|
* @method static \Illuminate\Database\Eloquent\Builder|static with($relations)
|
||||||
* @method static \Illuminate\Database\Query\Builder|static select($columns = [])
|
* @method static \Illuminate\Pagination\LengthAwarePaginator paginate(callable $callback)
|
||||||
* @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 change(array $array)
|
||||||
* @method int remove()
|
* @method int remove()
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
@ -34,6 +32,28 @@ class AbstractModel extends Model
|
|||||||
const ID = 'id';
|
const ID = 'id';
|
||||||
|
|
||||||
protected $dates = [
|
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',
|
||||||
|
'remind_at',
|
||||||
|
'reminded_at',
|
||||||
|
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
'deleted_at',
|
'deleted_at',
|
||||||
@ -131,6 +151,25 @@ class AbstractModel extends Model
|
|||||||
return $date->format($this->dateFormat ?: 'Y-m-d H:i:s');
|
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
|
* @param array $param
|
||||||
@ -189,24 +228,44 @@ class AbstractModel extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据库更新或插入
|
* 数据库更新或插入
|
||||||
* @param $where
|
* @param array $where 查询条件
|
||||||
* @param array $update 存在时更新的内容
|
* @param array|\Closure $update 存在时更新的内容
|
||||||
* @param array $insert 不存在时插入的内容,如果没有则插入更新内容
|
* @param array|\Closure $insert 不存在时插入的内容,如果没有则插入更新内容
|
||||||
* @param bool $isInsert 是否是插入数据
|
* @param bool $isInsert 是否是插入数据
|
||||||
|
* @param bool|null $lockForUpdate 是否加锁(true:加锁,false:不加锁,null:在事务中会自动加锁)
|
||||||
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
|
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
|
||||||
*/
|
*/
|
||||||
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true)
|
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true, $lockForUpdate = null)
|
||||||
{
|
{
|
||||||
$row = static::where($where)->first();
|
$query = static::where($where);
|
||||||
|
if ($lockForUpdate === null) {
|
||||||
|
$lockForUpdate = \DB::transactionLevel() > 0;
|
||||||
|
}
|
||||||
|
if ($lockForUpdate) {
|
||||||
|
$query->lockForUpdate();
|
||||||
|
}
|
||||||
|
$row = $query->first();
|
||||||
if (empty($row)) {
|
if (empty($row)) {
|
||||||
$row = new static;
|
$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])) {
|
if (isset($array[$row->primaryKey])) {
|
||||||
unset($array[$row->primaryKey]);
|
unset($array[$row->primaryKey]);
|
||||||
}
|
}
|
||||||
$row->updateInstance($array);
|
$row->updateInstance($array);
|
||||||
$isInsert = true;
|
$isInsert = true;
|
||||||
} elseif ($update) {
|
} elseif ($update) {
|
||||||
|
if ($update instanceof \Closure) {
|
||||||
|
$update = $update();
|
||||||
|
}
|
||||||
$row->updateInstance($update);
|
$row->updateInstance($update);
|
||||||
$isInsert = false;
|
$isInsert = false;
|
||||||
}
|
}
|
||||||
|
|||||||
22
app/Models/AiAssistantSession.php
Normal file
22
app/Models/AiAssistantSession.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 助手会话
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $userid
|
||||||
|
* @property string $session_key
|
||||||
|
* @property string $session_id
|
||||||
|
* @property string $scene_key
|
||||||
|
* @property string $title
|
||||||
|
* @property string|null $data
|
||||||
|
* @property string|null $images
|
||||||
|
* @property \Carbon\Carbon $created_at
|
||||||
|
* @property \Carbon\Carbon $updated_at
|
||||||
|
*/
|
||||||
|
class AiAssistantSession extends AbstractModel
|
||||||
|
{
|
||||||
|
protected $table = 'ai_assistant_sessions';
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,9 +11,15 @@ namespace App\Models;
|
|||||||
* @property int|null $msg_id 消息ID
|
* @property int|null $msg_id 消息ID
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg query()
|
* @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 whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereId($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 whereMsgId($value)
|
||||||
|
|||||||
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
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@ -12,9 +12,15 @@ use Carbon\Carbon;
|
|||||||
* @property int|null $did 删除的数据ID
|
* @property int|null $did 删除的数据ID
|
||||||
* @property int|null $userid 关系会员ID
|
* @property int|null $userid 关系会员ID
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Deleted newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|Deleted newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Deleted query()
|
* @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 whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereDid($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 whereId($value)
|
||||||
|
|||||||
@ -3,8 +3,11 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Request;
|
use Request;
|
||||||
|
use App\Module\Apps;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use App\Tasks\PushTask;
|
use App\Tasks\PushTask;
|
||||||
|
use App\Tasks\ManticoreSyncTask;
|
||||||
|
use App\Observers\AbstractObserver;
|
||||||
use App\Exceptions\ApiException;
|
use App\Exceptions\ApiException;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||||
@ -23,20 +26,30 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
* @property int|null $size 大小(B)
|
* @property int|null $size 大小(B)
|
||||||
* @property int|null $userid 拥有者ID
|
* @property int|null $userid 拥有者ID
|
||||||
* @property int|null $share 是否共享
|
* @property int|null $share 是否共享
|
||||||
|
* @property int|null $guest_access 是否允许游客访问
|
||||||
* @property int|null $pshare 所属分享ID
|
* @property int|null $pshare 所属分享ID
|
||||||
* @property int|null $created_id 创建者
|
* @property int|null $created_id 创建者
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property \Illuminate\Support\Carbon|null $deleted_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|File newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|File newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|File onlyTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|File onlyTrashed()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|File query()
|
* @method static \Illuminate\Database\Eloquent\Builder|File query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|File searchByKeyword(string $keyword)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|File sharedToUser(int $userid)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereCid($value)
|
* @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 whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedId($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 whereDeletedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereExt($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 whereId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|File whereName($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 wherePid($value)
|
||||||
@ -73,9 +86,28 @@ class File extends AbstractModel
|
|||||||
* office文件
|
* office文件
|
||||||
*/
|
*/
|
||||||
const officeExt = [
|
const officeExt = [
|
||||||
'doc', 'docx',
|
// 文本文件
|
||||||
'xls', 'xlsx',
|
'doc', 'docx', // Microsoft Word 文档
|
||||||
'ppt', 'pptx',
|
'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 演示文稿模板
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,7 +124,7 @@ class File extends AbstractModel
|
|||||||
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw',
|
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw',
|
||||||
'tif', 'tiff',
|
'tif', 'tiff',
|
||||||
'mp3', 'wav', 'mp4', 'flv',
|
'mp3', 'wav', 'mp4', 'flv',
|
||||||
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
|
// 'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm', // 这一排是要转换的,无法使用本地播放
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,11 +132,52 @@ class File extends AbstractModel
|
|||||||
*/
|
*/
|
||||||
const zipMaxSize = 1024 * 1024 * 1024; // 1G
|
const zipMaxSize = 1024 * 1024 * 1024; // 1G
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按关键词搜索文件(Scope)
|
||||||
|
* 支持:文件ID(纯数字)、文件名
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param string $keyword 搜索关键词
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeSearchByKeyword($query, string $keyword)
|
||||||
|
{
|
||||||
|
if (is_numeric($keyword)) {
|
||||||
|
return $query->where(function ($q) use ($keyword) {
|
||||||
|
$q->where("id", intval($keyword))
|
||||||
|
->orWhere("name", "like", "%{$keyword}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return $query->where("name", "like", "%{$keyword}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 筛选用户可访问的共享文件(Scope)
|
||||||
|
* 不包括用户自己的文件,仅返回他人共享给该用户的文件
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param int $userid 用户ID
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeSharedToUser($query, int $userid)
|
||||||
|
{
|
||||||
|
return $query->whereIn('pshare', function ($subQuery) use ($userid) {
|
||||||
|
$subQuery->select('files.id')
|
||||||
|
->from('files')
|
||||||
|
->join('file_users', 'files.id', '=', 'file_users.file_id')
|
||||||
|
->where('files.userid', '!=', $userid)
|
||||||
|
->where(function ($q) use ($userid) {
|
||||||
|
$q->whereIn('file_users.userid', [0, $userid]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文件列表
|
* 获取文件列表
|
||||||
* @param user $user
|
* @param user $user
|
||||||
* @param int $pid
|
* @param int $pid
|
||||||
|
* @param string $type
|
||||||
|
* @param bool $isGetparent
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getFileList($user, int $pid, $type = "all", $isGetparent = true)
|
public function getFileList($user, int $pid, $type = "all", $isGetparent = true)
|
||||||
@ -112,7 +185,7 @@ class File extends AbstractModel
|
|||||||
$permission = 1000;
|
$permission = 1000;
|
||||||
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
|
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
|
||||||
$builder = File::wherePid($pid)
|
$builder = File::wherePid($pid)
|
||||||
->when($type=='dir',function($q){
|
->when($type == 'dir', function ($q) {
|
||||||
$q->whereType('folder');
|
$q->whereType('folder');
|
||||||
});
|
});
|
||||||
if ($pid > 0) {
|
if ($pid > 0) {
|
||||||
@ -128,7 +201,7 @@ class File extends AbstractModel
|
|||||||
//
|
//
|
||||||
if ($pid > 0) {
|
if ($pid > 0) {
|
||||||
// 遍历获取父级
|
// 遍历获取父级
|
||||||
if($isGetparent){
|
if ($isGetparent) {
|
||||||
while ($pid > 0) {
|
while ($pid > 0) {
|
||||||
$file = File::whereId($pid)->first();
|
$file = File::whereId($pid)->first();
|
||||||
if (empty($file)) {
|
if (empty($file)) {
|
||||||
@ -166,8 +239,8 @@ class File extends AbstractModel
|
|||||||
->whereIn('file_users.userid', $userids)
|
->whereIn('file_users.userid', $userids)
|
||||||
->groupBy('files.id')
|
->groupBy('files.id')
|
||||||
->take(100)
|
->take(100)
|
||||||
->when($type=='dir',function($q){
|
->when($type == 'dir', function ($q) {
|
||||||
$q->where('files.type','folder');
|
$q->where('files.type', 'folder');
|
||||||
})
|
})
|
||||||
->get();
|
->get();
|
||||||
if ($list->isNotEmpty()) {
|
if ($list->isNotEmpty()) {
|
||||||
@ -190,9 +263,10 @@ class File extends AbstractModel
|
|||||||
* @param user $user
|
* @param user $user
|
||||||
* @param int $pid
|
* @param int $pid
|
||||||
* @param string $webkitRelativePath
|
* @param string $webkitRelativePath
|
||||||
|
* @param bool $overwrite
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function contentUpload($user, int $pid, $webkitRelativePath)
|
public function contentUpload($user, int $pid, $webkitRelativePath, $overwrite = false)
|
||||||
{
|
{
|
||||||
$userid = $user->userid;
|
$userid = $user->userid;
|
||||||
if ($pid > 0) {
|
if ($pid > 0) {
|
||||||
@ -238,14 +312,13 @@ class File extends AbstractModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$setting = Base::setting('system');
|
$path = 'uploads/tmp/file/' . date("Ym") . '/';
|
||||||
$path = 'uploads/tmp/' . date("Ym") . '/';
|
|
||||||
$data = Base::upload([
|
$data = Base::upload([
|
||||||
"file" => Request::file('files'),
|
"file" => Request::file('files'),
|
||||||
"type" => 'more',
|
"type" => 'more',
|
||||||
"autoThumb" => false,
|
"autoThumb" => false,
|
||||||
"path" => $path,
|
"path" => $path,
|
||||||
"size" => ($setting['file_upload_limit'] ?: 0) * 1024
|
"quality" => true
|
||||||
]);
|
]);
|
||||||
if (Base::isError($data)) {
|
if (Base::isError($data)) {
|
||||||
throw new ApiException($data['msg']);
|
throw new ApiException($data['msg']);
|
||||||
@ -256,9 +329,9 @@ class File extends AbstractModel
|
|||||||
'text', 'md', 'markdown' => 'document',
|
'text', 'md', 'markdown' => 'document',
|
||||||
'drawio' => 'drawio',
|
'drawio' => 'drawio',
|
||||||
'mind' => 'mind',
|
'mind' => 'mind',
|
||||||
'doc', 'docx' => "word",
|
'doc', 'docx', 'dot', 'dotx', 'odt', 'ott', 'rtf' => "word",
|
||||||
'xls', 'xlsx' => "excel",
|
'xls', 'xlsx', 'xlsm', 'xlt', 'xltx', 'ods', 'ots', 'csv', 'tsv' => "excel",
|
||||||
'ppt', 'pptx' => "ppt",
|
'ppt', 'pptx', 'pps', 'ppsx', 'pot', 'potx', 'odp', 'otp' => "ppt",
|
||||||
'wps' => "wps",
|
'wps' => "wps",
|
||||||
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw', 'svg' => "picture",
|
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw', 'svg' => "picture",
|
||||||
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z', 'gz', 'apk', 'dmg' => "archive",
|
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z', 'gz', 'apk', 'dmg' => "archive",
|
||||||
@ -283,17 +356,25 @@ class File extends AbstractModel
|
|||||||
if ($data['ext'] == 'markdown') {
|
if ($data['ext'] == 'markdown') {
|
||||||
$data['ext'] = 'md';
|
$data['ext'] = 'md';
|
||||||
}
|
}
|
||||||
$file = File::createInstance([
|
$file = null;
|
||||||
|
$params = [
|
||||||
'pid' => $pid,
|
'pid' => $pid,
|
||||||
'name' => Base::rightDelete($data['name'], '.' . $data['ext']),
|
'name' => Base::rightDelete($data['name'], '.' . $data['ext']),
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
'ext' => $data['ext'],
|
'ext' => $data['ext'],
|
||||||
'userid' => $userid,
|
'userid' => $userid,
|
||||||
'created_id' => $user->userid,
|
'created_id' => $user->userid,
|
||||||
]);
|
];
|
||||||
$file->handleDuplicateName();
|
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 ($addItem, $webkitRelativePath, $type, $user, $data, $file) {
|
return AbstractModel::transaction(function () use ($overwrite, $addItem, $webkitRelativePath, $type, $user, $data, $file) {
|
||||||
$file->size = $data['size'] * 1024;
|
$file->size = $data['size'] * 1024;
|
||||||
$file->saveBeforePP();
|
$file->saveBeforePP();
|
||||||
//
|
//
|
||||||
@ -321,11 +402,12 @@ class File extends AbstractModel
|
|||||||
$tmpRow->pushMsg('add', $tmpRow);
|
$tmpRow->pushMsg('add', $tmpRow);
|
||||||
//
|
//
|
||||||
$data = File::handleImageUrl($tmpRow->toArray());
|
$data = File::handleImageUrl($tmpRow->toArray());
|
||||||
$data['full_name'] = $webkitRelativePath ?: $data['name'];
|
$data['full_name'] = $webkitRelativePath ?: ($data['name'] . '.' . $data['ext']);
|
||||||
|
$data['overwrite'] = $overwrite ? 1 : 0;
|
||||||
//
|
//
|
||||||
$addItem[] = $data;
|
$addItem[] = $data;
|
||||||
|
|
||||||
return ['data'=>$data,'addItem'=>$addItem];
|
return ['data' => $data, 'addItem' => $addItem];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +418,8 @@ class File extends AbstractModel
|
|||||||
*/
|
*/
|
||||||
public function getPermission(array $userids)
|
public function getPermission(array $userids)
|
||||||
{
|
{
|
||||||
if (in_array($this->userid, $userids) || in_array($this->created_id, $userids)) {
|
$validUserIds = array_filter($userids);
|
||||||
|
if (in_array($this->userid, $validUserIds) || in_array($this->created_id, $validUserIds)) {
|
||||||
// ① 自己的文件夹 或 自己创建的文件夹
|
// ① 自己的文件夹 或 自己创建的文件夹
|
||||||
return 1000;
|
return 1000;
|
||||||
}
|
}
|
||||||
@ -544,6 +627,26 @@ class File extends AbstractModel
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新子文件的 userid 并同步到 Manticore
|
||||||
|
* @param int $userid 新的 userid
|
||||||
|
* @return int 更新的文件数量
|
||||||
|
*/
|
||||||
|
public function updateChildFilesUserid(int $userid): int
|
||||||
|
{
|
||||||
|
self::where('pids', 'like', "%,{$this->id},%")->update(['userid' => $userid]);
|
||||||
|
|
||||||
|
// 批量 update 绕过 Observer,手动触发 Manticore 同步
|
||||||
|
$childFileIds = self::where('pids', 'like', "%,{$this->id},%")
|
||||||
|
->where('type', '!=', 'folder')
|
||||||
|
->pluck('id')
|
||||||
|
->toArray();
|
||||||
|
foreach ($childFileIds as $childFileId) {
|
||||||
|
AbstractObserver::taskDeliver(new ManticoreSyncTask('file_sync', ['id' => $childFileId]));
|
||||||
|
}
|
||||||
|
return count($childFileIds);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文件分享链接
|
* 获取文件分享链接
|
||||||
* @param $userid
|
* @param $userid
|
||||||
@ -604,6 +707,29 @@ class File extends AbstractModel
|
|||||||
Task::deliver($task);
|
Task::deliver($task);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件推送消息
|
||||||
|
* @param $action
|
||||||
|
* @param array|null $data 发送内容
|
||||||
|
* @param int $userid 会员ID
|
||||||
|
*/
|
||||||
|
public static function pushMsgSimple($action, $data, $userid)
|
||||||
|
{
|
||||||
|
if (empty($data) || empty($userid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$msg = [
|
||||||
|
'type' => 'file',
|
||||||
|
'action' => $action,
|
||||||
|
'data' => $data,
|
||||||
|
];
|
||||||
|
$params = [
|
||||||
|
'userid' => $userid,
|
||||||
|
'msg' => $msg
|
||||||
|
];
|
||||||
|
Task::deliver(new PushTask($params));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取推送会员
|
* 获取推送会员
|
||||||
* @param $action
|
* @param $action
|
||||||
@ -647,7 +773,7 @@ class File extends AbstractModel
|
|||||||
/**
|
/**
|
||||||
* code获取文件ID、名称
|
* code获取文件ID、名称
|
||||||
* @param $code
|
* @param $code
|
||||||
* @return File
|
* @return File|null
|
||||||
*/
|
*/
|
||||||
public static function code2IdName($code) {
|
public static function code2IdName($code) {
|
||||||
$arr = explode(",", base64_decode($code));
|
$arr = explode(",", base64_decode($code));
|
||||||
@ -688,9 +814,9 @@ class File extends AbstractModel
|
|||||||
* @param int $permission
|
* @param int $permission
|
||||||
* @return File
|
* @return File
|
||||||
*/
|
*/
|
||||||
public static function permissionFind(int $id, $user, int $limit = 0, int &$permission = -1)
|
public static function permissionFind($id, $user, int $limit = 0, int &$permission = -1)
|
||||||
{
|
{
|
||||||
$file = File::find($id);
|
$file = File::find(intval($id));
|
||||||
if (empty($file)) {
|
if (empty($file)) {
|
||||||
throw new ApiException('文件不存在或已被删除');
|
throw new ApiException('文件不存在或已被删除');
|
||||||
}
|
}
|
||||||
@ -919,26 +1045,39 @@ class File extends AbstractModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件推送消息
|
* 根据文件类型判断是否需要安装应用
|
||||||
* @param $action
|
* @param $type
|
||||||
* @param array|null $data 发送内容
|
* @return void
|
||||||
* @param array $userid 会员ID
|
|
||||||
*/
|
*/
|
||||||
public static function filePushMsg($action, $data = null, $userid = null)
|
public static function isNeedInstallApp($type): void
|
||||||
{
|
{
|
||||||
$userid = User::auth()->userid();
|
// 文件类型与应用的映射配置
|
||||||
if (empty($userid)) {
|
$fileTypeAppMapping = [
|
||||||
return;
|
// Office 应用映射
|
||||||
|
[
|
||||||
|
'types' => ['word', 'excel', 'ppt', 'docx', 'xlsx', 'pptx'],
|
||||||
|
'app_id' => 'office',
|
||||||
|
'app_name' => 'OnlyOffice'
|
||||||
|
],
|
||||||
|
// Drawio 应用映射
|
||||||
|
[
|
||||||
|
'types' => ['drawio'],
|
||||||
|
'app_id' => 'drawio',
|
||||||
|
'app_name' => 'Drawio'
|
||||||
|
],
|
||||||
|
// Minder 应用映射
|
||||||
|
[
|
||||||
|
'types' => ['mind'],
|
||||||
|
'app_id' => 'minder',
|
||||||
|
'app_name' => 'Minder'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 遍历配置检查是否需要安装应用
|
||||||
|
foreach ($fileTypeAppMapping as $config) {
|
||||||
|
if (in_array($type, $config['types'])) {
|
||||||
|
Apps::isInstalledThrow($config['app_id']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$msg = [
|
|
||||||
'type' => 'file',
|
|
||||||
'action' => $action,
|
|
||||||
'data' => $data,
|
|
||||||
];
|
|
||||||
$params = [
|
|
||||||
'userid' => $userid,
|
|
||||||
'msg' => $msg
|
|
||||||
];
|
|
||||||
Task::deliver(new PushTask($params));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
|
use App\Module\Timer;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App\Models\FileContent
|
* App\Models\FileContent
|
||||||
@ -18,10 +19,16 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property \Illuminate\Support\Carbon|null $deleted_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|FileContent newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent onlyTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|FileContent onlyTrashed()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent query()
|
* @method static \Illuminate\Database\Eloquent\Builder|FileContent 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 whereContent($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereCreatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereDeletedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereDeletedAt($value)
|
||||||
@ -70,7 +77,7 @@ class FileContent extends AbstractModel
|
|||||||
'name' => $name,
|
'name' => $name,
|
||||||
'ext' => $fileExt
|
'ext' => $fileExt
|
||||||
]));
|
]));
|
||||||
return Base::fillUrl("online/preview/{$name}?key={$key}");
|
return Base::fillUrl("online/preview/{$name}?key={$key}&version=" . Base::getVersion() . "&__=" . Timer::msecTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,10 +104,10 @@ class FileContent extends AbstractModel
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取格式内容(或下载)
|
* 获取格式内容(或下载)
|
||||||
* @param File $file
|
* @param $file
|
||||||
* @param $content
|
* @param $content
|
||||||
* @param $download
|
* @param $download
|
||||||
* @return array|\Symfony\Component\HttpFoundation\StreamedResponse
|
* @return array|StreamedResponse
|
||||||
*/
|
*/
|
||||||
public static function formatContent($file, $content, $download = false)
|
public static function formatContent($file, $content, $download = false)
|
||||||
{
|
{
|
||||||
@ -112,7 +119,7 @@ class FileContent extends AbstractModel
|
|||||||
} else {
|
} else {
|
||||||
$filePath = public_path($content['url']);
|
$filePath = public_path($content['url']);
|
||||||
}
|
}
|
||||||
return Base::streamDownload($filePath, $name);
|
return Base::DownloadFileResponse($filePath, $name);
|
||||||
}
|
}
|
||||||
if (empty($content)) {
|
if (empty($content)) {
|
||||||
$content = match ($file->type) {
|
$content = match ($file->type) {
|
||||||
@ -122,9 +129,7 @@ class FileContent extends AbstractModel
|
|||||||
],
|
],
|
||||||
default => json_decode('{}'),
|
default => json_decode('{}'),
|
||||||
};
|
};
|
||||||
if ($download) {
|
abort_if($download, 403, "This file is empty.");
|
||||||
abort(403, "This file is empty.");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$path = $content['url'];
|
$path = $content['url'];
|
||||||
if ($file->ext) {
|
if ($file->ext) {
|
||||||
@ -140,13 +145,51 @@ class FileContent extends AbstractModel
|
|||||||
}
|
}
|
||||||
if ($download) {
|
if ($download) {
|
||||||
$filePath = public_path($path);
|
$filePath = public_path($path);
|
||||||
if (isset($filePath)) {
|
abort_if(!isset($filePath),403, "This file not support download.");
|
||||||
return Base::streamDownload($filePath, $name);
|
return Base::DownloadFileResponse($filePath, $name);
|
||||||
} else {
|
|
||||||
abort(403, "This file not support download.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Base::retSuccess('success', [ 'content' => $content ]);
|
return Base::retSuccess('success', [ 'content' => $content ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件访问URL
|
||||||
|
* @param int $fileId 文件ID
|
||||||
|
* @return string|null 返回完整的文件URL,如果文件无内容则返回null
|
||||||
|
*/
|
||||||
|
public static function getFileUrl($fileId)
|
||||||
|
{
|
||||||
|
$content = self::whereFid($fileId)->orderByDesc('id')->first();
|
||||||
|
if ($content) {
|
||||||
|
$contentData = Base::json2array($content->content ?: []);
|
||||||
|
if (!empty($contentData['url'])) {
|
||||||
|
return Base::fillUrl($contentData['url']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件内容
|
||||||
|
* @param $id
|
||||||
|
* @return self|null
|
||||||
|
*/
|
||||||
|
public static function idOrCodeToContent($id)
|
||||||
|
{
|
||||||
|
$builder = null;
|
||||||
|
if (Base::isNumber($id)) {
|
||||||
|
$builder = FileContent::whereFid($id);
|
||||||
|
} elseif ($id) {
|
||||||
|
$fileLink = FileLink::whereCode($id)->first();
|
||||||
|
if ($fileLink) {
|
||||||
|
$builder = FileContent::whereFid($fileLink->file_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** @var self $fileContent */
|
||||||
|
$fileContent = $builder?->orderByDesc('id')->first();
|
||||||
|
if ($fileContent) {
|
||||||
|
$fileContent->content = Base::json2array($fileContent->content ?: []);
|
||||||
|
}
|
||||||
|
return $fileContent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,9 +15,15 @@ use App\Module\Base;
|
|||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property-read \App\Models\File|null $file
|
* @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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|FileLink newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink query()
|
* @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 whereCode($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereCreatedAt($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 whereFileId($value)
|
||||||
|
|||||||
@ -12,9 +12,15 @@ namespace App\Models;
|
|||||||
* @property int|null $permission 权限:0只读,1读写
|
* @property int|null $permission 权限:0只读,1读写
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileUser newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|FileUser newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileUser query()
|
* @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 whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereFileId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereFileId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereId($value)
|
||||||
@ -39,7 +45,7 @@ class FileUser extends AbstractModel
|
|||||||
} else {
|
} else {
|
||||||
FileLink::whereFileId($file_id)->delete();
|
FileLink::whereFileId($file_id)->delete();
|
||||||
}
|
}
|
||||||
FileUser::whereFileId($file_id)->delete();
|
FileUser::whereFileId($file_id)->remove();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -52,7 +58,7 @@ class FileUser extends AbstractModel
|
|||||||
{
|
{
|
||||||
return AbstractModel::transaction(function() use ($userid, $file_id) {
|
return AbstractModel::transaction(function() use ($userid, $file_id) {
|
||||||
FileLink::whereFileId($file_id)->whereUserid($userid)->delete();
|
FileLink::whereFileId($file_id)->whereUserid($userid)->delete();
|
||||||
return self::whereFileId($file_id)->whereUserid($userid)->delete();
|
return self::whereFileId($file_id)->whereUserid($userid)->remove();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
132
app/Models/ManticoreSyncFailure.php
Normal file
132
app/Models/ManticoreSyncFailure.php
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manticore 同步失败记录
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property string $data_type 数据类型: msg/file/task/project/user
|
||||||
|
* @property int $data_id 数据ID
|
||||||
|
* @property string $action 操作类型: sync/delete
|
||||||
|
* @property string|null $error_message 错误信息
|
||||||
|
* @property int $retry_count 重试次数
|
||||||
|
* @property \Carbon\Carbon|null $last_retry_at 最后重试时间
|
||||||
|
* @property \Carbon\Carbon $created_at
|
||||||
|
* @property \Carbon\Carbon $updated_at
|
||||||
|
*/
|
||||||
|
class ManticoreSyncFailure extends AbstractModel
|
||||||
|
{
|
||||||
|
protected $table = 'manticore_sync_failures';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'data_type',
|
||||||
|
'data_id',
|
||||||
|
'action',
|
||||||
|
'error_message',
|
||||||
|
'retry_count',
|
||||||
|
'last_retry_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $dates = [
|
||||||
|
'last_retry_at',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录同步失败
|
||||||
|
*
|
||||||
|
* @param string $dataType 数据类型
|
||||||
|
* @param int $dataId 数据ID
|
||||||
|
* @param string $action 操作类型 sync/delete
|
||||||
|
* @param string $errorMessage 错误信息
|
||||||
|
*/
|
||||||
|
public static function recordFailure(string $dataType, int $dataId, string $action, string $errorMessage = ''): void
|
||||||
|
{
|
||||||
|
self::updateOrCreate(
|
||||||
|
[
|
||||||
|
'data_type' => $dataType,
|
||||||
|
'data_id' => $dataId,
|
||||||
|
'action' => $action,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'error_message' => mb_substr($errorMessage, 0, 500),
|
||||||
|
'retry_count' => \DB::raw('retry_count + 1'),
|
||||||
|
'last_retry_at' => now(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除成功记录
|
||||||
|
*
|
||||||
|
* @param string $dataType 数据类型
|
||||||
|
* @param int $dataId 数据ID
|
||||||
|
* @param string $action 操作类型
|
||||||
|
*/
|
||||||
|
public static function removeSuccess(string $dataType, int $dataId, string $action): void
|
||||||
|
{
|
||||||
|
self::where('data_type', $dataType)
|
||||||
|
->where('data_id', $dataId)
|
||||||
|
->where('action', $action)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待重试的记录
|
||||||
|
* 根据重试次数决定间隔:1次=1分钟,2次=5分钟,3次=15分钟,4次+=30分钟
|
||||||
|
*
|
||||||
|
* @param int $limit 数量限制
|
||||||
|
* @return \Illuminate\Database\Eloquent\Collection
|
||||||
|
*/
|
||||||
|
public static function getPendingRetries(int $limit = 100)
|
||||||
|
{
|
||||||
|
return self::where(function ($query) {
|
||||||
|
$query->whereNull('last_retry_at')
|
||||||
|
->orWhere(function ($q) {
|
||||||
|
// 根据重试次数决定间隔
|
||||||
|
$q->where(function ($sub) {
|
||||||
|
// 重试1次:等待1分钟
|
||||||
|
$sub->where('retry_count', 1)
|
||||||
|
->where('last_retry_at', '<', now()->subMinutes(1));
|
||||||
|
})->orWhere(function ($sub) {
|
||||||
|
// 重试2次:等待5分钟
|
||||||
|
$sub->where('retry_count', 2)
|
||||||
|
->where('last_retry_at', '<', now()->subMinutes(5));
|
||||||
|
})->orWhere(function ($sub) {
|
||||||
|
// 重试3次:等待15分钟
|
||||||
|
$sub->where('retry_count', 3)
|
||||||
|
->where('last_retry_at', '<', now()->subMinutes(15));
|
||||||
|
})->orWhere(function ($sub) {
|
||||||
|
// 重试4次以上:等待30分钟
|
||||||
|
$sub->where('retry_count', '>=', 4)
|
||||||
|
->where('last_retry_at', '<', now()->subMinutes(30));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orderBy('last_retry_at')
|
||||||
|
->limit($limit)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取统计信息
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getStats(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'total' => self::count(),
|
||||||
|
'by_type' => self::selectRaw('data_type, COUNT(*) as count')
|
||||||
|
->groupBy('data_type')
|
||||||
|
->pluck('count', 'data_type')
|
||||||
|
->toArray(),
|
||||||
|
'by_action' => self::selectRaw('action, COUNT(*) as count')
|
||||||
|
->groupBy('action')
|
||||||
|
->pluck('count', 'action')
|
||||||
|
->toArray(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,11 +16,17 @@ use Illuminate\Support\Carbon;
|
|||||||
* @property int|null $userid 创建人
|
* @property int|null $userid 创建人
|
||||||
* @property Carbon|null $created_at
|
* @property Carbon|null $created_at
|
||||||
* @property Carbon|null $updated_at
|
* @property Carbon|null $updated_at
|
||||||
* @property string|null $end_at
|
* @property Carbon|null $end_at
|
||||||
* @property Carbon|null $deleted_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting query()
|
* @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 whereChannel($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereCreatedAt($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 whereDeletedAt($value)
|
||||||
@ -44,12 +50,12 @@ class Meeting extends AbstractModel
|
|||||||
public function getShareLink()
|
public function getShareLink()
|
||||||
{
|
{
|
||||||
$code = base64_encode("{$this->meetingid}" . Base::generatePassword());
|
$code = base64_encode("{$this->meetingid}" . Base::generatePassword());
|
||||||
Cache::put(self::CACHE_KEY.'_'.$code, [
|
Cache::put(self::CACHE_KEY . '_' . $code, [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'meetingid' => $this->meetingid,
|
'meetingid' => $this->meetingid,
|
||||||
'channel' => $this->channel,
|
'channel' => $this->channel,
|
||||||
], Carbon::now()->addHours(self::CACHE_EXPIRED_TIME));
|
], Carbon::now()->addHours(self::CACHE_EXPIRED_TIME));
|
||||||
return Base::fillUrl("meeting/{$this->meetingid}/".$code);
|
return Base::fillUrl("meeting/{$this->meetingid}/" . $code);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,19 +64,19 @@ class Meeting extends AbstractModel
|
|||||||
*/
|
*/
|
||||||
public static function getShareInfo($code)
|
public static function getShareInfo($code)
|
||||||
{
|
{
|
||||||
if(Cache::has(self::CACHE_KEY.'_'.$code)){
|
if (Cache::has(self::CACHE_KEY . '_' . $code)) {
|
||||||
return Cache::get(self::CACHE_KEY.'_'.$code);
|
return Cache::get(self::CACHE_KEY . '_' . $code);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存访客信息
|
* 保存访客信息
|
||||||
* @return mixed
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function setTouristInfo($data)
|
public static function setTouristInfo($data)
|
||||||
{
|
{
|
||||||
Cache::put(Meeting::CACHE_KEY.'_'.$data['uid'], [
|
Cache::put(Meeting::CACHE_KEY . '_' . $data['uid'], [
|
||||||
'uid' => $data['uid'],
|
'uid' => $data['uid'],
|
||||||
'userimg' => $data['userimg'],
|
'userimg' => $data['userimg'],
|
||||||
'nickname' => $data['nickname'],
|
'nickname' => $data['nickname'],
|
||||||
@ -83,8 +89,8 @@ class Meeting extends AbstractModel
|
|||||||
*/
|
*/
|
||||||
public static function getTouristInfo($touristId)
|
public static function getTouristInfo($touristId)
|
||||||
{
|
{
|
||||||
if(Cache::has(Meeting::CACHE_KEY.'_'.$touristId)){
|
if (Cache::has(Meeting::CACHE_KEY . '_' . $touristId)) {
|
||||||
return Cache::get(Meeting::CACHE_KEY.'_'.$touristId);
|
return Cache::get(Meeting::CACHE_KEY . '_' . $touristId);
|
||||||
}
|
}
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,11 +20,15 @@ use Request;
|
|||||||
* @property string|null $desc 描述、备注
|
* @property string|null $desc 描述、备注
|
||||||
* @property int|null $userid 创建人
|
* @property int|null $userid 创建人
|
||||||
* @property int|null $personal 是否个人项目
|
* @property int|null $personal 是否个人项目
|
||||||
|
* @property string|null $archive_method 自动归档方式
|
||||||
|
* @property int|null $archive_days 自动归档天数
|
||||||
|
* @property string|null $ai_auto_analyze AI自动分析
|
||||||
|
* @property string|null $task_template_share 共享模板开关
|
||||||
|
* @property string|null $department_owner_view 部门负责人视角可见开关
|
||||||
* @property string|null $user_simple 成员总数|1,2,3
|
* @property string|null $user_simple 成员总数|1,2,3
|
||||||
* @property int|null $dialog_id 聊天会话ID
|
* @property int|null $dialog_id 聊天会话ID
|
||||||
* @property string|null $archived_at 归档时间
|
* @property \Illuminate\Support\Carbon|null $archived_at 归档时间
|
||||||
* @property int|null $archived_userid 归档会员
|
* @property int|null $archived_userid 归档会员
|
||||||
* @property int|null $is_fixed 是否固定
|
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||||
@ -37,10 +41,19 @@ use Request;
|
|||||||
* @property-read int|null $project_user_count
|
* @property-read int|null $project_user_count
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project allData($userid = null)
|
* @method static \Illuminate\Database\Eloquent\Builder|Project allData($userid = null)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project authData($userid = null, $owner = null)
|
* @method static \Illuminate\Database\Eloquent\Builder|Project 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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|Project newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project onlyTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|Project onlyTrashed()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project query()
|
* @method static \Illuminate\Database\Eloquent\Builder|Project query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|Project searchByKeyword(string $keyword)
|
||||||
|
* @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 whereArchivedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedUserid($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedUserid($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereCreatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Project whereCreatedAt($value)
|
||||||
@ -48,7 +61,6 @@ use Request;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereDesc($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Project whereDesc($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereDialogId($value)
|
* @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 whereId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereIsFixed($value)
|
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereName($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 wherePersonal($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUpdatedAt($value)
|
||||||
@ -68,6 +80,7 @@ class Project extends AbstractModel
|
|||||||
|
|
||||||
protected $appends = [
|
protected $appends = [
|
||||||
'owner_userid',
|
'owner_userid',
|
||||||
|
'deputy_userids',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,6 +96,58 @@ class Project extends AbstractModel
|
|||||||
return $this->appendattrs['owner_userid'];
|
return $this->appendattrs['owner_userid'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目管理员 userid 列表
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getDeputyUseridsAttribute(): array
|
||||||
|
{
|
||||||
|
if (empty($this->id)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return ProjectUser::whereProjectId($this->id)
|
||||||
|
->whereOwner(ProjectUser::OWNER_DEPUTY)
|
||||||
|
->pluck('userid')
|
||||||
|
->map(fn($v) => (int)$v)
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否项目负责人(与 project_users.owner=1 一致)
|
||||||
|
*/
|
||||||
|
public function isPrimaryOwner($userid): bool
|
||||||
|
{
|
||||||
|
if (empty($this->id) || $userid <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ProjectUser::whereProjectId($this->id)
|
||||||
|
->whereUserid($userid)
|
||||||
|
->whereOwner(ProjectUser::OWNER_PRIMARY)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否项目管理员(与 project_users.owner=2 一致)
|
||||||
|
*/
|
||||||
|
public function isDeputyOwner($userid): bool
|
||||||
|
{
|
||||||
|
if (empty($this->id) || $userid <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ProjectUser::whereProjectId($this->id)
|
||||||
|
->whereUserid($userid)
|
||||||
|
->whereOwner(ProjectUser::OWNER_DEPUTY)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否负责人(含项目管理员)
|
||||||
|
*/
|
||||||
|
public function isOwner($userid): bool
|
||||||
|
{
|
||||||
|
return $this->isPrimaryOwner($userid) || $this->isDeputyOwner($userid);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
*/
|
*/
|
||||||
@ -121,6 +186,7 @@ class Project extends AbstractModel
|
|||||||
'projects.*',
|
'projects.*',
|
||||||
'project_users.owner',
|
'project_users.owner',
|
||||||
'project_users.top_at',
|
'project_users.top_at',
|
||||||
|
'project_users.sort',
|
||||||
])
|
])
|
||||||
->leftJoin('project_users', function ($leftJoin) use ($userid) {
|
->leftJoin('project_users', function ($leftJoin) use ($userid) {
|
||||||
$leftJoin
|
$leftJoin
|
||||||
@ -145,6 +211,7 @@ class Project extends AbstractModel
|
|||||||
'projects.*',
|
'projects.*',
|
||||||
'project_users.owner',
|
'project_users.owner',
|
||||||
'project_users.top_at',
|
'project_users.top_at',
|
||||||
|
'project_users.sort',
|
||||||
])
|
])
|
||||||
->join('project_users', 'projects.id', '=', 'project_users.project_id')
|
->join('project_users', 'projects.id', '=', 'project_users.project_id')
|
||||||
->where('project_users.userid', $userid);
|
->where('project_users.userid', $userid);
|
||||||
@ -154,6 +221,18 @@ class Project extends AbstractModel
|
|||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按关键词搜索项目(Scope)
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param string $keyword 搜索关键词
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeSearchByKeyword($query, string $keyword)
|
||||||
|
{
|
||||||
|
return $query->where("projects.name", "like", "%{$keyword}%");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取任务统计数据
|
* 获取任务统计数据
|
||||||
* @param $userid
|
* @param $userid
|
||||||
@ -204,16 +283,40 @@ class Project extends AbstractModel
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AbstractModel::transaction(function() {
|
AbstractModel::transaction(function() {
|
||||||
$userids = $this->relationUserids();
|
// 拉所有项目成员 + 各自 owner 值
|
||||||
|
$userOwnerMap = ProjectUser::whereProjectId($this->id)
|
||||||
|
->pluck('owner', 'userid');
|
||||||
|
$userids = $userOwnerMap->keys()->map(fn($v) => (int)$v)->toArray();
|
||||||
foreach ($userids as $userid) {
|
foreach ($userids as $userid) {
|
||||||
|
$owner = (int)$userOwnerMap[$userid];
|
||||||
|
// 巧合:编码完全一致 owner 0/1/2 → role 0/1/2
|
||||||
|
$role = $owner;
|
||||||
WebSocketDialogUser::updateInsert([
|
WebSocketDialogUser::updateInsert([
|
||||||
'dialog_id' => $this->dialog_id,
|
'dialog_id' => $this->dialog_id,
|
||||||
'userid' => $userid,
|
'userid' => $userid,
|
||||||
], [
|
], [
|
||||||
'important' => 1
|
'important' => 1,
|
||||||
]);
|
'role' => $role,
|
||||||
|
], function () use ($userid, $role) {
|
||||||
|
return [
|
||||||
|
'important' => 1,
|
||||||
|
'role' => $role,
|
||||||
|
'bot' => User::isBot($userid) ? 1 : 0,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
WebSocketDialogUser::whereDialogId($this->dialog_id)
|
||||||
|
->whereNotIn('userid', $userids)
|
||||||
|
->whereImportant(1)
|
||||||
|
->remove();
|
||||||
|
// 同步 dialog.owner_id 到主负责人(owner=1):前端「群主」标签依赖此字段,
|
||||||
|
// 必须随项目主负责人变更(含用户离职转移)一起刷新,否则会显示已离职用户
|
||||||
|
$primaryUserid = $userOwnerMap->search(ProjectUser::OWNER_PRIMARY);
|
||||||
|
if ($primaryUserid !== false && (int)$primaryUserid > 0) {
|
||||||
|
WebSocketDialog::whereId($this->dialog_id)
|
||||||
|
->where('owner_id', '!=', (int)$primaryUserid)
|
||||||
|
->update(['owner_id' => (int)$primaryUserid]);
|
||||||
}
|
}
|
||||||
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,44 +424,65 @@ class Project extends AbstractModel
|
|||||||
/**
|
/**
|
||||||
* 推送消息
|
* 推送消息
|
||||||
* @param string $action
|
* @param string $action
|
||||||
* @param array|self $data 发送内容,默认为[id=>项目ID]
|
* @param array|self $data 推送内容
|
||||||
* @param array $userid 指定会员,默认为项目所有成员
|
* @param array $userid 指定会员,默认为项目所有成员
|
||||||
*/
|
*/
|
||||||
public function pushMsg($action, $data = null, $userid = null)
|
public function pushMsg($action, $data = null, $userid = null)
|
||||||
{
|
{
|
||||||
if ($data === null) {
|
// 处理数据
|
||||||
$data = ['id' => $this->id];
|
if ($data instanceof self) {
|
||||||
} elseif ($data instanceof self) {
|
|
||||||
$data = $data->toArray();
|
$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) {
|
if ($userid === null) {
|
||||||
$array[0] = $this->relationUserids();
|
$recipients[0] = $this->relationUserids();
|
||||||
} elseif (!is_array($userid)) {
|
} elseif (!is_array($userid)) {
|
||||||
$array[0] = [$userid];
|
$recipients[0] = [$userid];
|
||||||
}
|
}
|
||||||
//
|
|
||||||
|
// 移除不需要的字段
|
||||||
|
unset($data['top_at']);
|
||||||
|
|
||||||
|
// 处理所有者权限
|
||||||
if (isset($data['owner'])) {
|
if (isset($data['owner'])) {
|
||||||
$owners = ProjectUser::whereProjectId($data['id'])->whereOwner(1)->pluck('userid')->toArray();
|
$owners = ProjectUser::whereProjectId($data['id'])
|
||||||
$array = [array_intersect($array[0], $owners), array_diff($array[0], $owners)];
|
->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY])
|
||||||
|
->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) {
|
if ($index > 0) {
|
||||||
$data['owner'] = 0;
|
$data['owner'] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$params = [
|
$params = [
|
||||||
'ignoreFd' => Request::header('fd'),
|
'ignoreFd' => Request::header('fd'),
|
||||||
'userid' => array_values($item),
|
'userid' => array_values($userids),
|
||||||
'msg' => [
|
'msg' => [
|
||||||
'type' => 'project',
|
'type' => 'project',
|
||||||
'action' => $action,
|
'action' => $action,
|
||||||
'data' => $data,
|
'data' => $data,
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
$task = new PushTask($params, false);
|
|
||||||
Task::deliver($task);
|
Task::deliver(new PushTask($params, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,29 +510,38 @@ class Project extends AbstractModel
|
|||||||
$hasStart = false;
|
$hasStart = false;
|
||||||
$hasEnd = false;
|
$hasEnd = false;
|
||||||
$upTaskList = [];
|
$upTaskList = [];
|
||||||
|
$projectUserids = $this->relationUserids();
|
||||||
foreach ($flows as $item) {
|
foreach ($flows as $item) {
|
||||||
$id = intval($item['id']);
|
$id = intval($item['id']);
|
||||||
|
$name = trim(str_replace('|', '·', $item['name']));
|
||||||
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
|
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
|
||||||
$userids = Base::arrayRetainInt($item['userids'] ?: [], true);
|
$userids = Base::arrayRetainInt($item['userids'] ?: [], true);
|
||||||
$usertype = trim($item['usertype']);
|
$usertype = trim($item['usertype']);
|
||||||
$userlimit = intval($item['userlimit']);
|
$userlimit = intval($item['userlimit']);
|
||||||
$columnid = intval($item['columnid']);
|
$columnid = intval($item['columnid']);
|
||||||
if ($usertype == 'replace' && empty($userids)) {
|
if ($usertype == 'replace' && empty($userids)) {
|
||||||
throw new ApiException("状态[{$item['name']}]设置错误,设置流转模式时必须填写状态负责人");
|
throw new ApiException("状态[{$name}]设置错误,设置流转模式时必须填写状态负责人");
|
||||||
}
|
}
|
||||||
if ($usertype == 'merge' && empty($userids)) {
|
if ($usertype == 'merge' && empty($userids)) {
|
||||||
throw new ApiException("状态[{$item['name']}]设置错误,设置剔除模式时必须填写状态负责人");
|
throw new ApiException("状态[{$name}]设置错误,设置剔除模式时必须填写状态负责人");
|
||||||
}
|
}
|
||||||
if ($userlimit && empty($userids)) {
|
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([
|
$flow = ProjectFlowItem::updateInsert([
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
'project_id' => $this->id,
|
'project_id' => $this->id,
|
||||||
'flow_id' => $projectFlow->id,
|
'flow_id' => $projectFlow->id,
|
||||||
], [
|
], [
|
||||||
'name' => trim($item['name']),
|
'name' => $name,
|
||||||
'status' => trim($item['status']),
|
'status' => trim($item['status']),
|
||||||
|
'color' => trim($item['color']),
|
||||||
'sort' => intval($item['sort']),
|
'sort' => intval($item['sort']),
|
||||||
'turns' => $turns,
|
'turns' => $turns,
|
||||||
'userids' => $userids,
|
'userids' => $userids,
|
||||||
@ -428,7 +561,7 @@ class Project extends AbstractModel
|
|||||||
$hasEnd = true;
|
$hasEnd = true;
|
||||||
}
|
}
|
||||||
if (!$isInsert) {
|
if (!$isInsert) {
|
||||||
$upTaskList[$flow->id] = $flow->status . "|" . $flow->name;
|
$upTaskList[$flow->id] = $flow->status . "|" . $flow->name . "|" . $flow->color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -472,6 +605,38 @@ class Project extends AbstractModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断用户是否有权限创建项目(依据系统设置「项目创建权限」)
|
||||||
|
* @param int $userid
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function userCanCreate($userid)
|
||||||
|
{
|
||||||
|
// 范围已在 Setting::getSettingAttribute() 归一化(默认 ['all'])
|
||||||
|
$modes = Base::settingFind('system', 'project_add_permission', ['all']);
|
||||||
|
// 「所有人」:放行(与具体用户无关,避免未携带身份时被误判为无权)
|
||||||
|
if (in_array('all', $modes)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$user = User::find(intval($userid));
|
||||||
|
if (empty($user)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 系统管理员始终可创建项目(不受开关限制)
|
||||||
|
if ($user->isAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 部门负责人/部门管理员
|
||||||
|
if (in_array('departmentOwner', $modes) && UserDepartment::getManagedDepartments($user->userid)->isNotEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 指定人员
|
||||||
|
if (in_array('appoint', $modes)) {
|
||||||
|
return in_array($user->userid, Base::settingFind('system', 'project_add_userids', []));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建项目
|
* 创建项目
|
||||||
* @param $params
|
* @param $params
|
||||||
@ -488,6 +653,10 @@ class Project extends AbstractModel
|
|||||||
$desc = trim(Arr::get($params, 'desc', ''));
|
$desc = trim(Arr::get($params, 'desc', ''));
|
||||||
$flow = trim(Arr::get($params, 'flow', 'close'));
|
$flow = trim(Arr::get($params, 'flow', 'close'));
|
||||||
$isPersonal = intval(Arr::get($params, 'personal'));
|
$isPersonal = intval(Arr::get($params, 'personal'));
|
||||||
|
// 个人项目为系统自动创建,不受创建权限限制
|
||||||
|
if (!$isPersonal && !self::userCanCreate($userid)) {
|
||||||
|
return Base::retError('当前仅指定人员可以创建项目');
|
||||||
|
}
|
||||||
if (mb_strlen($name) < 2) {
|
if (mb_strlen($name) < 2) {
|
||||||
return Base::retError('项目名称不可以少于2个字');
|
return Base::retError('项目名称不可以少于2个字');
|
||||||
} elseif (mb_strlen($name) > 32) {
|
} elseif (mb_strlen($name) > 32) {
|
||||||
@ -541,7 +710,7 @@ class Project extends AbstractModel
|
|||||||
$column['project_id'] = $project->id;
|
$column['project_id'] = $project->id;
|
||||||
ProjectColumn::createInstance($column)->save();
|
ProjectColumn::createInstance($column)->save();
|
||||||
}
|
}
|
||||||
$dialog = WebSocketDialog::createGroup($project->name, $project->userid, 'project');
|
$dialog = WebSocketDialog::createGroup($project->name, $project->userid, 'project', $project->userid);
|
||||||
if (empty($dialog)) {
|
if (empty($dialog)) {
|
||||||
throw new ApiException('创建项目聊天室失败');
|
throw new ApiException('创建项目聊天室失败');
|
||||||
}
|
}
|
||||||
@ -549,7 +718,7 @@ class Project extends AbstractModel
|
|||||||
$project->save();
|
$project->save();
|
||||||
//
|
//
|
||||||
if ($flow == 'open') {
|
if ($flow == 'open') {
|
||||||
$project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-14,"name":"已取消","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0}]'));
|
$project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-14,"name":"已取消","status":"end","color":"#999999","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0}]'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//
|
//
|
||||||
@ -563,7 +732,9 @@ class Project extends AbstractModel
|
|||||||
* 获取项目信息(用于判断会员是否存在项目内)
|
* 获取项目信息(用于判断会员是否存在项目内)
|
||||||
* @param int $project_id
|
* @param int $project_id
|
||||||
* @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制
|
* @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制
|
||||||
* @param null|bool $mustOwner true:仅限项目负责人, false:仅限非项目负责人, null:不限制
|
* @param null|bool|string $mustOwner true:负责人或项目管理员都可(共享操作);
|
||||||
|
* 'primary':仅负责人(转让/删除/任命项目管理员等独占操作);
|
||||||
|
* false:仅限非负责人;null:不限制
|
||||||
* @return self
|
* @return self
|
||||||
*/
|
*/
|
||||||
public static function userProject($project_id, $archived = true, $mustOwner = null)
|
public static function userProject($project_id, $archived = true, $mustOwner = null)
|
||||||
@ -581,9 +752,39 @@ class Project extends AbstractModel
|
|||||||
if ($mustOwner === true && !$project->owner) {
|
if ($mustOwner === true && !$project->owner) {
|
||||||
throw new ApiException('仅限项目负责人操作', [ 'project_id' => $project_id ]);
|
throw new ApiException('仅限项目负责人操作', [ 'project_id' => $project_id ]);
|
||||||
}
|
}
|
||||||
|
if ($mustOwner === 'primary' && (int)$project->owner !== 1) {
|
||||||
|
throw new ApiException('仅限项目负责人操作', [ 'project_id' => $project_id ]);
|
||||||
|
}
|
||||||
if ($mustOwner === false && $project->owner) {
|
if ($mustOwner === false && $project->owner) {
|
||||||
throw new ApiException('禁止项目负责人操作', [ 'project_id' => $project_id ]);
|
throw new ApiException('禁止项目负责人操作', [ 'project_id' => $project_id ]);
|
||||||
}
|
}
|
||||||
return $project;
|
return $project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目(含部门负责人只读视角兜底)
|
||||||
|
* @param int $project_id
|
||||||
|
* @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制
|
||||||
|
* @param null|bool|string $mustOwner 仅限 null 时尝试部门只读视角
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function findForDepartmentView($project_id, $archived = true, $mustOwner = null)
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$departmentView = UserDepartment::ownerViewContext($user, true);
|
||||||
|
if (UserDepartment::isDepartmentReadonlyProject($departmentView, intval($project_id)) && $mustOwner === null) {
|
||||||
|
$project = self::allData()->where('projects.id', intval($project_id))->first();
|
||||||
|
if (empty($project)) {
|
||||||
|
throw new ApiException('项目不存在或已被删除', [ 'project_id' => $project_id ], -4001);
|
||||||
|
}
|
||||||
|
if ($archived === true && $project->archived_at != null) {
|
||||||
|
throw new ApiException('项目已归档', [ 'project_id' => $project_id ], -4001);
|
||||||
|
}
|
||||||
|
if ($archived === false && $project->archived_at == null) {
|
||||||
|
throw new ApiException('项目未归档', [ 'project_id' => $project_id ]);
|
||||||
|
}
|
||||||
|
return $project;
|
||||||
|
}
|
||||||
|
return self::userProject($project_id, $archived, $mustOwner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,10 +22,16 @@ use Request;
|
|||||||
* @property-read \App\Models\Project|null $project
|
* @property-read \App\Models\Project|null $project
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTask> $projectTask
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTask> $projectTask
|
||||||
* @property-read int|null $project_task_count
|
* @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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn onlyTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn onlyTrashed()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn query()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn 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 whereColor($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereCreatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereDeletedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereDeletedAt($value)
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Module\Base;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App\Models\ProjectFlow
|
* App\Models\ProjectFlow
|
||||||
*
|
*
|
||||||
@ -14,9 +12,15 @@ use App\Module\Base;
|
|||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectFlowItem> $projectFlowItem
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectFlowItem> $projectFlowItem
|
||||||
* @property-read int|null $project_flow_item_count
|
* @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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow query()
|
* @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 whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereName($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereName($value)
|
||||||
|
|||||||
@ -12,6 +12,7 @@ use App\Module\Base;
|
|||||||
* @property int|null $flow_id 流程ID
|
* @property int|null $flow_id 流程ID
|
||||||
* @property string|null $name 名称
|
* @property string|null $name 名称
|
||||||
* @property string|null $status 状态
|
* @property string|null $status 状态
|
||||||
|
* @property string|null $color 自定义颜色
|
||||||
* @property array $turns 可流转
|
* @property array $turns 可流转
|
||||||
* @property array $userids 状态负责人ID
|
* @property array $userids 状态负责人ID
|
||||||
* @property string|null $usertype 流转模式
|
* @property string|null $usertype 流转模式
|
||||||
@ -21,9 +22,16 @@ use App\Module\Base;
|
|||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property-read \App\Models\ProjectFlow|null $projectFlow
|
* @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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem query()
|
* @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 whereColumnid($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereCreatedAt($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 whereFlowId($value)
|
||||||
|
|||||||
@ -13,9 +13,15 @@ namespace App\Models;
|
|||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property-read bool $already
|
* @property-read bool $already
|
||||||
* @property-read \App\Models\Project|null $project
|
* @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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite query()
|
* @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 whereCode($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereCreatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereId($value)
|
||||||
|
|||||||
@ -11,6 +11,7 @@ use App\Module\Base;
|
|||||||
* @property int|null $project_id 项目ID
|
* @property int|null $project_id 项目ID
|
||||||
* @property int|null $column_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 int|null $userid 会员ID
|
||||||
* @property string|null $detail 详细信息
|
* @property string|null $detail 详细信息
|
||||||
* @property array $record 记录数据
|
* @property array $record 记录数据
|
||||||
@ -18,9 +19,15 @@ use App\Module\Base;
|
|||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property-read \App\Models\ProjectTask|null $projectTask
|
* @property-read \App\Models\ProjectTask|null $projectTask
|
||||||
* @property-read \App\Models\User|null $user
|
* @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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog query()
|
* @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 whereColumnId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereCreatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereDetail($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 whereProjectId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereRecord($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 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 whereUpdatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereUserid($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereUserid($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class ProjectLog extends AbstractModel
|
class ProjectLog extends AbstractModel
|
||||||
{
|
{
|
||||||
|
protected $hidden = [
|
||||||
|
'task_only',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $value
|
* @param $value
|
||||||
|
|||||||
@ -4,19 +4,25 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Exceptions\ApiException;
|
use App\Exceptions\ApiException;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
|
use App\Module\Doo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App\Models\ProjectPermission
|
* App\Models\ProjectPermission
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int|null $project_id 项目ID
|
* @property int|null $project_id 项目ID
|
||||||
* @property string|null $permissions 权限
|
* @property array $permissions 权限
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_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|ProjectPermission newModelQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission query()
|
* @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 whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereId($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 wherePermissions($value)
|
||||||
@ -33,6 +39,7 @@ class ProjectPermission extends AbstractModel
|
|||||||
const TASK_LIST_SORT = 'task_list_sort'; // 列表排序
|
const TASK_LIST_SORT = 'task_list_sort'; // 列表排序
|
||||||
const TASK_ADD = 'task_add'; // 任务添加
|
const TASK_ADD = 'task_add'; // 任务添加
|
||||||
const TASK_UPDATE = 'task_update'; // 任务更新
|
const TASK_UPDATE = 'task_update'; // 任务更新
|
||||||
|
const TASK_TIME = 'task_time'; // 任务时间
|
||||||
const TASK_STATUS = 'task_status'; // 任务状态
|
const TASK_STATUS = 'task_status'; // 任务状态
|
||||||
const TASK_REMOVE = 'task_remove'; // 任务删除
|
const TASK_REMOVE = 'task_remove'; // 任务删除
|
||||||
const TASK_ARCHIVED = 'task_archived'; // 任务归档
|
const TASK_ARCHIVED = 'task_archived'; // 任务归档
|
||||||
@ -64,7 +71,7 @@ class ProjectPermission extends AbstractModel
|
|||||||
/**
|
/**
|
||||||
* 权限
|
* 权限
|
||||||
* @param $value
|
* @param $value
|
||||||
* @return string
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getPermissionsAttribute($value)
|
public function getPermissionsAttribute($value)
|
||||||
{
|
{
|
||||||
@ -105,7 +112,8 @@ class ProjectPermission extends AbstractModel
|
|||||||
self::TASK_LIST_REMOVE => [self::PERMISSIONS['project_leader']],
|
self::TASK_LIST_REMOVE => [self::PERMISSIONS['project_leader']],
|
||||||
self::TASK_LIST_SORT => $projectTaskList,
|
self::TASK_LIST_SORT => $projectTaskList,
|
||||||
self::TASK_ADD => $projectTaskList,
|
self::TASK_ADD => $projectTaskList,
|
||||||
self::TASK_UPDATE => [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader'], self::PERMISSIONS['task_assist']],
|
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_STATUS => $taskStatus = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader']],
|
||||||
self::TASK_REMOVE => $taskStatus,
|
self::TASK_REMOVE => $taskStatus,
|
||||||
self::TASK_ARCHIVED => $taskStatus,
|
self::TASK_ARCHIVED => $taskStatus,
|
||||||
@ -120,8 +128,8 @@ class ProjectPermission extends AbstractModel
|
|||||||
/**
|
/**
|
||||||
* 更新项目权限
|
* 更新项目权限
|
||||||
*
|
*
|
||||||
* @param int $projectId
|
* @param int $projectId
|
||||||
* @param array $permissions
|
* @param $newPermissions
|
||||||
* @return ProjectPermission
|
* @return ProjectPermission
|
||||||
*/
|
*/
|
||||||
public static function updatePermissions($projectId, $newPermissions)
|
public static function updatePermissions($projectId, $newPermissions)
|
||||||
@ -138,9 +146,9 @@ class ProjectPermission extends AbstractModel
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查用户是否有执行特定动作的权限
|
* 检查用户是否有执行特定动作的权限
|
||||||
* @param string $action 动作名称
|
|
||||||
* @param Project $project 项目实例
|
* @param Project $project 项目实例
|
||||||
* @param ProjectTask $task 任务实例
|
* @param string $action 动作名称
|
||||||
|
* @param ProjectTask|null $task 任务实例
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function userTaskPermission(Project $project, $action, ProjectTask $task = null)
|
public static function userTaskPermission(Project $project, $action, ProjectTask $task = null)
|
||||||
@ -148,13 +156,14 @@ class ProjectPermission extends AbstractModel
|
|||||||
$userid = User::userid();
|
$userid = User::userid();
|
||||||
$permissions = self::getPermission($project->id, $action);
|
$permissions = self::getPermission($project->id, $action);
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
// 任务添加,任务更新, 任务状态, 任务删除, 任务完成, 任务归档, 任务移动
|
// 任务添加,任务更新, 任务状态, 任务删除, 任务完成, 任务归档, 任务移动
|
||||||
case self::TASK_LIST_ADD:
|
case self::TASK_LIST_ADD:
|
||||||
case self::TASK_LIST_UPDATE:
|
case self::TASK_LIST_UPDATE:
|
||||||
case self::TASK_LIST_REMOVE:
|
case self::TASK_LIST_REMOVE:
|
||||||
case self::TASK_LIST_SORT:
|
case self::TASK_LIST_SORT:
|
||||||
case self::TASK_ADD:
|
case self::TASK_ADD:
|
||||||
case self::TASK_UPDATE:
|
case self::TASK_UPDATE:
|
||||||
|
case self::TASK_TIME:
|
||||||
case self::TASK_STATUS:
|
case self::TASK_STATUS:
|
||||||
case self::TASK_REMOVE:
|
case self::TASK_REMOVE:
|
||||||
case self::TASK_ARCHIVED:
|
case self::TASK_ARCHIVED:
|
||||||
@ -190,7 +199,7 @@ class ProjectPermission extends AbstractModel
|
|||||||
$desc = [];
|
$desc = [];
|
||||||
rsort($permissions);
|
rsort($permissions);
|
||||||
foreach ($permissions as $permission) {
|
foreach ($permissions as $permission) {
|
||||||
$desc[] = self::PERMISSIONS_DESC[$permission];
|
$desc[] = Doo::translate(self::PERMISSIONS_DESC[$permission]);
|
||||||
}
|
}
|
||||||
$desc = array_reverse($desc);
|
$desc = array_reverse($desc);
|
||||||
throw new ApiException(sprintf("仅限%s操作", implode('、', $desc)));
|
throw new ApiException(sprintf("仅限%s操作", implode('、', $desc)));
|
||||||
|
|||||||
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
154
app/Models/ProjectTaskAiEvent.php
Normal file
154
app/Models/ProjectTaskAiEvent.php
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App\Models\ProjectTaskAiEvent
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $task_id 任务ID
|
||||||
|
* @property string $event_type 事件类型
|
||||||
|
* @property string $status 状态
|
||||||
|
* @property int $retry_count 重试次数
|
||||||
|
* @property array|null $result 执行结果
|
||||||
|
* @property string|null $error 错误信息
|
||||||
|
* @property int $msg_id 消息ID
|
||||||
|
* @property \Illuminate\Support\Carbon|null $executed_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
*/
|
||||||
|
class ProjectTaskAiEvent extends AbstractModel
|
||||||
|
{
|
||||||
|
const EVENT_DESCRIPTION = 'description';
|
||||||
|
const EVENT_SUBTASKS = 'subtasks';
|
||||||
|
const EVENT_ASSIGNEE = 'assignee';
|
||||||
|
const EVENT_SIMILAR = 'similar';
|
||||||
|
|
||||||
|
const STATUS_PENDING = 'pending';
|
||||||
|
const STATUS_PROCESSING = 'processing';
|
||||||
|
const STATUS_COMPLETED = 'completed';
|
||||||
|
const STATUS_FAILED = 'failed';
|
||||||
|
const STATUS_SKIPPED = 'skipped';
|
||||||
|
const STATUS_APPLIED = 'applied';
|
||||||
|
const STATUS_DISMISSED = 'dismissed';
|
||||||
|
|
||||||
|
const MAX_RETRY = 3;
|
||||||
|
|
||||||
|
protected $table = 'project_task_ai_events';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'task_id',
|
||||||
|
'event_type',
|
||||||
|
'status',
|
||||||
|
'retry_count',
|
||||||
|
'result',
|
||||||
|
'error',
|
||||||
|
'msg_id',
|
||||||
|
'executed_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'result' => 'array',
|
||||||
|
'executed_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联任务
|
||||||
|
*/
|
||||||
|
public function task(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProjectTask::class, 'task_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有事件类型
|
||||||
|
*/
|
||||||
|
public static function getEventTypes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::EVENT_DESCRIPTION,
|
||||||
|
self::EVENT_SUBTASKS,
|
||||||
|
self::EVENT_ASSIGNEE,
|
||||||
|
self::EVENT_SIMILAR,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记为处理中
|
||||||
|
*/
|
||||||
|
public function markProcessing(): bool
|
||||||
|
{
|
||||||
|
return $this->update([
|
||||||
|
'status' => self::STATUS_PROCESSING,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记为完成
|
||||||
|
*/
|
||||||
|
public function markCompleted(array $result, int $msgId = 0): bool
|
||||||
|
{
|
||||||
|
return $this->update([
|
||||||
|
'status' => self::STATUS_COMPLETED,
|
||||||
|
'result' => $result,
|
||||||
|
'msg_id' => $msgId,
|
||||||
|
'executed_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记为失败
|
||||||
|
*/
|
||||||
|
public function markFailed(string $error): bool
|
||||||
|
{
|
||||||
|
return $this->update([
|
||||||
|
'status' => self::STATUS_FAILED,
|
||||||
|
'retry_count' => $this->retry_count + 1,
|
||||||
|
'error' => $error,
|
||||||
|
'executed_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记为跳过
|
||||||
|
*/
|
||||||
|
public function markSkipped(string $reason = ''): bool
|
||||||
|
{
|
||||||
|
return $this->update([
|
||||||
|
'status' => self::STATUS_SKIPPED,
|
||||||
|
'error' => $reason,
|
||||||
|
'executed_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可以重试
|
||||||
|
*/
|
||||||
|
public function canRetry(): bool
|
||||||
|
{
|
||||||
|
return $this->status === self::STATUS_FAILED
|
||||||
|
&& $this->retry_count < self::MAX_RETRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记为已采纳
|
||||||
|
*/
|
||||||
|
public function markApplied(): bool
|
||||||
|
{
|
||||||
|
return $this->update([
|
||||||
|
'status' => self::STATUS_APPLIED,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记为已忽略
|
||||||
|
*/
|
||||||
|
public function markDismissed(): bool
|
||||||
|
{
|
||||||
|
return $this->update([
|
||||||
|
'status' => self::STATUS_DISMISSED,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,24 +11,33 @@ use App\Exceptions\ApiException;
|
|||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int|null $project_id 项目ID
|
* @property int|null $project_id 项目ID
|
||||||
* @property int|null $task_id 任务ID
|
* @property int|null $task_id 任务ID
|
||||||
|
* @property int|null $userid 用户ID
|
||||||
|
* @property string|null $desc 内容描述
|
||||||
* @property string|null $content 内容
|
* @property string|null $content 内容
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent query()
|
* @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 whereContent($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereCreatedAt($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 whereId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereProjectId($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 whereTaskId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereUpdatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskContent whereUserid($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class ProjectTaskContent extends AbstractModel
|
class ProjectTaskContent extends AbstractModel
|
||||||
{
|
{
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'created_at',
|
|
||||||
'updated_at',
|
'updated_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -44,8 +53,8 @@ class ProjectTaskContent extends AbstractModel
|
|||||||
$array = $this->toArray();
|
$array = $this->toArray();
|
||||||
$array['content'] = file_get_contents($filePath) ?: '';
|
$array['content'] = file_get_contents($filePath) ?: '';
|
||||||
if ($array['content']) {
|
if ($array['content']) {
|
||||||
$replace = Base::fillUrl('uploads/task');
|
$replace = Base::fillUrl('uploads');
|
||||||
$array['content'] = str_replace('{{RemoteURL}}uploads/task', $replace, $array['content']);
|
$array['content'] = str_replace('{{RemoteURL}}uploads', $replace, $array['content']);
|
||||||
}
|
}
|
||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
@ -65,7 +74,7 @@ class ProjectTaskContent extends AbstractModel
|
|||||||
$oldContent = $content;
|
$oldContent = $content;
|
||||||
$path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/';
|
$path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/';
|
||||||
//
|
//
|
||||||
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp);base64,(.*?)\"/s", $content, $matchs);
|
preg_match_all('/<img[^>]*?src=\\\\?["\']data:image\/(png|jpg|jpeg|webp);base64,(.*?)\\\\?["\']/s', $content, $matchs);
|
||||||
foreach ($matchs[2] as $key => $text) {
|
foreach ($matchs[2] as $key => $text) {
|
||||||
$tmpPath = $path . 'attached/';
|
$tmpPath = $path . 'attached/';
|
||||||
Base::makeDir(public_path($tmpPath));
|
Base::makeDir(public_path($tmpPath));
|
||||||
@ -75,8 +84,14 @@ class ProjectTaskContent extends AbstractModel
|
|||||||
$content = str_replace($matchs[0][$key], '<img src="{{RemoteURL}}' . $tmpPath . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
|
$content = str_replace($matchs[0][$key], '<img src="{{RemoteURL}}' . $tmpPath . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$pattern = '/<img(.*?)src=("|\')https*:\/\/(.*?)\/(uploads\/task\/content\/(.*?))\2/is';
|
preg_match_all('/(<img[^>]*?src=\\\\?["\'])(https?:\/\/[^\/]+\/)(uploads\/[^\s"\'>]+)(\\\\?["\'][^>]*?>)/i', $content, $matches);
|
||||||
$content = preg_replace($pattern, '<img$1src=$2{{RemoteURL}}$4$2', $content);
|
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);
|
$filePath = $path . md5($content);
|
||||||
$publicPath = public_path($filePath);
|
$publicPath = public_path($filePath);
|
||||||
|
|||||||
@ -22,9 +22,15 @@ use Cache;
|
|||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property-read int $height
|
* @property-read int $height
|
||||||
* @property-read int $width
|
* @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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile query()
|
* @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 whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile whereDownload($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile whereDownload($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile whereExt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile whereExt($value)
|
||||||
|
|||||||
@ -14,9 +14,15 @@ namespace App\Models;
|
|||||||
* @property string|null $after_flow_item_name (变化后)工作流状态名称
|
* @property string|null $after_flow_item_name (变化后)工作流状态名称
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange query()
|
* @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 whereAfterFlowItemId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereAfterFlowItemName($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 whereBeforeFlowItemId($value)
|
||||||
|
|||||||
@ -17,10 +17,16 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property \Illuminate\Support\Carbon|null $deleted_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog onlyTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog onlyTrashed()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog query()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog 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 whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereDeletedAt($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 whereId($value)
|
||||||
|
|||||||
223
app/Models/ProjectTaskRelation.php
Normal file
223
app/Models/ProjectTaskRelation.php
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建双向任务关联
|
||||||
|
*
|
||||||
|
* @param int $sourceTaskId 源任务ID
|
||||||
|
* @param int $targetTaskId 目标任务ID
|
||||||
|
* @param int|null $dialogId 来源对话ID
|
||||||
|
* @param int|null $msgId 来源消息ID
|
||||||
|
* @param int|null $userid 操作人
|
||||||
|
* @param bool $push 是否推送更新
|
||||||
|
* @return bool 是否创建成功
|
||||||
|
*/
|
||||||
|
public static function createRelation(
|
||||||
|
int $sourceTaskId,
|
||||||
|
int $targetTaskId,
|
||||||
|
?int $dialogId = null,
|
||||||
|
?int $msgId = null,
|
||||||
|
?int $userid = null,
|
||||||
|
bool $push = true
|
||||||
|
): bool {
|
||||||
|
if ($sourceTaskId === $targetTaskId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceTask = ProjectTask::with('project')->find($sourceTaskId);
|
||||||
|
$targetTask = ProjectTask::with('project')->find($targetTaskId);
|
||||||
|
|
||||||
|
if (!$sourceTask || !$targetTask) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sourceTask->deleted_at || $targetTask->deleted_at) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建正向关联:源任务提及目标任务
|
||||||
|
$mentionRelation = static::updateOrCreate(
|
||||||
|
[
|
||||||
|
'task_id' => $sourceTaskId,
|
||||||
|
'related_task_id' => $targetTaskId,
|
||||||
|
'direction' => self::DIRECTION_MENTION,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dialog_id' => $dialogId,
|
||||||
|
'msg_id' => $msgId,
|
||||||
|
'userid' => $userid,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 创建反向关联:目标任务被源任务提及
|
||||||
|
$reverseRelation = static::updateOrCreate(
|
||||||
|
[
|
||||||
|
'task_id' => $targetTaskId,
|
||||||
|
'related_task_id' => $sourceTaskId,
|
||||||
|
'direction' => self::DIRECTION_MENTIONED_BY,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dialog_id' => $dialogId,
|
||||||
|
'msg_id' => $msgId,
|
||||||
|
'userid' => $userid,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 推送关联更新
|
||||||
|
if ($push) {
|
||||||
|
$needPush = $mentionRelation->wasRecentlyCreated || $mentionRelation->wasChanged()
|
||||||
|
|| $reverseRelation->wasRecentlyCreated || $reverseRelation->wasChanged();
|
||||||
|
|
||||||
|
if ($needPush) {
|
||||||
|
if ($sourceTask->project) {
|
||||||
|
$sourceTask->pushMsg('relation', null, null, false);
|
||||||
|
}
|
||||||
|
if ($targetTask->project) {
|
||||||
|
$targetTask->pushMsg('relation', null, null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除双向任务关联
|
||||||
|
*
|
||||||
|
* @param int $taskId 任务ID
|
||||||
|
* @param int $relatedTaskId 关联任务ID
|
||||||
|
* @return bool 是否删除成功
|
||||||
|
*/
|
||||||
|
public static function deleteRelation(int $taskId, int $relatedTaskId): bool
|
||||||
|
{
|
||||||
|
// 删除正向关联
|
||||||
|
$deleted1 = static::whereTaskId($taskId)
|
||||||
|
->whereRelatedTaskId($relatedTaskId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
// 删除反向关联
|
||||||
|
$deleted2 = static::whereTaskId($relatedTaskId)
|
||||||
|
->whereRelatedTaskId($taskId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
if ($deleted1 || $deleted2) {
|
||||||
|
// 推送关联更新
|
||||||
|
$sourceTask = ProjectTask::with('project')->find($taskId);
|
||||||
|
$targetTask = ProjectTask::with('project')->find($relatedTaskId);
|
||||||
|
if ($sourceTask?->project) {
|
||||||
|
$sourceTask->pushMsg('relation', null, null, false);
|
||||||
|
}
|
||||||
|
if ($targetTask?->project) {
|
||||||
|
$targetTask->pushMsg('relation', null, null, false);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceTaskIds = ProjectTask::whereDialogId($msg->dialog_id)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->pluck('id')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
if (empty($sourceTaskIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($sourceTaskIds as $sourceTaskId) {
|
||||||
|
foreach ($targetIds as $targetId) {
|
||||||
|
self::createRelation(
|
||||||
|
$sourceTaskId,
|
||||||
|
$targetId,
|
||||||
|
$msg->dialog_id,
|
||||||
|
$msg->id,
|
||||||
|
$msg->userid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,9 +12,15 @@ namespace App\Models;
|
|||||||
* @property string|null $color 颜色
|
* @property string|null $color 颜色
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag query()
|
* @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 whereColor($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereCreatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTag whereId($value)
|
||||||
|
|||||||
103
app/Models/ProjectTaskTemplate.php
Normal file
103
app/Models/ProjectTaskTemplate.php
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<?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 int $use_count 累计使用次数
|
||||||
|
* @property \Illuminate\Support\Carbon|null $last_used_at 最近一次使用时间
|
||||||
|
* @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',
|
||||||
|
'use_count',
|
||||||
|
'last_used_at'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $casts = [
|
||||||
|
'last_used_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联项目
|
||||||
|
*
|
||||||
|
* @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');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原子递增使用次数并刷新最近使用时间。
|
||||||
|
*/
|
||||||
|
public function incrementUsage(): void
|
||||||
|
{
|
||||||
|
$this->newQuery()
|
||||||
|
->where('id', $this->id)
|
||||||
|
->update([
|
||||||
|
'use_count' => \DB::raw('use_count + 1'),
|
||||||
|
'last_used_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,9 +14,15 @@ namespace App\Models;
|
|||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property-read \App\Models\ProjectTask|null $projectTask
|
* @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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser query()
|
* @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 whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser whereId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser whereOwner($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser whereOwner($value)
|
||||||
@ -46,7 +52,7 @@ class ProjectTaskUser extends AbstractModel
|
|||||||
*/
|
*/
|
||||||
public static function transfer($originalUserid, $newUserid)
|
public static function transfer($originalUserid, $newUserid)
|
||||||
{
|
{
|
||||||
self::whereUserid($originalUserid)->chunk(100, function ($list) use ($originalUserid, $newUserid) {
|
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
|
||||||
$tastIds = [];
|
$tastIds = [];
|
||||||
/** @var self $item */
|
/** @var self $item */
|
||||||
foreach ($list as $item) {
|
foreach ($list as $item) {
|
||||||
@ -62,7 +68,18 @@ class ProjectTaskUser extends AbstractModel
|
|||||||
$item->save();
|
$item->save();
|
||||||
}
|
}
|
||||||
if ($item->projectTask) {
|
if ($item->projectTask) {
|
||||||
$item->projectTask->addLog("移交{任务}身份", ['userid' => [$originalUserid, ' => ', $newUserid]]);
|
$item->projectTask->addLog("移交{任务}身份", [
|
||||||
|
'change' => [
|
||||||
|
[
|
||||||
|
'type' => 'user',
|
||||||
|
'data' => $originalUserid,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'user',
|
||||||
|
'data' => $newUserid,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
], 0, 1);
|
||||||
if (!in_array($item->task_pid, $tastIds)) {
|
if (!in_array($item->task_pid, $tastIds)) {
|
||||||
$tastIds[] = $item->task_pid;
|
$tastIds[] = $item->task_pid;
|
||||||
$item->projectTask->syncDialogUser();
|
$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');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Module\Base;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App\Models\ProjectUser
|
* App\Models\ProjectUser
|
||||||
*
|
*
|
||||||
@ -9,17 +11,25 @@ namespace App\Models;
|
|||||||
* @property int|null $project_id 项目ID
|
* @property int|null $project_id 项目ID
|
||||||
* @property int|null $userid 成员ID
|
* @property int|null $userid 成员ID
|
||||||
* @property int|null $owner 是否负责人
|
* @property int|null $owner 是否负责人
|
||||||
* @property string|null $top_at 置顶时间
|
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
|
||||||
|
* @property int|null $sort 排序(ASC)
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property-read \App\Models\Project|null $project
|
* @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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser query()
|
* @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 whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereId($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 whereOwner($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereProjectId($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 whereTopAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUpdatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUserid($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUserid($value)
|
||||||
@ -27,6 +37,36 @@ namespace App\Models;
|
|||||||
*/
|
*/
|
||||||
class ProjectUser extends AbstractModel
|
class ProjectUser extends AbstractModel
|
||||||
{
|
{
|
||||||
|
/** @var int 普通成员编码 */
|
||||||
|
const OWNER_MEMBER = 0;
|
||||||
|
/** @var int 项目负责人编码 */
|
||||||
|
const OWNER_PRIMARY = 1;
|
||||||
|
/** @var int 项目管理员编码 */
|
||||||
|
const OWNER_DEPUTY = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否项目负责人(owner=1)
|
||||||
|
*/
|
||||||
|
public function isPrimaryOwner(): bool
|
||||||
|
{
|
||||||
|
return (int)$this->owner === self::OWNER_PRIMARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否项目管理员(owner=2)
|
||||||
|
*/
|
||||||
|
public function isDeputyOwner(): bool
|
||||||
|
{
|
||||||
|
return (int)$this->owner === self::OWNER_DEPUTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否负责人(含项目管理员)
|
||||||
|
*/
|
||||||
|
public function isOwner(): bool
|
||||||
|
{
|
||||||
|
return $this->isPrimaryOwner() || $this->isDeputyOwner();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||||
@ -44,17 +84,26 @@ class ProjectUser extends AbstractModel
|
|||||||
*/
|
*/
|
||||||
public static function transfer($originalUserid, $newUserid)
|
public static function transfer($originalUserid, $newUserid)
|
||||||
{
|
{
|
||||||
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
|
$projectIds = [];
|
||||||
|
// 移交项目身份
|
||||||
|
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid, &$projectIds) {
|
||||||
/** @var self $item */
|
/** @var self $item */
|
||||||
foreach ($list as $item) {
|
foreach ($list as $item) {
|
||||||
$row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first();
|
$row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first();
|
||||||
if ($row) {
|
if ($row) {
|
||||||
// 已存在则删除原数据,判断改变已存在的数据
|
// 已存在:仅当离职用户是项目负责人(owner=1)时把接收人升为项目负责人;
|
||||||
$row->owner = max($row->owner, $item->owner);
|
// 离职用户是项目管理员(owner=2)时不传项目管理员身份给接收人(spec:项目管理员不替补)
|
||||||
|
if ((int)$item->owner === self::OWNER_PRIMARY) {
|
||||||
|
$row->owner = self::OWNER_PRIMARY;
|
||||||
|
}
|
||||||
|
// owner=2/0:保留接收人原有 owner 值不变
|
||||||
$row->save();
|
$row->save();
|
||||||
$item->delete();
|
$item->delete();
|
||||||
} else {
|
} else {
|
||||||
// 不存在则改变原数据
|
// 不存在:转移时如果离职用户是项目管理员,降级为普通成员(不带项目管理员身份过户给接收人)
|
||||||
|
if ((int)$item->owner === self::OWNER_DEPUTY) {
|
||||||
|
$item->owner = self::OWNER_MEMBER;
|
||||||
|
}
|
||||||
$item->userid = $newUserid;
|
$item->userid = $newUserid;
|
||||||
$item->save();
|
$item->save();
|
||||||
}
|
}
|
||||||
@ -64,11 +113,36 @@ class ProjectUser extends AbstractModel
|
|||||||
$item->project->name = "【{$name}】{$item->project->name}";
|
$item->project->name = "【{$name}】{$item->project->name}";
|
||||||
$item->project->save();
|
$item->project->save();
|
||||||
}
|
}
|
||||||
$item->project->addLog("移交项目身份", ['userid' => [$originalUserid, ' => ', $newUserid]]);
|
$item->project->addLog("移交项目身份", [
|
||||||
|
'change' => [
|
||||||
|
[
|
||||||
|
'type' => 'user',
|
||||||
|
'data' => $originalUserid
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'user',
|
||||||
|
'data' => $newUserid
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
$item->project->syncDialogUser();
|
$item->project->syncDialogUser();
|
||||||
|
$projectIds[] = $item->project_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// 移交工作流状态负责人
|
||||||
|
if ($projectIds) {
|
||||||
|
ProjectFlowItem::whereIn('project_id', $projectIds)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
|
||||||
|
/** @var ProjectFlowItem $item */
|
||||||
|
foreach ($list as $item) {
|
||||||
|
if (in_array($originalUserid, $item->userids)) {
|
||||||
|
$userids = array_values(array_diff($item->userids, [$originalUserid]));
|
||||||
|
$item->userids = Base::array2json(array_merge($userids, [$newUserid]));
|
||||||
|
$item->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Exceptions\ApiException;
|
use App\Exceptions\ApiException;
|
||||||
|
use App\Module\Base;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Carbon\Traits\Creator;
|
use Carbon\Traits\Creator;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
use JetBrains\PhpStorm\Pure;
|
use JetBrains\PhpStorm\Pure;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,13 +27,22 @@ use JetBrains\PhpStorm\Pure;
|
|||||||
* @property string $sign 汇报唯一标识
|
* @property string $sign 汇报唯一标识
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ReportReceive> $Receives
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ReportReceive> $Receives
|
||||||
* @property-read int|null $receives_count
|
* @property-read int|null $receives_count
|
||||||
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ReportAnalysis> $aiAnalyses
|
||||||
|
* @property-read int|null $ai_analyses_count
|
||||||
|
* @property-read \App\Models\ReportAnalysis|null $aiAnalysis
|
||||||
* @property-read mixed $receives
|
* @property-read mixed $receives
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $receivesUser
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $receivesUser
|
||||||
* @property-read int|null $receives_user_count
|
* @property-read int|null $receives_user_count
|
||||||
* @property-read \App\Models\User|null $sendUser
|
* @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 newModelQuery()
|
||||||
* @method static Builder|Report newQuery()
|
* @method static Builder|Report newQuery()
|
||||||
* @method static Builder|Report query()
|
* @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 whereContent($value)
|
||||||
* @method static Builder|Report whereCreatedAt($value)
|
* @method static Builder|Report whereCreatedAt($value)
|
||||||
* @method static Builder|Report whereId($value)
|
* @method static Builder|Report whereId($value)
|
||||||
@ -48,6 +59,15 @@ class Report extends AbstractModel
|
|||||||
|
|
||||||
const WEEKLY = "weekly";
|
const WEEKLY = "weekly";
|
||||||
const DAILY = "daily";
|
const DAILY = "daily";
|
||||||
|
public const LIST_FIELDS = [
|
||||||
|
'id',
|
||||||
|
'title',
|
||||||
|
'type',
|
||||||
|
'userid',
|
||||||
|
'sign',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
];
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
"title",
|
"title",
|
||||||
@ -68,7 +88,17 @@ class Report extends AbstractModel
|
|||||||
public function receivesUser(): BelongsToMany
|
public function receivesUser(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(User::class, ReportReceive::class, "rid", "userid")
|
return $this->belongsToMany(User::class, ReportReceive::class, "rid", "userid")
|
||||||
->withPivot("receive_time", "read");
|
->withPivot("receive_at", "read");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function aiAnalyses(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ReportAnalysis::class, 'rid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function aiAnalysis(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(ReportAnalysis::class, 'rid');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sendUser()
|
public function sendUser()
|
||||||
@ -76,15 +106,6 @@ class Report extends AbstractModel
|
|||||||
return $this->hasOne(User::class, "userid", "userid");
|
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
|
public function getContentAttribute($value): string
|
||||||
{
|
{
|
||||||
return htmlspecialchars_decode($value);
|
return htmlspecialchars_decode($value);
|
||||||
@ -98,6 +119,24 @@ class Report extends AbstractModel
|
|||||||
return $this->appendattrs['receives'];
|
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 $id
|
||||||
@ -142,12 +181,12 @@ class Report extends AbstractModel
|
|||||||
// 如果设置了周期偏移量
|
// 如果设置了周期偏移量
|
||||||
empty( $offset ) || $now_dt->subWeeks( abs( $offset ) );
|
empty( $offset ) || $now_dt->subWeeks( abs( $offset ) );
|
||||||
$now_dt->startOfWeek(); // 设置为当周第一天
|
$now_dt->startOfWeek(); // 设置为当周第一天
|
||||||
return $now_dt->year . $now_dt->weekOfYear;
|
return now()->year . $now_dt->weekOfYear;
|
||||||
},
|
},
|
||||||
Report::DAILY => function() use ($now_dt, $offset) {
|
Report::DAILY => function() use ($now_dt, $offset) {
|
||||||
// 如果设置了周期偏移量
|
// 如果设置了周期偏移量
|
||||||
empty( $offset ) || $now_dt->subDays( abs( $offset ) );
|
empty( $offset ) || $now_dt->subDays( abs( $offset ) );
|
||||||
return $now_dt->format("Ymd");
|
return now()->format("Ymd");
|
||||||
},
|
},
|
||||||
default => "",
|
default => "",
|
||||||
};
|
};
|
||||||
|
|||||||
58
app/Models/ReportAnalysis.php
Normal file
58
app/Models/ReportAnalysis.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App\Models\ReportAnalysis
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $rid 报告ID
|
||||||
|
* @property int $userid 生成分析的会员ID
|
||||||
|
* @property string $model 使用的模型名称
|
||||||
|
* @property string $analysis_text AI 分析的原始文本(Markdown)
|
||||||
|
* @property array|null $meta 额外的上下文信息
|
||||||
|
* @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|ReportAnalysis newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereAnalysisText($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereMeta($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereModel($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereRid($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereUpdatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportAnalysis whereUserid($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class ReportAnalysis extends AbstractModel
|
||||||
|
{
|
||||||
|
protected $table = 'report_ai_analyses';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'rid',
|
||||||
|
'userid',
|
||||||
|
'model',
|
||||||
|
'analysis_text',
|
||||||
|
'meta',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'meta' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function report(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Report::class, 'rid');
|
||||||
|
}
|
||||||
|
}
|
||||||
86
app/Models/ReportLink.php
Normal file
86
app/Models/ReportLink.php
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Exceptions\ApiException;
|
||||||
|
use App\Module\Base;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App\Models\ReportLink
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int|null $rid 报告ID
|
||||||
|
* @property int|null $num 累计访问
|
||||||
|
* @property string|null $code 链接码
|
||||||
|
* @property int|null $userid 会员ID
|
||||||
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @property-read \App\Models\Report|null $report
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCode($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereNum($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereRid($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUpdatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUserid($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class ReportLink extends AbstractModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||||
|
*/
|
||||||
|
public function report(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(Report::class, 'id', 'report_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成链接
|
||||||
|
* @param $rid
|
||||||
|
* @param $userid
|
||||||
|
* @param $refresh
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function generateLink($rid, $userid, $refresh = false)
|
||||||
|
{
|
||||||
|
$report = Report::find($rid);
|
||||||
|
if (empty($report)) {
|
||||||
|
throw new ApiException('报告不存在或已被删除');
|
||||||
|
}
|
||||||
|
if ($report->userid != $userid) {
|
||||||
|
if (!ReportReceive::whereRid($rid)->whereUserid($userid)->exists()) {
|
||||||
|
throw new ApiException('您没有权限查看该报告');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$reportLink = ReportLink::whereRid($rid)->whereUserid($userid)->first();
|
||||||
|
if (empty($reportLink)) {
|
||||||
|
$reportLink = ReportLink::createInstance([
|
||||||
|
'rid' => $rid,
|
||||||
|
'userid' => $userid,
|
||||||
|
'code' => base64_encode("{$rid},{$userid}," . Base::generatePassword()),
|
||||||
|
]);
|
||||||
|
$reportLink->save();
|
||||||
|
} else {
|
||||||
|
if ($refresh == 'yes') {
|
||||||
|
$reportLink->code = base64_encode("{$rid},{$userid}," . Base::generatePassword());
|
||||||
|
$reportLink->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'id' => $rid,
|
||||||
|
'url' => Base::fillUrl('single/report/detail/' . $reportLink->code),
|
||||||
|
'code' => $reportLink->code,
|
||||||
|
'num' => $reportLink->num
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,15 +10,21 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $rid
|
* @property int $rid
|
||||||
* @property string|null $receive_time 接收时间
|
* @property \Illuminate\Support\Carbon|null $receive_at 接收时间
|
||||||
* @property int $userid 接收人
|
* @property int $userid 接收人
|
||||||
* @property int $read 是否已读
|
* @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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive query()
|
* @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 whereId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereRead($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 whereRid($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereUserid($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereUserid($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
@ -32,7 +38,7 @@ class ReportReceive extends AbstractModel
|
|||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
"rid",
|
"rid",
|
||||||
"receive_time",
|
"receive_at",
|
||||||
"userid",
|
"userid",
|
||||||
"read",
|
"read",
|
||||||
];
|
];
|
||||||
|
|||||||
@ -2,7 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Exceptions\ApiException;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
|
use App\Module\Doo;
|
||||||
|
use App\Module\Timer;
|
||||||
|
use App\Module\AI;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App\Models\Setting
|
* App\Models\Setting
|
||||||
@ -10,12 +15,18 @@ use App\Module\Base;
|
|||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string|null $name
|
* @property string|null $name
|
||||||
* @property string|null $desc 参数描述、备注
|
* @property string|null $desc 参数描述、备注
|
||||||
* @property string|null $setting
|
* @property array $setting
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Setting newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|Setting newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Setting query()
|
* @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 whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Setting whereDesc($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Setting whereDesc($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Setting whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Setting whereId($value)
|
||||||
@ -26,6 +37,412 @@ use App\Module\Base;
|
|||||||
*/
|
*/
|
||||||
class Setting extends AbstractModel
|
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';
|
||||||
|
$value['task_user_limit'] = min(2000, max(1, intval($value['task_user_limit']) ?: 500));
|
||||||
|
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'];
|
||||||
|
}
|
||||||
|
// 项目创建权限:范围(all/departmentOwner/appoint,默认 all)+ 指定人员
|
||||||
|
$value['project_add_permission'] = array_values(array_intersect(
|
||||||
|
is_array($value['project_add_permission'] ?? null) ? $value['project_add_permission'] : [],
|
||||||
|
['all', 'departmentOwner', 'appoint']
|
||||||
|
)) ?: ['all'];
|
||||||
|
$value['project_add_userids'] = is_array($value['project_add_userids'] ?? null)
|
||||||
|
? array_values(array_unique(array_filter(array_map('intval', $value['project_add_userids']))))
|
||||||
|
: [];
|
||||||
|
break;
|
||||||
|
|
||||||
|
// 文件设置
|
||||||
|
case 'fileSetting':
|
||||||
|
$value['permission_pack_type'] = $value['permission_pack_type'] ?: 'all';
|
||||||
|
$value['permission_pack_userids'] = is_array($value['permission_pack_userids']) ? $value['permission_pack_userids'] : [];
|
||||||
|
break;
|
||||||
|
|
||||||
|
// AI 机器人设置
|
||||||
|
case 'aibotSetting':
|
||||||
|
if (!empty($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 = !empty($value[$key]) ? trim($value[$key]) : '';
|
||||||
|
switch ($fieldName) {
|
||||||
|
case 'models':
|
||||||
|
if ($content) {
|
||||||
|
$content = explode("\n", $content);
|
||||||
|
$content = array_filter($content);
|
||||||
|
}
|
||||||
|
$content = is_array($content) ? implode("\n", $content) : '';
|
||||||
|
break;
|
||||||
|
case 'model':
|
||||||
|
$models = Setting::AIBotModels2Array($array[$key . 's'], true);
|
||||||
|
$content = in_array($content, $models) ? $content : ($models[0] ?? '');
|
||||||
|
break;
|
||||||
|
case 'temperature':
|
||||||
|
if ($content) {
|
||||||
|
$content = floatval(min(1, max(0, floatval($content) ?: 0.7)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$array[$key] = $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$value = $array;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范任务优先级设置(确保字段完整且仅有一个默认项)
|
||||||
|
* @param mixed $list
|
||||||
|
* @return array<int, array{name:string,color:string,days:int,priority:int,is_default:int}>
|
||||||
|
*/
|
||||||
|
public static function normalizeTaskPriorityList($list)
|
||||||
|
{
|
||||||
|
if (!is_array($list)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$normalized = [];
|
||||||
|
$defaultIndex = null;
|
||||||
|
foreach ($list as $item) {
|
||||||
|
if (!is_array($item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$name = trim((string)($item['name'] ?? ''));
|
||||||
|
$color = trim((string)($item['color'] ?? ''));
|
||||||
|
$priority = intval($item['priority'] ?? 0);
|
||||||
|
if ($name === '' || $color === '' || $priority <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$days = intval($item['days'] ?? 0);
|
||||||
|
$isDefault = !empty($item['is_default']) || !empty($item['default']);
|
||||||
|
if ($defaultIndex === null && $isDefault) {
|
||||||
|
$defaultIndex = count($normalized);
|
||||||
|
}
|
||||||
|
$normalized[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'color' => $color,
|
||||||
|
'days' => $days,
|
||||||
|
'priority' => $priority,
|
||||||
|
'is_default' => $isDefault ? 1 : 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (!empty($normalized)) {
|
||||||
|
$defaultIndex = $defaultIndex ?? 0;
|
||||||
|
foreach ($normalized as $i => $row) {
|
||||||
|
$normalized[$i]['is_default'] = $i === $defaultIndex ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array_values($normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认任务优先级(来自 settings.priority)
|
||||||
|
* @param array|null $list
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function getDefaultTaskPriorityItem($list = null)
|
||||||
|
{
|
||||||
|
$list = $list ?? Base::setting('priority');
|
||||||
|
$list = self::normalizeTaskPriorityList($list);
|
||||||
|
if (empty($list)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
foreach ($list as $item) {
|
||||||
|
if (!empty($item['is_default'])) {
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $list[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启 AI 助手
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function AIOpen()
|
||||||
|
{
|
||||||
|
$setting = Base::setting('aibotSetting');
|
||||||
|
if (!is_array($setting) || empty($setting)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach (AI::TEXT_MODEL_PRIORITY as $vendor) {
|
||||||
|
if (self::isAIBotVendorEnabled($setting, $vendor)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断 AI 机器人厂商是否启用
|
||||||
|
* @param array $setting
|
||||||
|
* @param string $vendor
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected static function isAIBotVendorEnabled(array $setting, string $vendor): bool
|
||||||
|
{
|
||||||
|
$key = trim((string)($setting[$vendor . '_key'] ?? ''));
|
||||||
|
return match ($vendor) {
|
||||||
|
'ollama' => $key !== '' || !empty($setting['ollama_base_url']),
|
||||||
|
default => $key !== '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 机器人模型转数组
|
||||||
|
* @param $models
|
||||||
|
* @param bool $retValue
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function AIBotModels2Array($models, $retValue = false)
|
||||||
|
{
|
||||||
|
$list = is_array($models) ? $models : explode("\n", $models);
|
||||||
|
$array = [];
|
||||||
|
foreach ($list as $item) {
|
||||||
|
$arr = Base::newTrim(explode('|', $item . '|'));
|
||||||
|
if ($arr[0]) {
|
||||||
|
$array[] = [
|
||||||
|
'value' => $arr[0],
|
||||||
|
'label' => $arr[1] ?: $arr[0]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($retValue) {
|
||||||
|
return array_column($array, 'value');
|
||||||
|
}
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范自定义微应用配置
|
||||||
|
* @param array $list
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function normalizeCustomMicroApps($list)
|
||||||
|
{
|
||||||
|
if (!is_array($list)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$apps = [];
|
||||||
|
foreach ($list as $item) {
|
||||||
|
$app = self::normalizeCustomMicroAppItem($item);
|
||||||
|
if ($app) {
|
||||||
|
$apps[] = $app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户身份过滤可见的自定义微应用
|
||||||
|
* @param array $apps
|
||||||
|
* @param \App\Models\User|null $user
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function filterCustomMicroAppsForUser(array $apps, $user)
|
||||||
|
{
|
||||||
|
if (empty($apps)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$isAdmin = $user ? $user->isAdmin() : false;
|
||||||
|
$userId = $user ? intval($user->userid) : 0;
|
||||||
|
$filtered = [];
|
||||||
|
foreach ($apps as $app) {
|
||||||
|
$visible = self::normalizeCustomMicroVisible($app['visible_to'] ?? ['admin']);
|
||||||
|
if (!self::isCustomMicroVisibleTo($visible, $isAdmin, $userId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (empty($app['menu_items']) || !is_array($app['menu_items'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$menus = array_values(array_filter($app['menu_items'], function ($menu) use ($isAdmin, $userId) {
|
||||||
|
if (!isset($menu['visible_to'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$visible = self::normalizeCustomMicroVisible($menu['visible_to']);
|
||||||
|
return self::isCustomMicroVisibleTo($visible, $isAdmin, $userId);
|
||||||
|
}));
|
||||||
|
if (empty($menus)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$app['menu_items'] = $menus;
|
||||||
|
$filtered[] = $app;
|
||||||
|
}
|
||||||
|
return $filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将存储结构转换成 appstore 接口同款格式
|
||||||
|
* @param array $apps
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function formatCustomMicroAppsForResponse(array $apps)
|
||||||
|
{
|
||||||
|
return array_values(array_map(function ($app) {
|
||||||
|
unset($app['visible_to']);
|
||||||
|
if (!empty($app['menu_items']) && is_array($app['menu_items'])) {
|
||||||
|
$app['menu_items'] = array_values(array_map(function ($menu) {
|
||||||
|
$menu['keep_alive'] = isset($menu['keep_alive']) ? (bool)$menu['keep_alive'] : true;
|
||||||
|
$menu['disable_scope_css'] = (bool)($menu['disable_scope_css'] ?? false);
|
||||||
|
$menu['auto_dark_theme'] = isset($menu['auto_dark_theme']) ? (bool)$menu['auto_dark_theme'] : true;
|
||||||
|
$menu['transparent'] = (bool)($menu['transparent'] ?? false);
|
||||||
|
if (isset($menu['visible_to'])) {
|
||||||
|
unset($menu['visible_to']);
|
||||||
|
}
|
||||||
|
return $menu;
|
||||||
|
}, $app['menu_items']));
|
||||||
|
}
|
||||||
|
return $app;
|
||||||
|
}, $apps));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范自定义微应用
|
||||||
|
* @param array $item
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
protected static function normalizeCustomMicroAppItem($item)
|
||||||
|
{
|
||||||
|
if (!is_array($item)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$id = trim($item['id'] ?? '');
|
||||||
|
if ($id === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$name = Base::newTrim($item['name'] ?? '');
|
||||||
|
$version = Base::newTrim($item['version'] ?? '') ?: 'custom';
|
||||||
|
$menuItems = [];
|
||||||
|
if (isset($item['menu_items']) && is_array($item['menu_items'])) {
|
||||||
|
$menuItems = $item['menu_items'];
|
||||||
|
} elseif (isset($item['menu']) && is_array($item['menu'])) {
|
||||||
|
$menuItems = [$item['menu']];
|
||||||
|
}
|
||||||
|
if (empty($menuItems)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$normalizedMenus = [];
|
||||||
|
foreach ($menuItems as $menu) {
|
||||||
|
$formattedMenu = self::normalizeCustomMicroMenuItem($menu, $name ?: $id);
|
||||||
|
if ($formattedMenu) {
|
||||||
|
$normalizedMenus[] = $formattedMenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($normalizedMenus)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Base::newTrim([
|
||||||
|
'id' => $id,
|
||||||
|
'name' => $name,
|
||||||
|
'version' => $version,
|
||||||
|
'menu_items' => $normalizedMenus,
|
||||||
|
'visible_to' => self::normalizeCustomMicroVisible($item['visible_to'] ?? 'admin'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范自定义微应用菜单项
|
||||||
|
* @param array $menu
|
||||||
|
* @param string $fallbackLabel
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
protected static function normalizeCustomMicroMenuItem($menu, $fallbackLabel = '')
|
||||||
|
{
|
||||||
|
if (!is_array($menu)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$url = trim($menu['url'] ?? '');
|
||||||
|
if ($url === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$location = trim($menu['location'] ?? 'application');
|
||||||
|
$label = trim($menu['label'] ?? $fallbackLabel);
|
||||||
|
$type = strtolower(trim($menu['type'] ?? 'iframe'));
|
||||||
|
$payload = [
|
||||||
|
'location' => $location,
|
||||||
|
'label' => $label,
|
||||||
|
'icon' => Base::newTrim($menu['icon'] ?? ''),
|
||||||
|
'url' => $url,
|
||||||
|
'type' => $type,
|
||||||
|
'keep_alive' => isset($menu['keep_alive']) ? (bool)$menu['keep_alive'] : true,
|
||||||
|
'disable_scope_css' => (bool)($menu['disable_scope_css'] ?? false),
|
||||||
|
'auto_dark_theme' => isset($menu['auto_dark_theme']) ? (bool)$menu['auto_dark_theme'] : true,
|
||||||
|
'transparent' => (bool)($menu['transparent'] ?? false),
|
||||||
|
];
|
||||||
|
if (!empty($menu['background'])) {
|
||||||
|
$payload['background'] = Base::newTrim($menu['background']);
|
||||||
|
}
|
||||||
|
if (!empty($menu['capsule']) && is_array($menu['capsule'])) {
|
||||||
|
$payload['capsule'] = Base::newTrim($menu['capsule']);
|
||||||
|
}
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范自定义微应用可见范围
|
||||||
|
* @param mixed $value
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected static function normalizeCustomMicroVisible($value)
|
||||||
|
{
|
||||||
|
if (is_array($value)) {
|
||||||
|
$list = array_filter(array_map('trim', $value));
|
||||||
|
} else {
|
||||||
|
$list = array_filter(array_map('trim', explode(',', (string)$value)));
|
||||||
|
}
|
||||||
|
if (empty($list)) {
|
||||||
|
return ['admin'];
|
||||||
|
}
|
||||||
|
if (in_array('all', $list)) {
|
||||||
|
return ['all'];
|
||||||
|
}
|
||||||
|
return array_values($list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断自定义微应用是否可见
|
||||||
|
* @param array $visible
|
||||||
|
* @param bool $isAdmin
|
||||||
|
* @param int $userId
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected static function isCustomMicroVisibleTo(array $visible, bool $isAdmin, int $userId)
|
||||||
|
{
|
||||||
|
if (in_array('all', $visible)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ($isAdmin && in_array('admin', $visible)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ($userId > 0 && in_array((string)$userId, $visible, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证邮箱地址(过滤忽略地址)
|
* 验证邮箱地址(过滤忽略地址)
|
||||||
* @param $array
|
* @param $array
|
||||||
@ -62,4 +479,36 @@ class Setting extends AbstractModel
|
|||||||
}
|
}
|
||||||
return $array;
|
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('已超过' . Base::forumMinuteDay($limitNum) . ',' . $error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,15 +10,21 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string|null $args
|
* @property string|null $args
|
||||||
* @property string|null $error
|
* @property string|null $error
|
||||||
* @property string|null $start_at 开始时间
|
* @property \Illuminate\Support\Carbon|null $start_at 开始时间
|
||||||
* @property string|null $end_at 结束时间
|
* @property \Illuminate\Support\Carbon|null $end_at 结束时间
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property \Illuminate\Support\Carbon|null $deleted_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker onlyTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker onlyTrashed()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker query()
|
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker 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 whereArgs($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereCreatedAt($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 whereDeletedAt($value)
|
||||||
|
|||||||
@ -11,9 +11,15 @@ namespace App\Models;
|
|||||||
* @property string|null $content
|
* @property string|null $content
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Tmp newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|Tmp newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Tmp query()
|
* @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 whereContent($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Tmp whereCreatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Tmp whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Tmp whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Tmp whereId($value)
|
||||||
|
|||||||
@ -14,23 +14,109 @@ use Hedeqiang\UMeng\IOS;
|
|||||||
* @property int|null $userid 会员ID
|
* @property int|null $userid 会员ID
|
||||||
* @property string|null $alias 别名
|
* @property string|null $alias 别名
|
||||||
* @property string|null $platform 平台类型
|
* @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 $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias query()
|
* @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 whereAlias($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereCreatedAt($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 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 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 whereUpdatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUserid($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUserid($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereVersion($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class UmengAlias extends AbstractModel
|
class UmengAlias extends AbstractModel
|
||||||
{
|
{
|
||||||
protected $table = 'umeng_alias';
|
protected $table = 'umeng_alias';
|
||||||
|
|
||||||
|
private static $waitSend = [];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送消息
|
||||||
|
* @param $push
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function sendTask($push = null)
|
||||||
|
{
|
||||||
|
if ($push) {
|
||||||
|
self::$waitSend[] = $push;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self::$waitSend) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first = array_shift(self::$waitSend);
|
||||||
|
if (empty($first)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = null;
|
||||||
|
$responsePayload = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($first['platform']) {
|
||||||
|
case 'ios':
|
||||||
|
$instance = new IOS($first['config']);
|
||||||
|
break;
|
||||||
|
case 'android':
|
||||||
|
$instance = new Android($first['config']);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$responsePayload = $instance->send($first['data']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$responsePayload = [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
];
|
||||||
|
$first['retry'] = intval($first['retry'] ?? 0) + 1;
|
||||||
|
if ($first['retry'] > 3) {
|
||||||
|
info("[PushMsg] fail: " . $e->getMessage());
|
||||||
|
} else {
|
||||||
|
info("[PushMsg] retry ({$first['retry']}): " . $e->getMessage());
|
||||||
|
self::$waitSend[] = $first;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if ($instance !== null) {
|
||||||
|
UmengLog::create([
|
||||||
|
'request' => Base::array2json($first['data']),
|
||||||
|
'response' => Base::array2json($responsePayload),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
self::sendTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送内容处理
|
||||||
|
* @param $string
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function specialCharacters($string)
|
||||||
|
{
|
||||||
|
return str_replace(["\r\n", "\r", "\n"], '', $string);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取推送配置
|
* 获取推送配置
|
||||||
* @return array|false
|
* @return array|false
|
||||||
@ -64,79 +150,98 @@ class UmengAlias extends AbstractModel
|
|||||||
* @param string $alias
|
* @param string $alias
|
||||||
* @param string $platform
|
* @param string $platform
|
||||||
* @param array $array [title, subtitle, body, description, extra, seconds, badge]
|
* @param array $array [title, subtitle, body, description, extra, seconds, badge]
|
||||||
* @return array|false
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function pushMsgToAlias($alias, $platform, $array)
|
private static function pushMsgToAlias($alias, $platform, $array)
|
||||||
{
|
{
|
||||||
$config = self::getPushConfig();
|
$config = self::getPushConfig();
|
||||||
if ($config === false) {
|
if ($config === false) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$title = $array['title'] ?: ''; // 标题
|
$title = self::specialCharacters($array['title'] ?: ''); // 标题
|
||||||
$subtitle = $array['subtitle'] ?: ''; // 副标题(iOS)
|
$subtitle = self::specialCharacters($array['subtitle'] ?: ''); // 副标题(iOS)
|
||||||
$body = $array['body'] ?: ''; // 通知内容
|
$body = self::specialCharacters($array['body'] ?: ''); // 通知内容
|
||||||
$description = $array['description'] ?: 'no description'; // 描述
|
$description = $array['description'] ?: 'no description'; // 描述
|
||||||
$extra = is_array($array['extra']) ? $array['extra'] : []; // 额外参数
|
$extra = is_array($array['extra']) ? $array['extra'] : []; // 额外参数
|
||||||
$seconds = intval($array['seconds']) ?: 86400; // 有效时间(单位:秒)
|
$seconds = intval($array['seconds']) ?: 86400; // 有效时间(单位:秒)
|
||||||
$badge = intval($array['badge']) ?: 0; // 角标数(iOS)
|
$badge = intval($array['badge']) ?: 0; // 角标数
|
||||||
//
|
//
|
||||||
switch ($platform) {
|
switch ($platform) {
|
||||||
case 'ios':
|
case 'ios':
|
||||||
if (!isset($config['iOS'])) {
|
if (!isset($config['iOS'])) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
$ios = new IOS($config);
|
self::sendTask([
|
||||||
return $ios->send([
|
'platform' => $platform,
|
||||||
'description' => $description,
|
'config' => $config,
|
||||||
'payload' => array_merge([
|
'data' => [
|
||||||
'aps' => [
|
'description' => $description,
|
||||||
'alert' => [
|
'payload' => array_merge([
|
||||||
'title' => $title,
|
'aps' => [
|
||||||
'subtitle' => $subtitle,
|
'alert' => [
|
||||||
'body' => $body,
|
'title' => $title,
|
||||||
|
'subtitle' => $subtitle,
|
||||||
|
'body' => $body,
|
||||||
|
],
|
||||||
|
'sound' => 'default',
|
||||||
|
'badge' => $badge,
|
||||||
],
|
],
|
||||||
'sound' => 'default',
|
], $extra),
|
||||||
'badge' => $badge,
|
'type' => 'customizedcast',
|
||||||
|
'alias_type' => 'userid',
|
||||||
|
'alias' => $alias,
|
||||||
|
'policy' => [
|
||||||
|
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
|
||||||
],
|
],
|
||||||
], $extra),
|
]
|
||||||
'type' => 'customizedcast',
|
|
||||||
'alias_type' => 'userid',
|
|
||||||
'alias' => $alias,
|
|
||||||
'policy' => [
|
|
||||||
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'android':
|
case 'android':
|
||||||
if (!isset($config['Android'])) {
|
if (!isset($config['Android'])) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
$android = new Android($config);
|
self::sendTask([
|
||||||
return $android->send([
|
'platform' => $platform,
|
||||||
'description' => $description,
|
'config' => $config,
|
||||||
'payload' => array_merge([
|
'data' => [
|
||||||
'display_type' => 'notification',
|
'description' => $description,
|
||||||
'body' => [
|
'payload' => array_merge([
|
||||||
'ticker' => $title,
|
'display_type' => 'notification',
|
||||||
'text' => $body,
|
'body' => [
|
||||||
'title' => $title,
|
'ticker' => $title,
|
||||||
'after_open' => 'go_app',
|
'text' => $body,
|
||||||
'play_sound' => true,
|
'title' => $title,
|
||||||
|
'after_open' => 'go_app',
|
||||||
|
'play_sound' => true,
|
||||||
|
'set_badge' => min(99, $badge),
|
||||||
|
],
|
||||||
|
], $extra),
|
||||||
|
'type' => 'customizedcast',
|
||||||
|
'alias_type' => 'userid',
|
||||||
|
'alias' => $alias,
|
||||||
|
'mipush' => true,
|
||||||
|
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
|
||||||
|
'policy' => [
|
||||||
|
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
|
||||||
],
|
],
|
||||||
], $extra),
|
'category' => 1,
|
||||||
'type' => 'customizedcast',
|
'channel_properties' => [
|
||||||
'alias_type' => 'userid',
|
'main_activity' => 'com.dootask.task.WelcomeActivity',
|
||||||
'alias' => $alias,
|
'oppo_channel_id' => 'dootask',
|
||||||
'mipush' => true,
|
'vivo_category' => 'IM',
|
||||||
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
|
'huawei_channel_importance' => 'NORMAL',
|
||||||
'policy' => [
|
'huawei_channel_category' => 'IM',
|
||||||
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
|
'channel_fcm' => 0,
|
||||||
|
],
|
||||||
|
'local_properties' => [
|
||||||
|
'importance' => 'IMPORTANCE_DEFAULT',
|
||||||
|
'category' => 'CATEGORY_MESSAGE',
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
break;
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +268,11 @@ class UmengAlias extends AbstractModel
|
|||||||
$lists = $rows->take(5)->groupBy('platform'); // 每个会员最多推送5个别名
|
$lists = $rows->take(5)->groupBy('platform'); // 每个会员最多推送5个别名
|
||||||
foreach ($lists as $platform => $list) {
|
foreach ($lists as $platform => $list) {
|
||||||
$alias = $list->pluck('alias')->implode(',');
|
$alias = $list->pluck('alias')->implode(',');
|
||||||
self::pushMsgToAlias($alias, $platform, $array);
|
try {
|
||||||
|
self::pushMsgToAlias($alias, $platform, $array);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
info("[PushMsg] fail: " . $e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
32
app/Models/UmengLog.php
Normal file
32
app/Models/UmengLog.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App\Models\UmengLog
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property string|null $request 请求参数
|
||||||
|
* @property string|null $response 推送返回
|
||||||
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereRequest($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereResponse($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UmengLog whereUpdatedAt($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class UmengLog extends AbstractModel
|
||||||
|
{
|
||||||
|
protected $guarded = [];
|
||||||
|
}
|
||||||
@ -2,10 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
|
||||||
use App\Exceptions\ApiException;
|
use App\Exceptions\ApiException;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use App\Module\Doo;
|
use App\Module\Doo;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Table\OnlineData;
|
||||||
|
use App\Observers\AbstractObserver;
|
||||||
|
use App\Services\RequestContext;
|
||||||
|
use App\Tasks\ManticoreSyncTask;
|
||||||
use Cache;
|
use Cache;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
@ -21,27 +25,40 @@ use Carbon\Carbon;
|
|||||||
* @property string|null $tel 联系电话
|
* @property string|null $tel 联系电话
|
||||||
* @property string $nickname 昵称
|
* @property string $nickname 昵称
|
||||||
* @property string|null $profession 职位/职称
|
* @property string|null $profession 职位/职称
|
||||||
|
* @property string|null $birthday 生日
|
||||||
|
* @property string|null $address 地址
|
||||||
|
* @property string|null $introduction 个人简介
|
||||||
* @property string $userimg 头像
|
* @property string $userimg 头像
|
||||||
* @property string|null $encrypt
|
* @property string|null $encrypt
|
||||||
* @property string|null $password 登录密码
|
* @property string|null $password 登录密码
|
||||||
* @property int|null $changepass 登录需要修改密码
|
* @property int|null $changepass 登录需要修改密码
|
||||||
* @property int|null $login_num 累计登录次数
|
* @property int|null $login_num 累计登录次数
|
||||||
* @property string|null $last_ip 最后登录IP
|
* @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_ip 最后在线IP(接口)
|
||||||
* @property string|null $line_at 最后在线时间(接口)
|
* @property \Illuminate\Support\Carbon|null $line_at 最后在线时间(接口)
|
||||||
* @property int|null $task_dialog_id 最后打开的任务会话ID
|
* @property int|null $task_dialog_id 最后打开的任务会话ID
|
||||||
* @property string|null $created_ip 注册IP
|
* @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 $email_verity 邮箱是否已验证
|
||||||
* @property int|null $bot 是否机器人
|
* @property int|null $bot 是否机器人
|
||||||
|
* @property string|null $lang 语言首选项
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_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 \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 newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User query()
|
* @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 searchByKeyword(string $keyword)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|User whereAddress($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereAz($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|User whereAz($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|User whereBirthday($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereBot($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 whereChangepass($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
|
||||||
@ -52,6 +69,8 @@ use Carbon\Carbon;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerity($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 whereEncrypt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereIdentity($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|User whereIdentity($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|User whereIntroduction($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 whereLastAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereLastIp($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|User whereLastIp($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereLineAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|User whereLineAt($value)
|
||||||
@ -70,6 +89,8 @@ use Carbon\Carbon;
|
|||||||
*/
|
*/
|
||||||
class User extends AbstractModel
|
class User extends AbstractModel
|
||||||
{
|
{
|
||||||
|
const IMPORT_MAX = 500;
|
||||||
|
|
||||||
protected $primaryKey = 'userid';
|
protected $primaryKey = 'userid';
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
@ -89,7 +110,13 @@ class User extends AbstractModel
|
|||||||
*/
|
*/
|
||||||
public function getNicknameAttribute($value)
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -144,7 +171,7 @@ class User extends AbstractModel
|
|||||||
});
|
});
|
||||||
$array = [];
|
$array = [];
|
||||||
foreach ($list as $item) {
|
foreach ($list as $item) {
|
||||||
$array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? '(M)' : '');
|
$array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? ' (M)' : '');
|
||||||
}
|
}
|
||||||
return implode(', ', $array);
|
return implode(', ', $array);
|
||||||
}
|
}
|
||||||
@ -157,10 +184,9 @@ class User extends AbstractModel
|
|||||||
return UserDepartment::where('owner_userid', $this->userid)->exists();
|
return UserDepartment::where('owner_userid', $this->userid)->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取机器人所有者
|
* 获取机器人所有者
|
||||||
* @return int|mixed
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function getBotOwner()
|
public function getBotOwner()
|
||||||
{
|
{
|
||||||
@ -168,9 +194,9 @@ class User extends AbstractModel
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
$key = "userBotOwner::" . $this->userid;
|
$key = "userBotOwner::" . $this->userid;
|
||||||
return Cache::remember($key, now()->addMonth(), function() {
|
return intval(Cache::remember($key, now()->addMonth(), function() {
|
||||||
return intval(UserBot::whereBotId($this->userid)->value('userid')) ?: $this->userid;
|
return intval(UserBot::whereBotId($this->userid)->value('userid')) ?: $this->userid;
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -179,7 +205,7 @@ class User extends AbstractModel
|
|||||||
*/
|
*/
|
||||||
public function getOnlineStatus()
|
public function getOnlineStatus()
|
||||||
{
|
{
|
||||||
$online = $this->bot || Cache::get("User::online:" . $this->userid) === "on";
|
$online = $this->bot || OnlineData::live($this->userid) > 0;
|
||||||
if ($online) {
|
if ($online) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -206,10 +232,14 @@ class User extends AbstractModel
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回是否禁用帐号(离职)
|
* 返回是否禁用帐号(离职)
|
||||||
|
* @param bool $incAt 是否包含禁用时间
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function isDisable()
|
public function isDisable($incAt = false)
|
||||||
{
|
{
|
||||||
|
if ($incAt) {
|
||||||
|
return in_array('disable', $this->identity) || $this->disable_at;
|
||||||
|
}
|
||||||
return in_array('disable', $this->identity);
|
return in_array('disable', $this->identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +252,31 @@ class User extends AbstractModel
|
|||||||
return in_array('admin', $this->identity);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断是否管理员
|
* 判断是否管理员
|
||||||
*/
|
*/
|
||||||
@ -267,7 +322,7 @@ class User extends AbstractModel
|
|||||||
*/
|
*/
|
||||||
public function deleteUser($reason)
|
public function deleteUser($reason)
|
||||||
{
|
{
|
||||||
return AbstractModel::transaction(function () use ($reason) {
|
$ret = AbstractModel::transaction(function () use ($reason) {
|
||||||
// 删除原因
|
// 删除原因
|
||||||
$userDelete = UserDelete::createInstance([
|
$userDelete = UserDelete::createInstance([
|
||||||
'operator' => User::userid(),
|
'operator' => User::userid(),
|
||||||
@ -288,6 +343,27 @@ class User extends AbstractModel
|
|||||||
//
|
//
|
||||||
return $this->delete();
|
return $this->delete();
|
||||||
});
|
});
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查发送聊天内容前必须设置昵称、电话
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ***************************************************************************************** */
|
/** ***************************************************************************************** */
|
||||||
@ -341,7 +417,295 @@ class User extends AbstractModel
|
|||||||
$dialog?->joinGroup($user->userid, 0);
|
$dialog?->joinGroup($user->userid, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $user->find($user->userid);
|
$createdUser = $user->find($user->userid);
|
||||||
|
if (!$createdUser->bot) {
|
||||||
|
// Manticore 索引同步
|
||||||
|
AbstractObserver::taskDeliver(new ManticoreSyncTask('user_sync', $createdUser->toArray()));
|
||||||
|
// 触发 user_onboard hook
|
||||||
|
Apps::dispatchUserHook($createdUser, 'user_onboard', 'onboard');
|
||||||
|
}
|
||||||
|
return $createdUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员创建员工账号(复用注册逻辑,强制正式身份,可选首登改密 / 部门 / 职位)
|
||||||
|
* @param string $email
|
||||||
|
* @param string $password
|
||||||
|
* @param string $nickname
|
||||||
|
* @param array $options changePass(bool,默认true) / emailVerity(bool,默认false,标记邮箱已认证) / department(int[]) / profession(string)
|
||||||
|
* @return self
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
public static function createByAdmin(string $email, $password, string $nickname, array $options = []): self
|
||||||
|
{
|
||||||
|
$nickname = trim($nickname);
|
||||||
|
if (mb_strlen($nickname) < 2 || mb_strlen($nickname) > 20) {
|
||||||
|
throw new ApiException('昵称需为2-20个字');
|
||||||
|
}
|
||||||
|
$changePass = ($options['changePass'] ?? true) ? 1 : 0;
|
||||||
|
$emailVerity = ($options['emailVerity'] ?? false) ? 1 : 0;
|
||||||
|
$profession = trim((string)($options['profession'] ?? ''));
|
||||||
|
// 校验前置(reg 之前快速失败,且可在无 Swoole 环境单测)
|
||||||
|
self::assertValidProfession($profession);
|
||||||
|
$departmentIds = self::assertValidDepartments($options['department'] ?? []);
|
||||||
|
// 复用 reg:邮箱校验/查重、passwordPolicy、Doo::userCreate、az/pinyin、全员群、索引同步、user_onboard hook
|
||||||
|
$user = self::reg($email, $password, ['nickname' => $nickname]);
|
||||||
|
// 管理员显式创建的账号视为正式员工,去除系统 reg_identity 可能带上的 temp
|
||||||
|
if (in_array('temp', $user->identity)) {
|
||||||
|
$user->identity = Base::arrayImplode(array_diff($user->identity, ['temp']));
|
||||||
|
}
|
||||||
|
$user->changepass = $changePass; // 复用现有首登强制改密机制
|
||||||
|
$user->email_verity = $emailVerity; // 管理员可在创建时直接标记邮箱认证状态
|
||||||
|
if ($profession !== '') {
|
||||||
|
$user->profession = $profession;
|
||||||
|
}
|
||||||
|
if ($departmentIds) {
|
||||||
|
$user->department = Base::arrayImplode($departmentIds);
|
||||||
|
}
|
||||||
|
$user->save();
|
||||||
|
// 设置了部门 → 加入对应部门群(复刻 operation 的 type=department 入群逻辑)
|
||||||
|
if ($departmentIds) {
|
||||||
|
$departments = UserDepartment::whereIn('id', $departmentIds)->get();
|
||||||
|
foreach ($departments as $department) {
|
||||||
|
try {
|
||||||
|
if ($department->dialog_id > 0 && $dialog = WebSocketDialog::find($department->dialog_id)) {
|
||||||
|
$dialog->joinGroup([$user->userid], 0, true);
|
||||||
|
$dialog->pushMsg("groupJoin", null, [$user->userid]);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// 部门入群为尽力投递:单个部门失败不影响账号创建与其他部门
|
||||||
|
\Log::warning('createByAdmin: 部门入群失败', [
|
||||||
|
'userid' => $user->userid,
|
||||||
|
'department_id' => $department->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将上传表格(Excel::toArray 的二维数组)归一化为导入行
|
||||||
|
* @param array $sheet
|
||||||
|
* @return array [{line, email, nickname, password}]
|
||||||
|
*/
|
||||||
|
public static function parseImportRows(array $sheet): array
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
foreach ($sheet as $index => $cells) {
|
||||||
|
if ($index === 0) {
|
||||||
|
continue; // 表头
|
||||||
|
}
|
||||||
|
$email = trim((string)($cells[0] ?? ''));
|
||||||
|
$nickname = trim((string)($cells[1] ?? ''));
|
||||||
|
$password = trim((string)($cells[2] ?? ''));
|
||||||
|
$profession = trim((string)($cells[3] ?? ''));
|
||||||
|
if ($email === '' && $nickname === '' && $password === '') {
|
||||||
|
continue; // 空行(仅职位有值也视为空行跳过)
|
||||||
|
}
|
||||||
|
$rows[] = [
|
||||||
|
'line' => $index + 1, // 电子表格行号(从 1 开始)
|
||||||
|
'email' => $email,
|
||||||
|
'nickname' => $nickname,
|
||||||
|
'password' => $password,
|
||||||
|
'profession' => $profession,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验单条导入行
|
||||||
|
* @param array $row ['email'=>,'nickname'=>,'password'=>,'profession'=>(选填)]
|
||||||
|
* @return string|null 错误文案;null 表示通过
|
||||||
|
*/
|
||||||
|
public static function validateImportRow(array $row): ?string
|
||||||
|
{
|
||||||
|
$email = trim((string)($row['email'] ?? ''));
|
||||||
|
$nickname = trim((string)($row['nickname'] ?? ''));
|
||||||
|
$password = trim((string)($row['password'] ?? ''));
|
||||||
|
if ($email === '' || $nickname === '' || $password === '') {
|
||||||
|
return '邮箱、昵称、初始密码均为必填';
|
||||||
|
}
|
||||||
|
if (!Base::isEmail($email)) {
|
||||||
|
return '邮箱格式不正确';
|
||||||
|
}
|
||||||
|
if (mb_strlen($nickname) < 2 || mb_strlen($nickname) > 20) {
|
||||||
|
return '昵称需为2-20个字';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
self::passwordPolicy($password);
|
||||||
|
} catch (ApiException $e) {
|
||||||
|
return $e->getMessage();
|
||||||
|
}
|
||||||
|
// 职位/职称选填,填写则校验 2-20 字
|
||||||
|
try {
|
||||||
|
self::assertValidProfession((string)($row['profession'] ?? ''));
|
||||||
|
} catch (ApiException $e) {
|
||||||
|
return $e->getMessage();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验职位/职称:非空时必须 2-20 字(复用 operation 的现有文案)
|
||||||
|
* @param string $profession
|
||||||
|
* @return void
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
public static function assertValidProfession(string $profession): void
|
||||||
|
{
|
||||||
|
$profession = trim($profession);
|
||||||
|
if ($profession === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mb_strlen($profession) < 2) {
|
||||||
|
throw new ApiException('职位/职称不可以少于2个字');
|
||||||
|
}
|
||||||
|
if (mb_strlen($profession) > 20) {
|
||||||
|
throw new ApiException('职位/职称最多只能设置20个字');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规整并校验部门 ID 列表:转正整数去重、最多 10 个、且每个必须存在
|
||||||
|
* @param mixed $ids
|
||||||
|
* @return int[]
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
public static function assertValidDepartments($ids): array
|
||||||
|
{
|
||||||
|
if (!is_array($ids)) {
|
||||||
|
$ids = [];
|
||||||
|
}
|
||||||
|
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0)));
|
||||||
|
if (count($ids) > 10) {
|
||||||
|
throw new ApiException('最多只可加入10个部门');
|
||||||
|
}
|
||||||
|
if ($ids) {
|
||||||
|
$existing = UserDepartment::whereIn('id', $ids)->pluck('id')->map(fn($v) => (int)$v)->all();
|
||||||
|
if (count($existing) < count($ids)) {
|
||||||
|
throw new ApiException('修改部门不存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量导入用户(部门/职位逐行:department 来自前端逐行设置,profession 来自 Excel 行)
|
||||||
|
* @param array $rows 每行含 email/nickname/password/profession,可选 department(int[])
|
||||||
|
* @param bool $changePass 是否要求首登改密(对本批所有账号生效)
|
||||||
|
* @return array ['total'=>int, 'success'=>int, 'failed'=>[['line','email','reason']]]
|
||||||
|
* @throws ApiException 行数超限
|
||||||
|
*/
|
||||||
|
public static function importUsers(array $rows, bool $changePass = true): array
|
||||||
|
{
|
||||||
|
if (count($rows) > self::IMPORT_MAX) {
|
||||||
|
throw new ApiException('单次最多导入' . self::IMPORT_MAX . '条');
|
||||||
|
}
|
||||||
|
$success = 0;
|
||||||
|
$failed = [];
|
||||||
|
$seen = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$error = self::validateImportRow($row);
|
||||||
|
if ($error === null) {
|
||||||
|
$emailLower = strtolower(trim((string)$row['email']));
|
||||||
|
if (isset($seen[$emailLower])) {
|
||||||
|
$error = '文件内邮箱重复';
|
||||||
|
} else {
|
||||||
|
$seen[$emailLower] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($error === null) {
|
||||||
|
try {
|
||||||
|
self::createByAdmin($row['email'], $row['password'], $row['nickname'], [
|
||||||
|
'changePass' => $changePass,
|
||||||
|
'emailVerity' => !empty($row['email_verity']),
|
||||||
|
'department' => $row['department'] ?? [],
|
||||||
|
'profession' => $row['profession'] ?? '',
|
||||||
|
]);
|
||||||
|
$success++;
|
||||||
|
continue;
|
||||||
|
} catch (ApiException $e) {
|
||||||
|
$error = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$failed[] = [
|
||||||
|
'line' => $row['line'] ?? 0,
|
||||||
|
'email' => $row['email'] ?? '',
|
||||||
|
'reason' => $error,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'total' => count($rows),
|
||||||
|
'success' => $success,
|
||||||
|
'failed' => $failed,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量导入预览(只解析+校验,不创建任何账号)
|
||||||
|
* 逐行判定 ok/error:必填/邮箱格式/昵称长度/密码策略、文件内邮箱重复、系统中邮箱已存在
|
||||||
|
* @param array $rows parseImportRows 的输出
|
||||||
|
* @return array ['total'=>int,'valid'=>int,'invalid'=>int,'rows'=>[['line','email','nickname','password','status','reason']]]
|
||||||
|
*/
|
||||||
|
public static function importPreview(array $rows): array
|
||||||
|
{
|
||||||
|
if (count($rows) > self::IMPORT_MAX) {
|
||||||
|
throw new ApiException('单次最多导入' . self::IMPORT_MAX . '条');
|
||||||
|
}
|
||||||
|
// 预查系统中已存在的邮箱(小写比较)
|
||||||
|
$emails = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$e = strtolower(trim((string)($row['email'] ?? '')));
|
||||||
|
if ($e !== '') {
|
||||||
|
$emails[$e] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$existing = [];
|
||||||
|
if ($emails) {
|
||||||
|
foreach (self::whereIn('email', array_keys($emails))->pluck('email') as $em) {
|
||||||
|
$existing[strtolower($em)] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$seen = [];
|
||||||
|
$valid = 0;
|
||||||
|
$list = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$reason = self::validateImportRow($row);
|
||||||
|
$emailLower = strtolower(trim((string)($row['email'] ?? '')));
|
||||||
|
if ($reason === null) {
|
||||||
|
if (isset($seen[$emailLower])) {
|
||||||
|
$reason = '文件内邮箱重复';
|
||||||
|
} else {
|
||||||
|
$seen[$emailLower] = true;
|
||||||
|
if (isset($existing[$emailLower])) {
|
||||||
|
$reason = '邮箱地址已存在';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$ok = $reason === null;
|
||||||
|
if ($ok) {
|
||||||
|
$valid++;
|
||||||
|
}
|
||||||
|
$list[] = [
|
||||||
|
'line' => $row['line'] ?? 0,
|
||||||
|
'email' => $row['email'] ?? '',
|
||||||
|
'nickname' => $row['nickname'] ?? '',
|
||||||
|
'password' => $row['password'] ?? '',
|
||||||
|
'profession' => $row['profession'] ?? '',
|
||||||
|
'email_verity' => 1, // 默认标记为已认证,前端可在预览中按行调整
|
||||||
|
'status' => $ok ? 'ok' : 'error',
|
||||||
|
'reason' => $reason ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'total' => count($rows),
|
||||||
|
'valid' => $valid,
|
||||||
|
'invalid' => count($rows) - $valid,
|
||||||
|
'rows' => $list,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -379,13 +743,15 @@ class User extends AbstractModel
|
|||||||
{
|
{
|
||||||
$user = self::authInfo();
|
$user = self::authInfo();
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
if (Base::token()) {
|
$token = Base::token();
|
||||||
|
if ($token) {
|
||||||
|
UserDevice::forget($token);
|
||||||
throw new ApiException('身份已失效,请重新登录', [], -1);
|
throw new ApiException('身份已失效,请重新登录', [], -1);
|
||||||
} else {
|
} else {
|
||||||
throw new ApiException('请登录后继续...', [], -1);
|
throw new ApiException('请登录后继续...', [], -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (in_array('disable', $user->identity)) {
|
if ($user->isDisable()) {
|
||||||
throw new ApiException('帐号已停用...', [], -1);
|
throw new ApiException('帐号已停用...', [], -1);
|
||||||
}
|
}
|
||||||
if ($identity) {
|
if ($identity) {
|
||||||
@ -400,27 +766,47 @@ class User extends AbstractModel
|
|||||||
*/
|
*/
|
||||||
private static function authInfo()
|
private static function authInfo()
|
||||||
{
|
{
|
||||||
global $_A;
|
if (RequestContext::has('auth')) {
|
||||||
if (isset($_A["__static_auth"])) {
|
// 缓存
|
||||||
return $_A["__static_auth"];
|
return RequestContext::get('auth');
|
||||||
}
|
}
|
||||||
if (Doo::userId() > 0
|
if (Doo::userId() <= 0) {
|
||||||
&& !Doo::userExpired()
|
// 没有登录
|
||||||
&& $user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first()) {
|
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();
|
|
||||||
}
|
|
||||||
if ($upArray) {
|
|
||||||
$user->updateInstance($upArray);
|
|
||||||
$user->save();
|
|
||||||
}
|
|
||||||
return $_A["__static_auth"] = $user;
|
|
||||||
}
|
}
|
||||||
return $_A["__static_auth"] = false;
|
if (Doo::userExpired()) {
|
||||||
|
// 登录过期
|
||||||
|
return RequestContext::save('auth', false);
|
||||||
|
}
|
||||||
|
if (!UserDevice::check()) {
|
||||||
|
// token 不存在
|
||||||
|
return RequestContext::save('auth', false);
|
||||||
|
}
|
||||||
|
$user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first();
|
||||||
|
if (!$user) {
|
||||||
|
// 登录信息不匹配
|
||||||
|
return RequestContext::save('auth', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新登录信息
|
||||||
|
$upArray = [];
|
||||||
|
if (Base::getIp() && $user->line_ip != Base::getIp()) {
|
||||||
|
$upArray['line_ip'] = Base::getIp();
|
||||||
|
}
|
||||||
|
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
|
||||||
|
$upArray['line_at'] = Carbon::now();
|
||||||
|
}
|
||||||
|
$headerLanguage = RequestContext::get('header_language');
|
||||||
|
if (empty($user->lang) || $headerLanguage) {
|
||||||
|
if (Doo::checkLanguage($headerLanguage) && $user->lang != $headerLanguage) {
|
||||||
|
$upArray['lang'] = $headerLanguage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($upArray) {
|
||||||
|
$user->updateInstance($upArray);
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
return RequestContext::save('auth', $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -444,32 +830,48 @@ class User extends AbstractModel
|
|||||||
} else {
|
} else {
|
||||||
$token = Doo::userToken();
|
$token = Doo::userToken();
|
||||||
}
|
}
|
||||||
|
UserDevice::record($token);
|
||||||
unset($userinfo->encrypt);
|
unset($userinfo->encrypt);
|
||||||
unset($userinfo->password);
|
unset($userinfo->password);
|
||||||
return $userinfo->token = $token;
|
return $userinfo->token = $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成无设备的 token(主要用于接口调用,此 token 不检查设备是否存在)
|
||||||
|
* @param self $userinfo
|
||||||
|
* @param $ttl
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function generateTokenNoDevice($userinfo, $ttl)
|
||||||
|
{
|
||||||
|
$key = 'user_token_no_device_' . $userinfo->userid;
|
||||||
|
return Cache::remember($key, $ttl, function () use ($userinfo, $ttl) {
|
||||||
|
$token = Doo::tokenEncode($userinfo->userid, $userinfo->email, $userinfo->encrypt);
|
||||||
|
Cache::put(UserDevice::ck(md5($token)), $userinfo->userid, $ttl);
|
||||||
|
return $token;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* userid 获取 基础信息
|
* userid 获取 基础信息
|
||||||
* @param int $userid 会员ID
|
* @param int $userid 会员ID
|
||||||
* @return self
|
* @return self
|
||||||
*/
|
*/
|
||||||
public static function userid2basic($userid)
|
public static function userid2basic($userid, $addField = [])
|
||||||
{
|
{
|
||||||
global $_A;
|
|
||||||
if (empty($userid)) {
|
if (empty($userid)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$userid = intval($userid);
|
$userid = intval($userid);
|
||||||
if (isset($_A["__static_userid2basic_" . $userid])) {
|
if (RequestContext::has("userid2basic_" . $userid)) {
|
||||||
return $_A["__static_userid2basic_" . $userid];
|
return RequestContext::get("userid2basic_" . $userid);
|
||||||
}
|
}
|
||||||
$userInfo = self::whereUserid($userid)->select(User::$basicField)->first();
|
$userInfo = self::whereUserid($userid)->select(array_merge(User::$basicField, $addField))->first();
|
||||||
if ($userInfo) {
|
if ($userInfo) {
|
||||||
$userInfo->online = $userInfo->getOnlineStatus();
|
$userInfo->online = $userInfo->getOnlineStatus();
|
||||||
$userInfo->department_name = $userInfo->getDepartmentName();
|
$userInfo->department_name = $userInfo->getDepartmentName();
|
||||||
}
|
}
|
||||||
return $_A["__static_userid2basic_" . $userid] = ($userInfo ?: []);
|
return RequestContext::save("userid2basic_" . $userid, $userInfo ?: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -507,6 +909,16 @@ 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 $userid
|
||||||
@ -539,6 +951,16 @@ class User extends AbstractModel
|
|||||||
return url("images/avatar/default_openai.png");
|
return url("images/avatar/default_openai.png");
|
||||||
case 'ai-claude@bot.system':
|
case 'ai-claude@bot.system':
|
||||||
return url("images/avatar/default_claude.png");
|
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':
|
case 'bot-manager@bot.system':
|
||||||
return url("images/avatar/default_bot.png");
|
return url("images/avatar/default_bot.png");
|
||||||
case 'meeting-alert@bot.system':
|
case 'meeting-alert@bot.system':
|
||||||
@ -611,16 +1033,67 @@ class User extends AbstractModel
|
|||||||
])->save();
|
])->save();
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$update['nickname'] = UserBot::systemBotName($email);
|
if (empty($update['nickname'])) {
|
||||||
|
$update['nickname'] = UserBot::systemBotName($email);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($update) {
|
if ($update) {
|
||||||
$botUser->updateInstance($update);
|
if (isset($update['nickname']) && $botUser->nickname != $update['nickname']) {
|
||||||
if (isset($update['nickname'])) {
|
|
||||||
$botUser->az = Base::getFirstCharter($botUser->nickname);
|
$botUser->az = Base::getFirstCharter($botUser->nickname);
|
||||||
$botUser->pinyin = Base::cn2pinyin($botUser->nickname);
|
$botUser->pinyin = Base::cn2pinyin($botUser->nickname);
|
||||||
}
|
}
|
||||||
|
$botUser->updateInstance($update);
|
||||||
$botUser->save();
|
$botUser->save();
|
||||||
}
|
}
|
||||||
return $botUser;
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按关键词搜索用户(Scope)
|
||||||
|
* 支持:邮箱(含@)、用户ID(纯数字)、昵称/拼音/职业
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param string $keyword 搜索关键词
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeSearchByKeyword($query, string $keyword)
|
||||||
|
{
|
||||||
|
if (str_contains($keyword, "@")) {
|
||||||
|
// 包含 @ 按邮箱搜索
|
||||||
|
return $query->where("email", "like", "%{$keyword}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($keyword)) {
|
||||||
|
// 纯数字:匹配用户ID 或 昵称/拼音/职业
|
||||||
|
return $query->where(function ($q) use ($keyword) {
|
||||||
|
$q->where("userid", intval($keyword))
|
||||||
|
->orWhere("nickname", "like", "%{$keyword}%")
|
||||||
|
->orWhere("pinyin", "like", "%{$keyword}%")
|
||||||
|
->orWhere("profession", "like", "%{$keyword}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通文本:搜索昵称/拼音/职业
|
||||||
|
return $query->where(function ($q) use ($keyword) {
|
||||||
|
$q->where("nickname", "like", "%{$keyword}%")
|
||||||
|
->orWhere("pinyin", "like", "%{$keyword}%")
|
||||||
|
->orWhere("profession", "like", "%{$keyword}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
app/Models/UserAppSort.php
Normal file
102
app/Models/UserAppSort.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App\Models\UserAppSort
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $userid 用户ID
|
||||||
|
* @property array|null $sorts 排序配置
|
||||||
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|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|UserAppSort newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereSorts($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereUpdatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAppSort whereUserid($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class UserAppSort extends AbstractModel
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'userid',
|
||||||
|
'sorts',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'sorts' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户排序配置
|
||||||
|
* @param int $userid
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getSorts(int $userid): array
|
||||||
|
{
|
||||||
|
$record = static::whereUserid($userid)->first();
|
||||||
|
if (!$record) {
|
||||||
|
return self::normalizeSorts([]);
|
||||||
|
}
|
||||||
|
return self::normalizeSorts($record->sorts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存排序配置
|
||||||
|
* @param int $userid
|
||||||
|
* @param array $sorts
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public static function saveSorts(int $userid, array $sorts): self
|
||||||
|
{
|
||||||
|
return static::updateOrCreate(
|
||||||
|
['userid' => $userid],
|
||||||
|
['sorts' => self::normalizeSorts($sorts)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化排序数据
|
||||||
|
* @param mixed $sorts
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function normalizeSorts($sorts): array
|
||||||
|
{
|
||||||
|
$result = [
|
||||||
|
'base' => [],
|
||||||
|
'admin' => [],
|
||||||
|
];
|
||||||
|
if (!is_array($sorts)) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
foreach (['base', 'admin'] as $group) {
|
||||||
|
$list = $sorts[$group] ?? [];
|
||||||
|
if (!is_array($list)) {
|
||||||
|
$list = [];
|
||||||
|
}
|
||||||
|
$normalized = [];
|
||||||
|
foreach ($list as $value) {
|
||||||
|
if (!is_string($value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$value = trim($value);
|
||||||
|
if ($value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$normalized[] = $value;
|
||||||
|
}
|
||||||
|
$result[$group] = array_values(array_unique($normalized));
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user