Compare commits
721 Commits
b6dc0b8608
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f89ed66bcc | ||
|
|
a893f3cd61 | ||
|
|
c2be15e3f5 | ||
|
|
8445b500ae | ||
|
|
ee67d1ae8f | ||
|
|
a20e92d7bf | ||
| 69d1d91f5e | |||
|
|
570fcb0b93 | ||
|
|
7fda3da9ed | ||
|
|
e7687c8909 | ||
|
|
8e12076225 | ||
|
|
5b48727fb2 | ||
|
|
bb6c907cda | ||
|
|
bdd33581f1 | ||
|
|
ef358cc6b3 | ||
|
|
e76c6d4451 | ||
| 31ecfa6a2f | |||
| 127a5b71c6 | |||
| 1872908dae | |||
| efdb727e48 | |||
| 4af64b58d6 | |||
| 424cf37260 | |||
| 47fd91b948 | |||
| 89b37907e7 | |||
| 6b36f0ee52 | |||
| 5f75603532 | |||
| a2d011fb01 | |||
| 1a6ddce3f0 | |||
| a82ff0d39f | |||
| 1c9c9cfa06 | |||
| 8905ce179c | |||
| 2114f5d0f6 | |||
| 4010910846 | |||
| bd9b0f9384 | |||
|
|
b9de9ed7f4 | ||
|
|
fd940bbd66 | ||
|
|
473e305bb7 | ||
|
|
c1fbe3bb4b | ||
|
|
41083d4519 | ||
| 595642677f | |||
| d74af8a07f | |||
| 6e68591991 | |||
|
|
5543b5bcde | ||
| c3a23bf6fa | |||
| b7528dc077 | |||
| 3883cd76b4 | |||
| 49320e35ed | |||
| a95956a73e | |||
| b983219502 | |||
| cbf1600497 | |||
|
|
cce7ffad00 | ||
|
|
841c4a6a5a | ||
|
|
e736ad9a96 | ||
| d582014f24 | |||
| 1cd75c8384 | |||
| c80d3b05b8 | |||
| bdba500fa2 | |||
|
|
7eb31a1d4a | ||
|
|
4e024460b4 | ||
|
|
81b39804e1 | ||
|
|
b28af91409 | ||
|
|
b035649467 | ||
|
|
a005158d97 | ||
| 81cc5bec82 | |||
| 54df8a4bc8 | |||
| e3f0160876 | |||
| 3b561b0fc7 | |||
|
|
488eb5f64c | ||
|
|
26c2b51d72 | ||
|
|
37fc0697b3 | ||
|
|
f97a5b0130 | ||
|
|
7408206f77 | ||
|
|
11da6257d6 | ||
|
|
1a762eb50a | ||
|
|
cdb871a202 | ||
|
|
09741e3b48 | ||
|
|
0ba97c39a2 | ||
|
|
2f0157e03e | ||
|
|
b4c27a26d7 | ||
|
|
aabbb3cc17 | ||
|
|
8def7a1cce | ||
|
|
6cf6e95751 | ||
|
|
90a8871d36 | ||
|
|
34583fce59 | ||
|
|
60f8c51f44 | ||
|
|
4708323384 | ||
|
|
d7c08ce505 | ||
|
|
af56d7626c | ||
|
|
0f6ffe06ae | ||
|
|
e1414f5e20 | ||
|
|
b99c167db2 | ||
|
|
d93575aacb | ||
|
|
0ae4bd345f | ||
|
|
eb070d37e9 | ||
|
|
8cc8ff5c6d | ||
|
|
6d2db74edc | ||
|
|
fc711b57fc | ||
|
|
d9858ded89 | ||
|
|
b5975e101c | ||
|
|
30b9a936f2 | ||
|
|
0a3f91493b | ||
|
|
1fc912fa2c | ||
|
|
79e7e8db2a | ||
|
|
55b7490fa0 | ||
|
|
77be6551e4 | ||
|
|
4db21ffd8d | ||
|
|
60777dda32 | ||
|
|
a85024cfad | ||
|
|
a573d71a4f | ||
|
|
91fcbabfd1 | ||
|
|
7c42536413 | ||
|
|
d0cef50b7c | ||
|
|
12ec14179c | ||
|
|
d876892cb0 | ||
|
|
6e385259c0 | ||
|
|
fda52b2aeb | ||
|
|
3de6193702 | ||
|
|
55596ae9e5 | ||
|
|
f13e6119d3 | ||
|
|
259607949c | ||
|
|
d6969e6f6f | ||
|
|
c68a01e47b | ||
|
|
af08e1b5fb | ||
|
|
9b457d25eb | ||
|
|
58a1b30de9 | ||
|
|
2fb1a45e5e | ||
|
|
ec3ebdd5d4 | ||
|
|
bf585d7cd1 | ||
|
|
47a77eff47 | ||
|
|
cdaff20462 | ||
|
|
b86b7e44dc | ||
|
|
dbf4b34072 | ||
|
|
c4799ea7c6 | ||
|
|
543f85d9a5 | ||
|
|
c60ea50c2e | ||
|
|
61fcedf5d7 | ||
|
|
c0a86f6a6f | ||
|
|
96e053dd48 | ||
|
|
bb33fe065f | ||
|
|
be0ae71154 | ||
|
|
13c9d0350c | ||
|
|
245f90901c | ||
|
|
d4c8054c94 | ||
|
|
1e035a7b41 | ||
|
|
3744177e16 | ||
|
|
960b68113d | ||
|
|
3f2f88422e | ||
|
|
7e6dd11969 | ||
|
|
417edf1f72 | ||
|
|
712c6f84ee | ||
|
|
19369ba2c2 | ||
|
|
90eafb4385 | ||
|
|
7b3c24bfac | ||
|
|
ede5ed8d28 | ||
|
|
5c512a01c3 | ||
|
|
6105e963fa | ||
|
|
97e62eda54 | ||
|
|
d33549529e | ||
|
|
4fd197451f | ||
|
|
b03f1d33fe | ||
|
|
2ebcc4915d | ||
|
|
e0f9952773 | ||
|
|
faab92e9b5 | ||
|
|
d733f47e92 | ||
|
|
9da48ac245 | ||
|
|
959abb6845 | ||
|
|
b8ba94612b | ||
|
|
2e4128ab40 | ||
|
|
98204bb9f9 | ||
|
|
8e8726804b | ||
|
|
8635c05565 | ||
|
|
0a503e04fc | ||
|
|
1a3da997a1 | ||
|
|
b77713adc9 | ||
|
|
09294be57e | ||
|
|
71c64420ef | ||
|
|
78f3c72e78 | ||
|
|
efb9b41a57 | ||
|
|
4f97e84f18 | ||
|
|
1579e570eb | ||
|
|
e2e139ae7e | ||
|
|
e47f24d3ba | ||
|
|
f674eaa9de | ||
|
|
08ee1606c2 | ||
|
|
b1229340bb | ||
|
|
031789acfb | ||
|
|
ab62f8714b | ||
|
|
7a91d5aba1 | ||
|
|
2371274929 | ||
|
|
d101ea3c6a | ||
|
|
122e2390b9 | ||
|
|
913f0168ec | ||
|
|
1847e25d28 | ||
|
|
eabbf1b591 | ||
|
|
5c8998fb3a | ||
|
|
eadfdb02a8 | ||
|
|
e91a336d35 | ||
|
|
80c396cd81 | ||
|
|
9b97816127 | ||
|
|
65f726e33d | ||
|
|
650c030b13 | ||
|
|
786b636e3b | ||
|
|
c331511fef | ||
|
|
110236df55 | ||
|
|
592b7d34f9 | ||
|
|
51c4c93b03 | ||
|
|
c46d050356 | ||
|
|
f15d480d37 | ||
|
|
fb1b3d40de | ||
|
|
53e23dc29c | ||
|
|
3d3cb5d208 | ||
|
|
b2fce8c93d | ||
|
|
ef0b10ee78 | ||
|
|
b04acda600 | ||
|
|
95c466dbc3 | ||
|
|
6d22c906ec | ||
|
|
5b4e861f74 | ||
|
|
e3c368fd3a | ||
|
|
c4ef932dba | ||
|
|
b44b719cff | ||
|
|
a1ad742d96 | ||
|
|
383af37178 | ||
|
|
9b8617ed79 | ||
|
|
0e37c73fb2 | ||
|
|
ebe9abd880 | ||
|
|
99d46acaa2 | ||
|
|
4931842ee6 | ||
|
|
893402279e | ||
|
|
f340c94bd6 | ||
|
|
794f797a20 | ||
|
|
24c196c8a3 | ||
|
|
f4d72b4335 | ||
|
|
f33d04ca9a | ||
|
|
a46dd6b864 | ||
|
|
6bc5635f35 | ||
|
|
91f17c017d | ||
|
|
9ffc04d496 | ||
|
|
949bc163af | ||
|
|
8bf257ffa3 | ||
|
|
3fdb909d09 | ||
|
|
6401776841 | ||
|
|
a54a9d1a5d | ||
|
|
84d0dd328a | ||
|
|
e216f2f686 | ||
|
|
5a02bdaedd | ||
|
|
74e42a3d6f | ||
|
|
cdc81cd91c | ||
|
|
632ebef136 | ||
|
|
9521e6472a | ||
|
|
af302a5c85 | ||
|
|
0be7367afe | ||
|
|
7459a58f91 | ||
|
|
7c5cc24b32 | ||
|
|
fc98a45f93 | ||
|
|
26f745fad1 | ||
|
|
6c5ef2d462 | ||
|
|
76a1715fde | ||
|
|
ba6135c2ac | ||
|
|
5ee53a5d00 | ||
|
|
20c87f5f51 | ||
|
|
a3f9695f68 | ||
|
|
43f098479c | ||
|
|
86523b37b7 | ||
|
|
e4fccb5820 | ||
|
|
2347a46453 | ||
|
|
38bab8e1b0 | ||
|
|
93c11ebbdb | ||
|
|
fc666e9c0f | ||
|
|
ea210ff341 | ||
|
|
622b8b1a9a | ||
|
|
d54bf5affb | ||
|
|
b6562ba7ec | ||
|
|
e1a2d59748 | ||
|
|
bcb8404450 | ||
|
|
ab87a57b62 | ||
|
|
69db3d68d5 | ||
|
|
a25e78715f | ||
|
|
624a38317c | ||
|
|
6b2a0bab5d | ||
|
|
50f825644e | ||
|
|
c93da08c82 | ||
|
|
781fc6cb5d | ||
|
|
0a98360989 | ||
|
|
364241f419 | ||
|
|
5c110d9ce3 | ||
|
|
9250ed919d | ||
|
|
100462f3bb | ||
|
|
29ef2d2968 | ||
|
|
358258ab66 | ||
|
|
ab91c3e68d | ||
|
|
33f71e742b | ||
|
|
531a9df7f1 | ||
|
|
a40cb9a8f0 | ||
|
|
e62e6aa3fc | ||
|
|
978b19d110 | ||
|
|
82a4474d63 | ||
|
|
fedd739018 | ||
|
|
f84910a678 | ||
|
|
0d10b3583e | ||
|
|
e74eeba527 | ||
|
|
d24ed29d47 | ||
|
|
3850715413 | ||
|
|
e51e6da212 | ||
|
|
b243a1d5c8 | ||
|
|
ba46d413f6 | ||
|
|
a394689e9f | ||
|
|
8c807c816d | ||
|
|
628b58f9b5 | ||
|
|
41d2f8b84b | ||
|
|
58c0d5884c | ||
|
|
dba9b08561 | ||
|
|
6f9057ce56 | ||
|
|
a9f247bf62 | ||
|
|
5d89d84aa5 | ||
|
|
6907836bad | ||
|
|
f3a198f977 | ||
|
|
3995ba4ee0 | ||
|
|
b5207fee2a | ||
|
|
334e59ab52 | ||
|
|
cfb3991c26 | ||
|
|
5159e5fd91 | ||
|
|
55588b4d30 | ||
|
|
b43a7109d1 | ||
|
|
abfbfec790 | ||
|
|
f379fd65a8 | ||
|
|
b804172a0d | ||
|
|
00408f7634 | ||
|
|
55ec60cdce | ||
|
|
7240f4f6a4 | ||
|
|
ff07eda25b | ||
|
|
8b7f5bf676 | ||
|
|
7885196aa6 | ||
|
|
aa98138760 | ||
|
|
80504627a8 | ||
|
|
e9eb1d18ac | ||
|
|
c2dd8dd3c0 | ||
|
|
deb797952f | ||
|
|
d02d4ed915 | ||
|
|
76d65c0cff | ||
|
|
5dcae40907 | ||
|
|
42f65bafd1 | ||
|
|
e673f39958 | ||
|
|
e599e00bb7 | ||
|
|
4151ba2f3c | ||
|
|
d55eacd0b1 | ||
|
|
c6f64218db | ||
|
|
f82c22e3ee | ||
|
|
d7ac62222f | ||
|
|
55ac5efbfb | ||
|
|
4091299931 | ||
|
|
3cbd0a8025 | ||
|
|
040f99d285 | ||
|
|
2fdf300849 | ||
|
|
c8e886a0f8 | ||
|
|
433da799ef | ||
|
|
7f25fc1619 | ||
|
|
a57c7780be | ||
|
|
e7f312ec50 | ||
|
|
2b15a40f75 | ||
|
|
d6110a2d4b | ||
|
|
39ea8e5859 | ||
|
|
17e592e8ed | ||
|
|
999fd713f3 | ||
|
|
35a8f79b54 | ||
|
|
9c5416719e | ||
|
|
36ff1696ea | ||
|
|
152e5f7b91 | ||
|
|
2a78a0a96c | ||
|
|
8894a62ab7 | ||
|
|
fab02dc96f | ||
|
|
e4fdd75e9f | ||
|
|
3143681cd7 | ||
|
|
f40f7c35ad | ||
|
|
c084faeee4 | ||
|
|
8eb8eaa87a | ||
|
|
ff7a70f3fc | ||
|
|
afafb15d64 | ||
|
|
f62dbbaaa7 | ||
|
|
2692f9af79 | ||
|
|
27fc61138e | ||
|
|
e5d00e82b2 | ||
|
|
1d33162c1f | ||
|
|
d2502a6cb6 | ||
|
|
9f7595e4b0 | ||
|
|
80050bc231 | ||
|
|
64ee48ab14 | ||
|
|
e7ef6a8e52 | ||
|
|
0291cee0a8 | ||
|
|
6733ecad88 | ||
|
|
1f91a349da | ||
|
|
609451a9d6 | ||
|
|
28de9359fa | ||
|
|
b03fc6f781 | ||
|
|
7bb58e5eb1 | ||
|
|
ba8c1f14a5 | ||
|
|
614a8abb91 | ||
|
|
88b11a0d31 | ||
|
|
c465ad9faa | ||
|
|
47d1efd4d6 | ||
|
|
f8c817fa8c | ||
|
|
c8efd8512e | ||
|
|
b63d50ab5f | ||
|
|
226f594539 | ||
|
|
14a1dfd85d | ||
|
|
f1c7710fb3 | ||
|
|
d88bcfbca2 | ||
|
|
2876964c64 | ||
|
|
00dc3d2fc1 | ||
|
|
aaaf79aafd | ||
|
|
3f15d22d89 | ||
|
|
ec5a50ca35 | ||
|
|
f8e189c5f4 | ||
|
|
11aa10d8db | ||
|
|
e1477026a3 | ||
|
|
b0a85a0c4f | ||
|
|
1b38b7004d | ||
|
|
464af3b9e1 | ||
|
|
def2e1434e | ||
|
|
3204de5f56 | ||
|
|
653102b351 | ||
|
|
bb044fe7fa | ||
|
|
aa0d15f64d | ||
|
|
3c18e42e25 | ||
|
|
bb41323253 | ||
|
|
aa6ee5b694 | ||
|
|
8f6709e752 | ||
|
|
adde86ff06 | ||
|
|
18d7e6aaf4 | ||
|
|
ef3f50259e | ||
|
|
9c48e02531 | ||
|
|
02346ebff0 | ||
|
|
173dd0cd4c | ||
|
|
10a1d11962 | ||
|
|
d54c35cd2c | ||
|
|
aa35e8a49d | ||
|
|
49d27bbc10 | ||
|
|
109f1b382d | ||
|
|
4e058fd816 | ||
|
|
2e4d9d97cd | ||
|
|
ef8225be06 | ||
|
|
787033a129 | ||
|
|
85c8cbd238 | ||
|
|
ae46ce4a98 | ||
|
|
ee771c0343 | ||
|
|
01d7b10f5a | ||
|
|
c7534437d8 | ||
|
|
6b2900e442 | ||
|
|
f523b057dc | ||
|
|
16ae2952a4 | ||
|
|
fca0a1d871 | ||
|
|
c81d0224c8 | ||
|
|
e1df955d07 | ||
|
|
82c54f409c | ||
|
|
79775dde63 | ||
|
|
4e1e4c77ec | ||
|
|
066382ae6c | ||
|
|
ddf181420b | ||
|
|
82b6052825 | ||
|
|
d1bfd28460 | ||
|
|
3ef1b9b220 | ||
|
|
c64cd986f1 | ||
|
|
f0ab445bd8 | ||
|
|
d6165109e3 | ||
|
|
f5715aa683 | ||
|
|
2f87f4a38a | ||
|
|
1187f8ef68 | ||
|
|
76c114b06d | ||
|
|
f4d06f6861 | ||
|
|
c2b473f6a2 | ||
|
|
78865292a9 | ||
|
|
3be388f8e8 | ||
|
|
ca5a51d55c | ||
|
|
2da47a1439 | ||
|
|
a4d274211b | ||
|
|
eb6b8a4d92 | ||
|
|
370c446bfd | ||
|
|
3e614aab58 | ||
|
|
642f75d034 | ||
|
|
6ef1bb0412 | ||
|
|
5c72c3da3e | ||
|
|
a6682ad2bf | ||
|
|
fb59f1e065 | ||
|
|
0e09457f0e | ||
|
|
0fdc491f9e | ||
|
|
e9f01b7e5b | ||
|
|
63aa72fea4 | ||
|
|
837276845e | ||
|
|
2ca42bd3f3 | ||
|
|
be3da7f2b1 | ||
|
|
c655d9154a | ||
|
|
4109622e42 | ||
|
|
cbd7e7d26c | ||
|
|
118b25096b | ||
|
|
d9dba2c6ba | ||
|
|
da417b6f00 | ||
|
|
be050b3922 | ||
|
|
f17a4be78f | ||
|
|
fc9e345514 | ||
|
|
21e5cc3dd0 | ||
|
|
335c940353 | ||
|
|
a3388168cf | ||
|
|
581a3a5962 | ||
|
|
a5104fa6ed | ||
|
|
7408d3a1cb | ||
|
|
bb2970e05b | ||
|
|
6d38db33a7 | ||
|
|
e7f5b419c7 | ||
|
|
0429b24ffc | ||
|
|
1c4259e13f | ||
|
|
6d8962e006 | ||
|
|
25e8f4504c | ||
|
|
02ea973d24 | ||
|
|
c53a5c6f4f | ||
|
|
df4a73cd73 | ||
|
|
3e93ab2506 | ||
|
|
9834dcb2b9 | ||
|
|
c4aeace21a | ||
|
|
fb3b8bdf84 | ||
|
|
85ba779a1c | ||
|
|
a0d185da73 | ||
|
|
caa0a24c24 | ||
|
|
5faf591987 | ||
|
|
30f5cb7a3d | ||
|
|
d1ad973028 | ||
|
|
98dcaae37f | ||
|
|
29c0ba7308 | ||
|
|
176b3f5034 | ||
|
|
39301f49c3 | ||
|
|
8d003e5181 | ||
|
|
11b453e0d6 | ||
|
|
d2393d7fe2 | ||
|
|
800811a140 | ||
|
|
b37e3c05dd | ||
|
|
918700fdbc | ||
|
|
751e2c9584 | ||
|
|
946a86d48d | ||
|
|
f936baea47 | ||
|
|
1edabd4eb4 | ||
|
|
c60a782728 | ||
|
|
a012cd99fc | ||
|
|
4964d973d0 | ||
|
|
727b1a5200 | ||
|
|
face5a7f32 | ||
|
|
232b86d32a | ||
|
|
57873075c1 | ||
|
|
e4e0e9a2f2 | ||
|
|
434ed21ffc | ||
|
|
a7d06c8906 | ||
|
|
d87c2597dc | ||
|
|
9d4bc33076 | ||
|
|
5223239000 | ||
|
|
a20807c607 | ||
|
|
461c5346d9 | ||
|
|
77df5fa16a | ||
|
|
25bca0df53 | ||
|
|
b91ef6487d | ||
|
|
d721fa7e0c | ||
|
|
a47efda7b5 | ||
|
|
2316951e7f | ||
|
|
9aa02430fd | ||
|
|
645b025172 | ||
|
|
72b7a125e0 | ||
|
|
f1524159d5 | ||
|
|
31a5178d9c | ||
|
|
d8e97e7e2b | ||
|
|
41fae55b3f | ||
|
|
a394f7f268 | ||
|
|
765abc5889 | ||
|
|
77a4b61c61 | ||
|
|
f960fa2df6 | ||
|
|
d26954a0bb | ||
|
|
44b4e5a78c | ||
|
|
532d74bcb2 | ||
|
|
121b753d66 | ||
|
|
ba98e1191a | ||
|
|
5bd8a6a5e2 | ||
|
|
5b9d3efe32 | ||
|
|
79c98932a9 | ||
|
|
691d215741 | ||
|
|
333551f4a0 | ||
|
|
d8c3450426 | ||
|
|
5b0ea491a5 | ||
|
|
8589460fa5 | ||
|
|
8fa377fb99 | ||
|
|
fb6fa476a2 | ||
|
|
59cec4879e | ||
|
|
d058b58cd0 | ||
|
|
8df1fa9fc8 | ||
|
|
888702297f | ||
|
|
b62ab3ea5c | ||
|
|
d95a7271c3 | ||
|
|
cf28b6c8ea | ||
|
|
c9215b2e70 | ||
|
|
10f3396b8c | ||
|
|
1bb4a3be7b | ||
|
|
13fbc16557 | ||
|
|
530f8ce0a6 | ||
|
|
7b5233a380 | ||
|
|
5b0fa54e3a | ||
|
|
016a99b17c | ||
|
|
249c486459 | ||
|
|
ca0474c081 | ||
|
|
ebf51e48c4 | ||
|
|
9bda4419fe | ||
|
|
d685d1c5eb | ||
|
|
dac626755b | ||
|
|
c05595a3b5 | ||
|
|
6e117ce2d7 | ||
|
|
1ad343806b | ||
|
|
0cd88c40e5 | ||
|
|
4ff7a9efe9 | ||
|
|
ef8cba2bba | ||
|
|
2e10d95ffb | ||
|
|
53a86ec5df | ||
|
|
52ad2d4b97 | ||
|
|
da9128443e | ||
| 344f927990 | |||
| 195b207a71 | |||
|
|
6e2d5ba6fb | ||
|
|
1efc77d57c | ||
| a6afb713da | |||
|
|
f2ae6014be | ||
| 0dacbfab24 | |||
|
|
7d6dc23266 | ||
|
|
cad6037169 | ||
|
|
42606ce94e | ||
| d155678636 | |||
| e9d21c41a6 | |||
| a31feca107 | |||
|
|
5df9add885 | ||
|
|
4d4a8ff36e | ||
|
|
f5f4bc281b | ||
|
|
ad2e300b3d | ||
| e1d764a49e | |||
|
|
87d25d1f9b | ||
|
|
243ff323c6 | ||
|
|
6b357b0727 | ||
|
|
20b555ee46 | ||
|
|
67aa3e47bc | ||
| 38ba262d9c | |||
|
|
3e617ad656 | ||
|
|
e54de8e55e | ||
|
|
6cd1d60827 | ||
|
|
d140d7d1ac | ||
|
|
f8a62e6c3d | ||
|
|
0f4dc7599f | ||
|
|
de98c3e1bb | ||
|
|
8dc4f2d170 | ||
|
|
ed6ee47d53 | ||
|
|
3eff113894 | ||
|
|
9d63f6e180 | ||
|
|
4fddc08818 | ||
|
|
b39fe302a1 | ||
| 9dfd4e4633 | |||
| 553b013180 | |||
|
|
d970f3f177 | ||
|
|
5605dd08a5 | ||
|
|
89fb711251 | ||
|
|
11a13fe5ec | ||
|
|
3d454ea30f | ||
|
|
77f2ff5052 | ||
|
|
7ec08b69bb | ||
|
|
ba58e77bca | ||
|
|
35d8cc7eec | ||
| 4ffead8554 | |||
|
|
c59e7c398a | ||
|
|
442bca0b3e | ||
|
|
2c6cabba15 | ||
|
|
b6f1bd8253 | ||
| 338414b5a6 | |||
| a1d5eb9db2 | |||
|
|
2cedbd49f4 | ||
| 8a65c3f21f | |||
| c9c8b9850b | |||
|
|
6ecd8963b7 | ||
|
|
0411cb647b | ||
|
|
2073f8c9db | ||
|
|
acc0dbace6 | ||
|
|
1deb229328 | ||
|
|
8fd6bad912 | ||
|
|
176f202f25 | ||
|
|
f99f7f67d2 | ||
|
|
6cc2c72789 | ||
|
|
d2f1c977b4 | ||
|
|
dbbd1de26d | ||
| b673fc93c7 | |||
| b65b53dc53 | |||
|
|
4f28f15406 | ||
| 641d52902a | |||
| 1d4d0eed86 | |||
|
|
4a6a477c0c | ||
|
|
ae3e2149c0 | ||
| 2d8e343e4f | |||
| ffc93f26fa | |||
|
|
293cbbfd0a | ||
|
|
a159f315a9 | ||
|
|
b73f1b0f52 | ||
|
|
eb02d7f825 | ||
|
|
4ef5ae256b | ||
| 6ba281b7ed | |||
| 9cfbf2ae2d | |||
| 8011eaf9fd | |||
|
|
965b9d56d1 | ||
|
|
c798d78179 | ||
| 05e0bdff96 | |||
| aad3989165 | |||
| 3dfc6f0b3d | |||
|
|
cd8dd2ee4a | ||
|
|
a89d8e3377 | ||
|
|
2a77c74ba8 | ||
|
|
f3a9536685 | ||
| 34c2e8638e | |||
|
|
f34c402757 | ||
|
|
74e5d34c9f | ||
|
|
213cf32012 | ||
|
|
7ac0e5f94d | ||
|
|
535e6bf9bc | ||
|
|
70b339b009 | ||
|
|
5346a12c3c | ||
|
|
8c28084b7a | ||
|
|
463af418e5 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -35,4 +35,6 @@ build/
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
/logs/app.log
|
||||
/logs/
|
||||
|
||||
BIN
.idea/.cache/.Apifox_Helper/.toolWindow.db
generated
Normal file
BIN
.idea/.cache/.Apifox_Helper/.toolWindow.db
generated
Normal file
Binary file not shown.
11
.idea/ApifoxUploaderProjectSetting.xml
generated
11
.idea/ApifoxUploaderProjectSetting.xml
generated
File diff suppressed because one or more lines are too long
28
.idea/dataSources.xml
generated
28
.idea/dataSources.xml
generated
@@ -1,12 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="@192.168.8.88" uuid="d0f6174d-14a9-4a99-9dfa-6f782ed4fa8d">
|
||||
<data-source source="LOCAL" name="jd@192.168.8.88" uuid="31c422ff-86c8-4fe9-ba98-f496d4f1b534">
|
||||
<driver-ref>mysql.8</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<imported>true</imported>
|
||||
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:mysql://192.168.8.88:3306/?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT</jdbc-url>
|
||||
<jdbc-url>jdbc:mysql://192.168.8.88:3306/jd?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="7@192.168.8.88" uuid="a384b02a-80ce-4ca4-b73c-1bd2b6e91838">
|
||||
<driver-ref>redis</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>jdbc.RedisDriver</jdbc-driver>
|
||||
<jdbc-url>jdbc:redis://192.168.8.88:6379/7</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
@@ -15,17 +27,5 @@
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="7@192.168.8.88" uuid="02bd8878-dd85-4337-9b25-55ca707e2a81">
|
||||
<driver-ref>redis</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>jdbc.RedisDriver</jdbc-driver>
|
||||
<jdbc-url>jdbc:redis://192.168.8.88:6379/7</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/diff-generator.xml
generated
7
.idea/diff-generator.xml
generated
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiffGenerationConfig" isInitGenerateDdl="true">
|
||||
<option name="initGenerateDdl" value="true" />
|
||||
<option name="sourceModelValue" value="jd:jd" />
|
||||
<option name="targetDbValue" value="eb8a6f9c-c8ff-4224-9875-8dd3cf91a680" />
|
||||
<component name="DiffGenerationConfig">
|
||||
<option name="sourceModelValue" value="jd:entityManagerFactory" />
|
||||
<option name="targetDbValue" value="31c422ff-86c8-4fe9-ba98-f496d4f1b534" />
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/jpa-buddy-datasource.xml
generated
Normal file
9
.idea/jpa-buddy-datasource.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JpbDataSourceConfig">
|
||||
<data-source-infos>
|
||||
<data-source-info id="eb8a6f9c-c8ff-4224-9875-8dd3cf91a680" persistence-unit-name="jd:jd" />
|
||||
<data-source-info id="cc2bffcd-366e-45f1-9199-7c59c6f5def9" persistence-unit-name="jd:entityManagerFactory" />
|
||||
</data-source-infos>
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/jpa.xml
generated
8
.idea/jpa.xml
generated
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JpaBuddyIdeaProjectConfig">
|
||||
<component name="JpaBuddyIdeaProjectConfig" ddlActionDbType="mysql">
|
||||
<option name="defaultUnitInitialized" value="true" />
|
||||
<option name="reLastEntityCreationPackage" value="src/main/java/cn/van/business/model" />
|
||||
<option name="reLastEntityCreationPackage" value="src/main/java/cn/van/business/model/cj" />
|
||||
<option name="renamerInitialized" value="true" />
|
||||
<option name="reverseEngineeringLastDbConnectionId" value="d0f6174d-14a9-4a99-9dfa-6f782ed4fa8d__jd" />
|
||||
<option name="reverseEngineeringLastDbConnectionId" value="31c422ff-86c8-4fe9-ba98-f496d4f1b534" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
2
.idea/jpb-settings.xml
generated
2
.idea/jpb-settings.xml
generated
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DatabaseMigrationSettings" lastSelectedDirectory="src\main\java\cn\van\business\model" />
|
||||
<component name="DatabaseMigrationSettings" lastSelectedDirectory="src\main\java\cn\van\business\model\jd" />
|
||||
</project>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -8,7 +8,7 @@
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8(202)" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
6
.idea/sqldialects.xml
generated
6
.idea/sqldialects.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="PROJECT" dialect="MySQL" />
|
||||
</component>
|
||||
</project>
|
||||
115
Redis清理说明.md
Normal file
115
Redis清理说明.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Redis键清理功能说明
|
||||
|
||||
## 功能概述
|
||||
本功能用于清理Redis中超过93天(3个月)的旧数据,支持两种类型的键:
|
||||
1. **tag键**:格式为 `tag:hash值:YYYY-MM-DD HH`,例如 `tag:01381d95e4936f1f3fe643bba2171894:2025-01-12 00`
|
||||
2. **jd:refresh:tag键**:格式为 `jd:refresh:tag:hash值:YYYY-MM-DD HH:mm:ss` 或 `jd:refresh:tag:YYYY-MM-DD HH:mm:ss`
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 方式一:手动调用API接口(推荐)
|
||||
|
||||
⚠️ **注意端口号**:
|
||||
- **jd项目端口**:6666(直接访问,无需认证)
|
||||
- RuoYi框架端口:30313(需要认证,不推荐)
|
||||
|
||||
发送POST请求到:`http://your-server:6666/jd/cleanRedisData`
|
||||
|
||||
**请求示例:**
|
||||
```bash
|
||||
curl -X POST http://192.168.8.88:6666/jd/cleanRedisData \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"skey": "2192057370ef8140c201079969c956a3"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Redis清理任务已执行完成,详情请查看日志"
|
||||
}
|
||||
```
|
||||
|
||||
### 方式二:使用Postman等工具
|
||||
|
||||
1. 创建新的POST请求
|
||||
2. URL: `http://192.168.8.88:6666/jd/cleanRedisData`(注意是6666端口)
|
||||
3. Headers: `Content-Type: application/json`
|
||||
4. Body 标签选择 **raw** 格式,类型选择 **JSON**
|
||||
5. Body 内容:
|
||||
```json
|
||||
{
|
||||
"skey": "2192057370ef8140c201079969c956a3"
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **常见错误**:
|
||||
- ❌ 不要把skey放在Query参数中
|
||||
- ❌ 不要使用30313端口(会遇到401认证错误)
|
||||
- ✅ 确保在Body标签中使用JSON格式
|
||||
|
||||
### 方式三:自动定时执行
|
||||
|
||||
系统已配置定时任务,每天凌晨3点自动执行清理:
|
||||
- **tag键清理**:每天凌晨3点执行(cron: `0 0 3 * * ?`)
|
||||
- **jd:refresh:tag键清理**:每月1日11:45执行(cron: `0 45 11 * * ?`)
|
||||
|
||||
## 清理规则
|
||||
|
||||
- **截止日期**:当前时间减去93天
|
||||
- **清理对象**:所有早于截止日期的键
|
||||
- **安全性**:只删除符合特定格式且过期的键
|
||||
|
||||
例如:
|
||||
- 当前时间:2025-10-27
|
||||
- 截止日期:2025-07-26
|
||||
- 将删除:2025-07-26之前的所有符合格式的键
|
||||
- 保留:2025-07-26及之后的键
|
||||
|
||||
## 日志查看
|
||||
|
||||
清理任务执行时会输出详细日志,可通过以下日志查看执行情况:
|
||||
|
||||
```
|
||||
开始清理93天前的tag键数据,截止时间:YYYY-MM-DD HH:mm:ss
|
||||
找到 X 个tag相关的键
|
||||
已删除 100 个过期的tag键
|
||||
已删除 200 个过期的tag键
|
||||
...
|
||||
tag键清理完成,共删除 X 个过期键
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **skey验证**:调用接口需要提供正确的skey(密钥)
|
||||
2. **执行时间**:建议在业务低峰期手动执行清理,避免影响性能
|
||||
3. **备份建议**:首次执行前建议备份Redis数据
|
||||
4. **监控日志**:执行后及时查看日志,确认清理结果
|
||||
|
||||
## 代码位置
|
||||
|
||||
- **清理逻辑**:`d:\code\jd\src\main\java\cn\van\business\util\JDScheduleJob.java`
|
||||
- `cleanOldTagRedisData()` - 清理tag键
|
||||
- `cleanOldRedisHashData()` - 清理jd:refresh:tag键
|
||||
- `manualCleanOldRedisData()` - 手动触发清理
|
||||
|
||||
- **API接口**:`d:\code\jd\src\main\java\cn\van\business\controller\jd\JDInnerController.java`
|
||||
- `/jd/cleanRedisData` - POST接口
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 如何查看当前Redis中有多少符合条件的键?**
|
||||
A: 可以使用Redis命令:
|
||||
```bash
|
||||
redis-cli KEYS "tag:*" | wc -l
|
||||
redis-cli KEYS "jd:refresh:tag:*" | wc -l
|
||||
```
|
||||
|
||||
**Q: 清理后可以恢复吗?**
|
||||
A: 不可以,删除操作是不可逆的,请谨慎操作。
|
||||
|
||||
**Q: 如果需要调整清理天数怎么办?**
|
||||
A: 修改 `JDScheduleJob.java` 中的 `minusDays(93)` 参数,例如改为 `minusDays(60)` 清理60天前的数据。
|
||||
|
||||
176
doc/图片转换功能说明.md
Normal file
176
doc/图片转换功能说明.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# 评论图片WebP转JPG功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
评论模块中的图片如果是webp格式,会自动转换为jpg格式。转换后的图片会被缓存,下次不需要再次转换。
|
||||
|
||||
## 功能特性
|
||||
|
||||
1. **自动检测**:自动检测图片URL中的webp格式
|
||||
2. **格式转换**:将webp格式图片转换为jpg格式
|
||||
3. **结果缓存**:转换结果存储在数据库中,避免重复转换
|
||||
4. **文件存储**:转换后的jpg图片保存到本地目录
|
||||
5. **HTTP访问**:通过ImageController提供HTTP访问接口
|
||||
|
||||
## 配置说明
|
||||
|
||||
在 `application.yml` 中配置:
|
||||
|
||||
```yaml
|
||||
image:
|
||||
convert:
|
||||
# 图片存储路径(转换后的jpg图片存储目录)
|
||||
storage-path: ${user.home}/comment-images
|
||||
# 图片访问基础URL(如果配置,转换后的图片将通过此URL访问)
|
||||
# 例如: http://your-domain.com/images 或 http://localhost:6666/images
|
||||
# 如果为空,则返回本地文件路径
|
||||
base-url: http://localhost:6666/images
|
||||
```
|
||||
|
||||
### 配置项说明
|
||||
|
||||
- **storage-path**:转换后的jpg图片存储目录,默认使用 `${user.home}/comment-images`
|
||||
- **base-url**:图片访问的基础URL,如果配置,转换后的图片URL将使用此地址;如果不配置,则返回本地文件路径
|
||||
|
||||
## 数据库表
|
||||
|
||||
需要执行以下SQL创建图片转换记录表:
|
||||
|
||||
```sql
|
||||
-- 执行 sql/image_conversions.sql
|
||||
```
|
||||
|
||||
表结构:
|
||||
- `id`:主键ID
|
||||
- `original_url`:原始webp图片URL(唯一索引)
|
||||
- `converted_url`:转换后的jpg图片URL或路径
|
||||
- `converted_at`:转换时间
|
||||
- `file_size`:文件大小(字节)
|
||||
- `created_at`:创建时间
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. **图片URL解析**:从评论数据中解析出图片URL列表
|
||||
2. **格式检测**:检查URL中是否包含webp格式标识
|
||||
3. **缓存查询**:查询数据库,检查是否已转换过
|
||||
4. **格式转换**(如果未转换过):
|
||||
- 下载原始webp图片
|
||||
- 使用webp-imageio库读取webp格式
|
||||
- 转换为BufferedImage
|
||||
- 保存为jpg格式到本地目录
|
||||
- 保存转换记录到数据库
|
||||
5. **返回结果**:返回转换后的图片URL(或原URL,如果不是webp)
|
||||
|
||||
## 依赖库
|
||||
|
||||
项目已添加以下依赖:
|
||||
|
||||
1. **Thumbnailator** (0.4.20):图片处理库
|
||||
|
||||
**注意**:WebP格式支持需要系统或JVM本身支持webp格式。如果系统不支持webp,转换功能会跳过webp图片并返回原URL,不会影响系统正常运行。
|
||||
|
||||
如需支持webp格式转换,可以:
|
||||
- 使用支持webp的JVM版本
|
||||
- 手动添加webp-imageio库到项目中
|
||||
- 使用其他webp解码库
|
||||
|
||||
## API接口
|
||||
|
||||
### 图片访问接口
|
||||
|
||||
**GET** `/images/{filename}`
|
||||
|
||||
访问转换后的jpg图片文件。
|
||||
|
||||
**参数:**
|
||||
- `filename`:文件名(通常是MD5值.jpg)
|
||||
|
||||
**返回:**
|
||||
- 成功:图片文件(Content-Type: image/jpeg)
|
||||
- 失败:404 Not Found 或 400 Bad Request
|
||||
|
||||
**安全特性:**
|
||||
- 防止路径遍历攻击
|
||||
- 文件路径验证
|
||||
- 仅允许访问存储目录内的文件
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 前端调用
|
||||
|
||||
评论生成接口返回的图片URL列表已经过转换处理:
|
||||
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"commentText": "评论内容",
|
||||
"images": [
|
||||
"http://localhost:6666/images/abc123.jpg",
|
||||
"http://localhost:6666/images/def456.jpg"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 后端调用
|
||||
|
||||
图片转换服务会自动集成到评论生成流程中:
|
||||
|
||||
```java
|
||||
// 在 JDInnerController.commentGenerate 方法中
|
||||
List<String> imageUrls = parsePictureUrls(commentToUse.getPictureUrls());
|
||||
List<String> convertedImageUrls = imageConvertService.convertImageUrls(imageUrls);
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **首次转换**:首次转换webp图片时,需要下载图片并转换,可能耗时较长
|
||||
2. **存储空间**:转换后的jpg图片会占用磁盘空间,建议定期清理旧文件
|
||||
3. **网络依赖**:转换过程需要下载原始图片,确保网络连接正常
|
||||
4. **WebP支持**:确保webp-imageio库正确加载,否则转换会失败
|
||||
5. **并发处理**:多个请求同时转换同一张图片时,可能会导致重复转换(建议后续优化为加锁机制)
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题1:转换失败,提示"无法读取webp图片格式"
|
||||
|
||||
**解决方案:**
|
||||
1. 确认 `webp-imageio` 依赖已正确添加到pom.xml
|
||||
2. 确认依赖已成功下载(检查Maven本地仓库)
|
||||
3. 检查日志中的WebP支持检测信息
|
||||
|
||||
### 问题2:图片无法访问(404)
|
||||
|
||||
**解决方案:**
|
||||
1. 检查 `storage-path` 配置是否正确
|
||||
2. 确认图片文件已成功转换并保存
|
||||
3. 检查文件权限
|
||||
4. 如果配置了 `base-url`,确认URL是否正确
|
||||
|
||||
### 问题3:转换后的图片URL是本地路径
|
||||
|
||||
**解决方案:**
|
||||
在 `application.yml` 中配置 `base-url`:
|
||||
|
||||
```yaml
|
||||
image:
|
||||
convert:
|
||||
base-url: http://your-domain:6666/images
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
1. **异步转换**:对于大量图片,可以考虑异步转换
|
||||
2. **CDN加速**:将转换后的图片上传到CDN,提供更快的访问速度
|
||||
3. **定期清理**:定期清理长时间未使用的转换图片
|
||||
4. **缓存预热**:提前转换常用的图片
|
||||
|
||||
## 后续优化
|
||||
|
||||
1. 添加转换任务队列,支持批量转换
|
||||
2. 添加转换状态监控和统计
|
||||
3. 支持其他图片格式转换(如png、gif等)
|
||||
4. 添加图片压缩功能,减小文件大小
|
||||
|
||||
9
logs/app.log
Normal file
9
logs/app.log
Normal file
@@ -0,0 +1,9 @@
|
||||
2025-11-03 15:29:34 [main] INFO cn.van.Application - Starting Application using Java 17.0.14 with PID 56676 (D:\code\jd\target\classes started by CC in D:\code\jd)
|
||||
2025-11-03 15:29:34 [main] DEBUG cn.van.Application - Running with Spring Boot v3.1.5, Spring v6.0.13
|
||||
2025-11-03 15:29:34 [main] INFO cn.van.Application - The following 1 profile is active: "dev"
|
||||
2025-11-03 15:29:37 [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-6666"]
|
||||
2025-11-03 15:29:37 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||
2025-11-03 15:29:37 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.15]
|
||||
2025-11-03 15:29:37 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||
2025-11-03 15:29:39 [main] ERROR o.s.o.j.LocalContainerEntityManagerFactoryBean - Failed to initialize JPA EntityManagerFactory: Unable to create index (originalUrl) on table 'image_conversions' since the column 'originalUrl' was not found (specify the correct column name, which depends on the naming strategy, and may not be the same as the entity property name)
|
||||
2025-11-03 15:29:39 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create index (originalUrl) on table 'image_conversions' since the column 'originalUrl' was not found (specify the correct column name, which depends on the naming strategy, and may not be the same as the entity property name)
|
||||
19
out/production/jd/cn/van/business/model/jd/update-schema.sql
Normal file
19
out/production/jd/cn/van/business/model/jd/update-schema.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE product_order
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT NOT NULL,
|
||||
sku_name VARCHAR(255) NULL,
|
||||
sku_type INT NULL,
|
||||
order_id VARCHAR(255) NULL,
|
||||
order_time datetime NULL,
|
||||
order_account VARCHAR(255) NULL,
|
||||
is_reviewed BIT(1) NULL,
|
||||
review_time datetime NULL,
|
||||
is_cashback_received BIT(1) NULL,
|
||||
cashback_time datetime NULL,
|
||||
recipient_name VARCHAR(255) NULL,
|
||||
recipient_phone VARCHAR(255) NULL,
|
||||
recipient_address VARCHAR(255) NULL,
|
||||
who_order VARCHAR(255) NULL,
|
||||
from_wxid VARCHAR(255) NULL,
|
||||
CONSTRAINT pk_product_order PRIMARY KEY (id)
|
||||
);
|
||||
18
out/production/jd/cn/van/business/model/update-schema.sql
Normal file
18
out/production/jd/cn/van/business/model/update-schema.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE product_order
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT NOT NULL,
|
||||
sku_name VARCHAR(255) NULL,
|
||||
sku_type INT NULL,
|
||||
order_id VARCHAR(255) NULL,
|
||||
order_time datetime NULL,
|
||||
order_account VARCHAR(255) NULL,
|
||||
is_reviewed BIT(1) NULL,
|
||||
review_time datetime NULL,
|
||||
is_cashback_received BIT(1) NULL,
|
||||
cashback_time datetime NULL,
|
||||
recipient_name VARCHAR(255) NULL,
|
||||
recipient_phone VARCHAR(255) NULL,
|
||||
recipient_address VARCHAR(255) NULL,
|
||||
who_order VARCHAR(255) NULL,
|
||||
CONSTRAINT pk_product_order PRIMARY KEY (id)
|
||||
);
|
||||
154
pom.xml
154
pom.xml
@@ -4,20 +4,18 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<!-- 项目基本信息 -->
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>springboot-hql-web</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<name>SpringBootHQLWeb</name>
|
||||
<description>Spring Boot with HQL and Web support</description>
|
||||
<groupId>cn.van</groupId>
|
||||
<artifactId>jd-wx</artifactId>
|
||||
<version>1.0</version>
|
||||
<name>jd-wx</name>
|
||||
<description>jd-wx</description>
|
||||
|
||||
<!-- Java 版本 -->
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<spring-boot.version>2.6.13</spring-boot.version>
|
||||
<spring-boot.version>3.1.5</spring-boot.version>
|
||||
<rocketmq.version>2.3.2</rocketmq.version>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
</properties>
|
||||
|
||||
<!-- 依赖管理 -->
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -27,38 +25,71 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<version>${rocketmq.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<!-- 项目依赖 -->
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starter Web,用于REST API和MVC -->
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Starter Data JPA,包含Hibernate -->
|
||||
<!-- Spring Boot Data JPA -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL 数据库驱动 -->
|
||||
<!-- MySQL 驱动 -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>8.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Starter Test,测试依赖 -->
|
||||
<!-- RocketMQ -->
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<!-- 测试 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 京东依赖-->
|
||||
<!-- Hutool -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.24</version>
|
||||
</dependency>
|
||||
<!-- Fastjson2 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.21</version>
|
||||
</dependency>
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- JD 依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.jd</groupId>
|
||||
<artifactId>jdk</artifactId>
|
||||
<version>3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
<artifactId>jackson-mapper-asl</artifactId>
|
||||
@@ -70,41 +101,80 @@
|
||||
<artifactId>jackson-core-asl</artifactId>
|
||||
<version>1.9.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.8</version>
|
||||
</dependency>
|
||||
<!--fastjson-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.21</version>
|
||||
</dependency>
|
||||
|
||||
<!-- AOP -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<!-- HttpClient -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.13</version>
|
||||
</dependency>
|
||||
<!-- 图片处理库 Thumbnailator -->
|
||||
<dependency>
|
||||
<groupId>cn.van</groupId>
|
||||
<artifactId>open-api-sdk</artifactId>
|
||||
<version>2.0-2024-10-21</version>
|
||||
<groupId>net.coobird</groupId>
|
||||
<artifactId>thumbnailator</artifactId>
|
||||
<version>0.4.20</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<!-- Maven 插件 -->
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Spring Boot 插件 -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<configuration>
|
||||
<mainClass>cn.van.Application</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>maven-local88</id>
|
||||
<name>Local Repository</name>
|
||||
<url>http://134.175.126.60:38081/repository/maven-local88/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
<updatePolicy>always</updatePolicy>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<!-- RocketMQ 官方仓库 -->
|
||||
<repository>
|
||||
<id>rocketmq-repo</id>
|
||||
<name>RocketMQ Repository</name>
|
||||
<url>https://repo1.maven.org/maven2/org/apache/rocketmq/</url>
|
||||
</repository>
|
||||
<!-- Maven 中央仓库 -->
|
||||
<repository>
|
||||
<id>central</id>
|
||||
<name>Maven Central Repository</name>
|
||||
<url>https://repo1.maven.org/maven2</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
</project>
|
||||
|
||||
18
sql/image_conversions.sql
Normal file
18
sql/image_conversions.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- 图片转换记录表
|
||||
-- 用于存储webp格式图片转换为jpg格式的映射关系
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `image_conversions` (
|
||||
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`original_url` VARCHAR(2048) NOT NULL COMMENT '原始webp图片URL',
|
||||
`converted_url` VARCHAR(2048) NOT NULL COMMENT '转换后的jpg图片URL或本地路径',
|
||||
`converted_at` DATETIME NOT NULL COMMENT '转换时间',
|
||||
`file_size` BIGINT(20) DEFAULT NULL COMMENT '文件大小(字节)',
|
||||
`created_at` DATETIME NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_original_url` (`original_url`(255)),
|
||||
KEY `idx_original_url` (`original_url`(255)),
|
||||
KEY `idx_converted_url` (`converted_url`(255)),
|
||||
KEY `idx_converted_at` (`converted_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图片转换记录表';
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package cn.van;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
@@ -12,9 +18,25 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@Import(RocketMQAutoConfiguration.class)
|
||||
public class Application {
|
||||
|
||||
private final Environment env;
|
||||
|
||||
public Application(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
String[] activeProfiles = env.getActiveProfiles();
|
||||
if (activeProfiles.length == 0) {
|
||||
activeProfiles = env.getDefaultProfiles();
|
||||
}
|
||||
System.out.println("Active profiles: " + Arrays.toString(activeProfiles));
|
||||
}
|
||||
}
|
||||
|
||||
40
src/main/java/cn/van/business/config/AsyncConfig.java
Normal file
40
src/main/java/cn/van/business/config/AsyncConfig.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package cn.van.business.config;
|
||||
|
||||
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
|
||||
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.AsyncConfigurer;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2024/11/29 11:41
|
||||
* @description:
|
||||
*/
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig implements AsyncConfigurer {
|
||||
|
||||
@Bean(name = "threadPoolTaskExecutor")
|
||||
public Executor getAsyncExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(16); // 核心线程数
|
||||
executor.setMaxPoolSize(128); // 最大线程数
|
||||
executor.setQueueCapacity(500); // 队列容量
|
||||
executor.setThreadNamePrefix("Async-Executor-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
|
||||
executor.initialize(); // 初始化执行器
|
||||
return executor; // 返回执行器
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
|
||||
return new SimpleAsyncUncaughtExceptionHandler();
|
||||
}
|
||||
}
|
||||
142
src/main/java/cn/van/business/config/SchedulerConfig.java
Normal file
142
src/main/java/cn/van/business/config/SchedulerConfig.java
Normal file
@@ -0,0 +1,142 @@
|
||||
package cn.van.business.config;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.SchedulingConfigurer;
|
||||
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
@Configuration
|
||||
public class SchedulerConfig implements SchedulingConfigurer {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SchedulerConfig.class);
|
||||
private ScheduledExecutorService scheduledExecutorService;
|
||||
|
||||
@Override
|
||||
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
|
||||
// 动态配置线程池大小
|
||||
int poolSize = Integer.parseInt(System.getenv().getOrDefault("SCHEDULED_THREAD_POOL_SIZE", "10"));
|
||||
scheduledExecutorService = Executors.newScheduledThreadPool(poolSize);
|
||||
|
||||
taskRegistrar.setScheduler(new CustomScheduledExecutorService(scheduledExecutorService));
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
if (scheduledExecutorService != null && !scheduledExecutorService.isShutdown()) {
|
||||
scheduledExecutorService.shutdown();
|
||||
try {
|
||||
if (!scheduledExecutorService.awaitTermination(60, TimeUnit.SECONDS)) {
|
||||
scheduledExecutorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scheduledExecutorService.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record CustomScheduledExecutorService(
|
||||
ScheduledExecutorService delegate) implements ScheduledExecutorService {
|
||||
|
||||
@Override
|
||||
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
|
||||
return delegate.schedule(wrap(command), delay, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
|
||||
return delegate.scheduleAtFixedRate(wrap(command), initialDelay, period, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
|
||||
return delegate.scheduleWithFixedDelay(wrap(command), initialDelay, delay, unit);
|
||||
}
|
||||
|
||||
private Runnable wrap(Runnable command) {
|
||||
return () -> {
|
||||
try {
|
||||
command.run();
|
||||
} catch (Throwable t) { // 捕获所有类型的异常
|
||||
logger.error("Scheduled task error", t);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Delegate other methods to the delegate executor
|
||||
@Override
|
||||
public void shutdown() {
|
||||
delegate.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Runnable> shutdownNow() {
|
||||
return delegate.shutdownNow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShutdown() {
|
||||
return delegate.isShutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return delegate.isTerminated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
return delegate.awaitTermination(timeout, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Future<T> submit(Callable<T> task) {
|
||||
return delegate.submit(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Future<T> submit(Runnable task, T result) {
|
||||
return delegate.submit(task, result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<?> submit(Runnable task) {
|
||||
return delegate.submit(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
|
||||
return delegate.invokeAll(tasks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
|
||||
return delegate.invokeAll(tasks, timeout, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
|
||||
return delegate.invokeAny(tasks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
|
||||
return delegate.invokeAny(tasks, timeout, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ScheduledFuture<T> schedule(Callable<T> callable, long delay, TimeUnit unit) {
|
||||
return delegate.schedule(callable, delay, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
delegate.execute(wrap(command));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package cn.van.business.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* 图片访问控制器
|
||||
* 用于访问转换后的jpg图片
|
||||
*
|
||||
* @author System
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/images")
|
||||
public class ImageController {
|
||||
|
||||
/**
|
||||
* 图片存储根目录
|
||||
*/
|
||||
@Value("${image.convert.storage-path:${java.io.tmpdir}/comment-images}")
|
||||
private String storagePath;
|
||||
|
||||
/**
|
||||
* 获取转换后的图片
|
||||
*
|
||||
* @param filename 文件名(通常是MD5值.jpg)
|
||||
* @return 图片文件
|
||||
*/
|
||||
@GetMapping("/{filename:.+}")
|
||||
public ResponseEntity<Resource> getImage(@PathVariable String filename) {
|
||||
try {
|
||||
// 安全检查:防止路径遍历攻击
|
||||
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
|
||||
log.warn("非法的文件名请求: {}", filename);
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
Path filePath = Paths.get(storagePath, filename);
|
||||
File file = filePath.toFile();
|
||||
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
log.warn("图片文件不存在: {}", filePath);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// 检查文件是否在存储目录内(防止路径遍历)
|
||||
Path storageDir = Paths.get(storagePath).toAbsolutePath().normalize();
|
||||
Path resolvedPath = filePath.toAbsolutePath().normalize();
|
||||
if (!resolvedPath.startsWith(storageDir)) {
|
||||
log.warn("非法的文件路径访问: {}", resolvedPath);
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
Resource resource = new FileSystemResource(file);
|
||||
|
||||
// 判断文件类型
|
||||
String contentType = Files.probeContentType(filePath);
|
||||
if (contentType == null) {
|
||||
// 如果无法探测,默认使用image/jpeg
|
||||
contentType = MediaType.IMAGE_JPEG_VALUE;
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(contentType))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"")
|
||||
.body(resource);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取图片失败: {}", filename, e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package cn.van.business.controller;
|
||||
|
||||
import cn.van.business.model.ApiResponse;
|
||||
import cn.van.business.service.MarketingImageService;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 营销图片合成控制器
|
||||
* 提供营销图片生成的HTTP接口
|
||||
*
|
||||
* @author System
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/jarvis/marketing-image")
|
||||
public class MarketingImageController {
|
||||
|
||||
@Autowired
|
||||
private MarketingImageService marketingImageService;
|
||||
|
||||
/**
|
||||
* 生成单张营销图片
|
||||
*
|
||||
* POST /jarvis/marketing-image/generate
|
||||
*
|
||||
* 请求体:
|
||||
* {
|
||||
* "productImageUrl": "商品主图URL",
|
||||
* "originalPrice": 499.0,
|
||||
* "finalPrice": 199.0,
|
||||
* "productName": "商品名称(可选)"
|
||||
* }
|
||||
*
|
||||
* 返回:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "msg": "操作成功",
|
||||
* "data": {
|
||||
* "imageBase64": "data:image/jpg;base64,..."
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@PostMapping("/generate")
|
||||
public JSONObject generateMarketingImage(@RequestBody Map<String, Object> request) {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
String productImageUrl = (String) request.get("productImageUrl");
|
||||
Object originalPriceObj = request.get("originalPrice");
|
||||
Object finalPriceObj = request.get("finalPrice");
|
||||
String productName = (String) request.get("productName");
|
||||
|
||||
if (productImageUrl == null || originalPriceObj == null || finalPriceObj == null) {
|
||||
response.put("code", 400);
|
||||
response.put("msg", "缺少必要参数: productImageUrl, originalPrice, finalPrice");
|
||||
return response;
|
||||
}
|
||||
|
||||
Double originalPrice = parseDouble(originalPriceObj);
|
||||
Double finalPrice = parseDouble(finalPriceObj);
|
||||
|
||||
if (originalPrice == null || finalPrice == null) {
|
||||
response.put("code", 400);
|
||||
response.put("msg", "价格参数格式错误");
|
||||
return response;
|
||||
}
|
||||
|
||||
String base64Image = marketingImageService.generateMarketingImage(
|
||||
productImageUrl, originalPrice, finalPrice, productName);
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("imageBase64", base64Image);
|
||||
|
||||
response.put("code", 200);
|
||||
response.put("msg", "操作成功");
|
||||
response.put("data", data);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成营销图片失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("msg", "生成营销图片失败: " + e.getMessage());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成营销图片
|
||||
*
|
||||
* POST /jarvis/marketing-image/batch-generate
|
||||
*
|
||||
* 请求体:
|
||||
* {
|
||||
* "requests": [
|
||||
* {
|
||||
* "productImageUrl": "商品主图URL1",
|
||||
* "originalPrice": 499.0,
|
||||
* "finalPrice": 199.0,
|
||||
* "productName": "商品名称1(可选)"
|
||||
* },
|
||||
* {
|
||||
* "productImageUrl": "商品主图URL2",
|
||||
* "originalPrice": 699.0,
|
||||
* "finalPrice": 349.0,
|
||||
* "productName": "商品名称2(可选)"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* 返回:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "msg": "操作成功",
|
||||
* "data": {
|
||||
* "results": [
|
||||
* {
|
||||
* "success": true,
|
||||
* "imageBase64": "data:image/jpg;base64,...",
|
||||
* "index": 0
|
||||
* },
|
||||
* {
|
||||
* "success": false,
|
||||
* "error": "错误信息",
|
||||
* "index": 1
|
||||
* }
|
||||
* ],
|
||||
* "total": 2,
|
||||
* "successCount": 1,
|
||||
* "failCount": 1
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@PostMapping("/batch-generate")
|
||||
public JSONObject batchGenerateMarketingImages(@RequestBody Map<String, Object> request) {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> requests = (List<Map<String, Object>>) request.get("requests");
|
||||
|
||||
if (requests == null || requests.isEmpty()) {
|
||||
response.put("code", 400);
|
||||
response.put("msg", "请求列表不能为空");
|
||||
return response;
|
||||
}
|
||||
|
||||
Map<String, Object> result = marketingImageService.batchGenerateMarketingImages(requests);
|
||||
|
||||
response.put("code", 200);
|
||||
response.put("msg", "操作成功");
|
||||
response.put("data", result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("批量生成营销图片失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("msg", "批量生成营销图片失败: " + e.getMessage());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Double值
|
||||
*/
|
||||
private Double parseDouble(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Double) {
|
||||
return (Double) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
}
|
||||
try {
|
||||
return Double.parseDouble(value.toString());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package cn.van.business.controller;
|
||||
|
||||
import cn.van.business.service.SocialMediaService;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 小红书/抖音内容生成控制器
|
||||
*
|
||||
* @author System
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/jarvis/social-media")
|
||||
public class SocialMediaController {
|
||||
|
||||
@Autowired
|
||||
private SocialMediaService socialMediaService;
|
||||
|
||||
/**
|
||||
* 提取关键词
|
||||
*
|
||||
* POST /jarvis/social-media/extract-keywords
|
||||
*
|
||||
* {
|
||||
* "productName": "商品名称"
|
||||
* }
|
||||
*/
|
||||
@PostMapping("/extract-keywords")
|
||||
public JSONObject extractKeywords(@RequestBody Map<String, Object> request) {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
String productName = (String) request.get("productName");
|
||||
|
||||
if (productName == null || productName.trim().isEmpty()) {
|
||||
response.put("code", 400);
|
||||
response.put("msg", "商品名称不能为空");
|
||||
return response;
|
||||
}
|
||||
|
||||
Map<String, Object> result = socialMediaService.extractKeywords(productName);
|
||||
|
||||
response.put("code", 200);
|
||||
response.put("msg", "操作成功");
|
||||
response.put("data", result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("提取关键词失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("msg", "提取关键词失败: " + e.getMessage());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文案
|
||||
*
|
||||
* POST /jarvis/social-media/generate-content
|
||||
*
|
||||
* {
|
||||
* "productName": "商品名称",
|
||||
* "originalPrice": 499.0,
|
||||
* "finalPrice": 199.0,
|
||||
* "keywords": "关键词1、关键词2",
|
||||
* "style": "xhs" // xhs/douyin/both
|
||||
* }
|
||||
*/
|
||||
@PostMapping("/generate-content")
|
||||
public JSONObject generateContent(@RequestBody Map<String, Object> request) {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
String productName = (String) request.get("productName");
|
||||
Object originalPriceObj = request.get("originalPrice");
|
||||
Object finalPriceObj = request.get("finalPrice");
|
||||
String keywords = (String) request.get("keywords");
|
||||
String style = (String) request.getOrDefault("style", "both");
|
||||
|
||||
if (productName == null || productName.trim().isEmpty()) {
|
||||
response.put("code", 400);
|
||||
response.put("msg", "商品名称不能为空");
|
||||
return response;
|
||||
}
|
||||
|
||||
Double originalPrice = parseDouble(originalPriceObj);
|
||||
Double finalPrice = parseDouble(finalPriceObj);
|
||||
|
||||
Map<String, Object> result = socialMediaService.generateContent(
|
||||
productName, originalPrice, finalPrice, keywords, style
|
||||
);
|
||||
|
||||
response.put("code", 200);
|
||||
response.put("msg", "操作成功");
|
||||
response.put("data", result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成文案失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("msg", "生成文案失败: " + e.getMessage());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键生成完整内容(关键词 + 文案 + 图片)
|
||||
*
|
||||
* POST /jarvis/social-media/generate-complete
|
||||
*
|
||||
* {
|
||||
* "productImageUrl": "商品主图URL",
|
||||
* "productName": "商品名称",
|
||||
* "originalPrice": 499.0,
|
||||
* "finalPrice": 199.0,
|
||||
* "style": "xhs"
|
||||
* }
|
||||
*/
|
||||
@PostMapping("/generate-complete")
|
||||
public JSONObject generateComplete(@RequestBody Map<String, Object> request) {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
String productImageUrl = (String) request.get("productImageUrl");
|
||||
String productName = (String) request.get("productName");
|
||||
Object originalPriceObj = request.get("originalPrice");
|
||||
Object finalPriceObj = request.get("finalPrice");
|
||||
String style = (String) request.getOrDefault("style", "both");
|
||||
|
||||
if (productName == null || productName.trim().isEmpty()) {
|
||||
response.put("code", 400);
|
||||
response.put("msg", "商品名称不能为空");
|
||||
return response;
|
||||
}
|
||||
|
||||
Double originalPrice = parseDouble(originalPriceObj);
|
||||
Double finalPrice = parseDouble(finalPriceObj);
|
||||
|
||||
Map<String, Object> result = socialMediaService.generateCompleteContent(
|
||||
productImageUrl, productName, originalPrice, finalPrice, style
|
||||
);
|
||||
|
||||
response.put("code", 200);
|
||||
response.put("msg", "操作成功");
|
||||
response.put("data", result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成完整内容失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("msg", "生成完整内容失败: " + e.getMessage());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Double值
|
||||
*/
|
||||
private Double parseDouble(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Double) {
|
||||
return (Double) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
}
|
||||
try {
|
||||
return Double.parseDouble(value.toString());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package cn.van.business.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 小红书/抖音提示词模板配置Controller
|
||||
*
|
||||
* @author System
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/jarvis/social-media/prompt")
|
||||
public class SocialMediaPromptController {
|
||||
|
||||
@Autowired(required = false)
|
||||
private StringRedisTemplate redisTemplate;
|
||||
|
||||
// Redis Key 前缀
|
||||
private static final String REDIS_KEY_PREFIX = "social_media:prompt:";
|
||||
|
||||
// 模板键名列表
|
||||
private static final String[] TEMPLATE_KEYS = {
|
||||
"keywords",
|
||||
"content:xhs",
|
||||
"content:douyin",
|
||||
"content:both"
|
||||
};
|
||||
|
||||
// 模板说明
|
||||
private static final Map<String, String> TEMPLATE_DESCRIPTIONS = new HashMap<String, String>() {{
|
||||
put("keywords", "关键词提取提示词模板\n占位符:%s - 商品名称");
|
||||
put("content:xhs", "小红书文案生成提示词模板\n占位符:%s - 商品名称,%s - 价格信息,%s - 关键词信息");
|
||||
put("content:douyin", "抖音文案生成提示词模板\n占位符:%s - 商品名称,%s - 价格信息,%s - 关键词信息");
|
||||
put("content:both", "通用文案生成提示词模板\n占位符:%s - 商品名称,%s - 价格信息,%s - 关键词信息");
|
||||
}};
|
||||
|
||||
/**
|
||||
* 获取所有提示词模板
|
||||
*
|
||||
* GET /jarvis/social-media/prompt/list
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public JSONObject listTemplates() {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
Map<String, Object> templates = new HashMap<>();
|
||||
|
||||
for (String key : TEMPLATE_KEYS) {
|
||||
Map<String, Object> templateInfo = new HashMap<>();
|
||||
templateInfo.put("key", key);
|
||||
templateInfo.put("description", TEMPLATE_DESCRIPTIONS.get(key));
|
||||
|
||||
String template = getTemplateFromRedis(key);
|
||||
templateInfo.put("template", template);
|
||||
templateInfo.put("isDefault", template == null);
|
||||
|
||||
templates.put(key, templateInfo);
|
||||
}
|
||||
|
||||
response.put("code", 200);
|
||||
response.put("msg", "操作成功");
|
||||
response.put("data", templates);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取提示词模板列表失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("msg", "获取失败: " + e.getMessage());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个提示词模板
|
||||
*
|
||||
* GET /jarvis/social-media/prompt/{key}
|
||||
*/
|
||||
@GetMapping("/{key}")
|
||||
public JSONObject getTemplate(@PathVariable String key) {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
if (!isValidKey(key)) {
|
||||
response.put("code", 400);
|
||||
response.put("msg", "无效的模板键名");
|
||||
return response;
|
||||
}
|
||||
|
||||
String template = getTemplateFromRedis(key);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("key", key);
|
||||
data.put("description", TEMPLATE_DESCRIPTIONS.get(key));
|
||||
data.put("template", template);
|
||||
data.put("isDefault", template == null);
|
||||
|
||||
response.put("code", 200);
|
||||
response.put("msg", "操作成功");
|
||||
response.put("data", data);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取提示词模板失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("msg", "获取失败: " + e.getMessage());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存提示词模板
|
||||
*
|
||||
* POST /jarvis/social-media/prompt/save
|
||||
*
|
||||
* {
|
||||
* "key": "keywords",
|
||||
* "template": "提示词模板内容..."
|
||||
* }
|
||||
*/
|
||||
@PostMapping("/save")
|
||||
public JSONObject saveTemplate(@RequestBody Map<String, Object> request) {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
String key = (String) request.get("key");
|
||||
String template = (String) request.get("template");
|
||||
|
||||
if (!isValidKey(key)) {
|
||||
response.put("code", 400);
|
||||
response.put("msg", "无效的模板键名");
|
||||
return response;
|
||||
}
|
||||
|
||||
if (StrUtil.isBlank(template)) {
|
||||
response.put("code", 400);
|
||||
response.put("msg", "模板内容不能为空");
|
||||
return response;
|
||||
}
|
||||
|
||||
if (redisTemplate == null) {
|
||||
response.put("code", 500);
|
||||
response.put("msg", "Redis未配置,无法保存模板");
|
||||
return response;
|
||||
}
|
||||
|
||||
String redisKey = REDIS_KEY_PREFIX + key;
|
||||
redisTemplate.opsForValue().set(redisKey, template);
|
||||
|
||||
log.info("保存提示词模板成功: {}", key);
|
||||
|
||||
response.put("code", 200);
|
||||
response.put("msg", "保存成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("保存提示词模板失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("msg", "保存失败: " + e.getMessage());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除提示词模板(恢复默认)
|
||||
*
|
||||
* DELETE /jarvis/social-media/prompt/{key}
|
||||
*/
|
||||
@DeleteMapping("/{key}")
|
||||
public JSONObject deleteTemplate(@PathVariable String key) {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
if (!isValidKey(key)) {
|
||||
response.put("code", 400);
|
||||
response.put("msg", "无效的模板键名");
|
||||
return response;
|
||||
}
|
||||
|
||||
if (redisTemplate == null) {
|
||||
response.put("code", 500);
|
||||
response.put("msg", "Redis未配置,无法删除模板");
|
||||
return response;
|
||||
}
|
||||
|
||||
String redisKey = REDIS_KEY_PREFIX + key;
|
||||
redisTemplate.delete(redisKey);
|
||||
|
||||
log.info("删除提示词模板成功: {}", key);
|
||||
|
||||
response.put("code", 200);
|
||||
response.put("msg", "删除成功,已恢复默认模板");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("删除提示词模板失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("msg", "删除失败: " + e.getMessage());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Redis 获取模板
|
||||
*/
|
||||
private String getTemplateFromRedis(String key) {
|
||||
if (redisTemplate == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
String redisKey = REDIS_KEY_PREFIX + key;
|
||||
return redisTemplate.opsForValue().get(redisKey);
|
||||
} catch (Exception e) {
|
||||
log.warn("读取Redis模板失败: {}", key, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证模板键名是否有效
|
||||
*/
|
||||
private boolean isValidKey(String key) {
|
||||
if (StrUtil.isBlank(key)) {
|
||||
return false;
|
||||
}
|
||||
for (String validKey : TEMPLATE_KEYS) {
|
||||
if (validKey.equals(key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
package cn.van.business.controller.jd;
|
||||
|
||||
import cn.van.business.model.pl.TaobaoComment;
|
||||
import cn.van.business.repository.TaobaoCommentRepository;
|
||||
import cn.van.business.service.ImageConvertService;
|
||||
import cn.van.business.util.JDProductService;
|
||||
import cn.van.business.util.JDScheduleJob;
|
||||
import cn.van.business.util.JDUtil;
|
||||
import cn.van.business.repository.CommentRepository;
|
||||
import cn.van.business.model.pl.Comment;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.Random;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/jd")
|
||||
@Slf4j
|
||||
public class JDInnerController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(JDInnerController.class);
|
||||
|
||||
private static final String SKEY = "2192057370ef8140c201079969c956a3";
|
||||
|
||||
private boolean checkSkey(String provided) {
|
||||
return !SKEY.equals(provided);
|
||||
}
|
||||
|
||||
private final JDProductService jdProductService;
|
||||
private final JDUtil jdUtil;
|
||||
private final JDScheduleJob jdScheduleJob;
|
||||
private final CommentRepository commentRepository;
|
||||
private final TaobaoCommentRepository taobaoCommentRepository;
|
||||
private final ImageConvertService imageConvertService;
|
||||
|
||||
@Autowired
|
||||
public JDInnerController(JDProductService jdProductService, JDUtil jdUtil, JDScheduleJob jdScheduleJob, CommentRepository commentRepository, TaobaoCommentRepository taobaoCommentRepository, ImageConvertService imageConvertService) {
|
||||
this.jdProductService = jdProductService;
|
||||
this.jdUtil = jdUtil;
|
||||
this.jdScheduleJob = jdScheduleJob;
|
||||
this.commentRepository = commentRepository;
|
||||
this.taobaoCommentRepository = taobaoCommentRepository;
|
||||
this.imageConvertService = imageConvertService;
|
||||
}
|
||||
|
||||
@PostMapping("/generatePromotionContent")
|
||||
public Object generatePromotionContent(@RequestBody Map<String, Object> requestBody) {
|
||||
String skey = requestBody.get("skey") != null ? String.valueOf(requestBody.get("skey")) : null;
|
||||
String promotionContent = requestBody.get("promotionContent") != null ? String.valueOf(requestBody.get("promotionContent")) : null;
|
||||
logger.info("generatePromotionContent message: {}", promotionContent);
|
||||
if (checkSkey(skey)) {
|
||||
return error("invalid skey");
|
||||
}
|
||||
if (promotionContent == null || promotionContent.trim().isEmpty()) {
|
||||
return error("promotionContent is required");
|
||||
}
|
||||
JSONArray arr = jdProductService.generatePromotionContentAsJsonArray(promotionContent);
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论可选类型(型号)
|
||||
* 返回:[{name, value}],按 name 排序
|
||||
*/
|
||||
@GetMapping("/comment/types")
|
||||
public Object commentTypes(@RequestParam(value = "skey", required = false) String skey) {
|
||||
if (checkSkey(skey)) {
|
||||
return error("invalid skey");
|
||||
}
|
||||
try {
|
||||
HashMap<String, String> map = jdUtil.getProductTypeMap();
|
||||
List<JSONObject> list = map.entrySet().stream()
|
||||
.map(e -> {
|
||||
JSONObject o = new JSONObject();
|
||||
o.put("name", e.getKey());
|
||||
o.put("value", e.getValue());
|
||||
return o;
|
||||
})
|
||||
.sorted(Comparator.comparing(o -> o.getString("name")))
|
||||
.collect(Collectors.toList());
|
||||
return list;
|
||||
} catch (Exception e) {
|
||||
logger.error("commentTypes error", e);
|
||||
return error("commentTypes failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成评论(严格按照JDUtil.generateComment的业务流程)
|
||||
* 优先使用京东评论,若无可用评论则尝试从淘宝获取
|
||||
* 入参:{ skey, productType }
|
||||
* 返回:{ productType, list: [ { commentText, images:[] } ] }
|
||||
*/
|
||||
@PostMapping("/comment/generate")
|
||||
public Object commentGenerate(@RequestBody Map<String, Object> body) {
|
||||
String skey = body.get("skey") != null ? String.valueOf(body.get("skey")) : null;
|
||||
if (checkSkey(skey)) {
|
||||
return error("invalid skey");
|
||||
}
|
||||
String productType = body.get("productType") != null ? String.valueOf(body.get("productType")) : null;
|
||||
if (productType == null || productType.trim().isEmpty()) {
|
||||
return error("productType is required");
|
||||
}
|
||||
try {
|
||||
// 评论统计变量初始化
|
||||
int allCommentCount = 0;
|
||||
int usedCommentCount = 0;
|
||||
int canUseCommentCount = 0;
|
||||
int addCommentCount = 0;
|
||||
int allTbCommentCount = 0;
|
||||
int usedTbCommentCount = 0;
|
||||
int canUseTbCommentCount = 0;
|
||||
boolean isTb = false;
|
||||
|
||||
HashMap<String, String> map = jdUtil.getProductTypeMap();
|
||||
String productId = map.get(productType);
|
||||
if (productId == null || productId.trim().isEmpty()) {
|
||||
return error("unknown productType");
|
||||
}
|
||||
|
||||
// 获取本地可用的京东评论并统计
|
||||
// 查询未使用的评论:isUse != 1(即 isUse = 0 或 isUse is null)
|
||||
List<Comment> availableComments = commentRepository.findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(productId, 1);
|
||||
// 查询已使用的评论:isUse != 0(即 isUse = 1)
|
||||
List<Comment> usedComments = commentRepository.findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(productId, 0);
|
||||
|
||||
canUseCommentCount = availableComments.size();
|
||||
usedCommentCount = usedComments.size();
|
||||
allCommentCount = canUseCommentCount + usedCommentCount;
|
||||
|
||||
// 获取淘宝评论统计信息
|
||||
HashMap<String, String> tbMap = jdUtil.getProductTypeMapForTB();
|
||||
String taobaoProductId = tbMap.getOrDefault(productId, productId);
|
||||
// 查询未使用的淘宝评论:isUse != 1
|
||||
List<TaobaoComment> availableTbComments = taobaoCommentRepository.findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(taobaoProductId, 1);
|
||||
// 查询已使用的淘宝评论:isUse != 0
|
||||
List<TaobaoComment> usedTbComments = taobaoCommentRepository.findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(taobaoProductId, 0);
|
||||
canUseTbCommentCount = availableTbComments.size();
|
||||
usedTbCommentCount = usedTbComments.size();
|
||||
allTbCommentCount = canUseTbCommentCount + usedTbCommentCount;
|
||||
|
||||
Comment commentToUse = null;
|
||||
|
||||
// 按优先级获取评论:
|
||||
// 1️⃣ 先尝试使用未使用过的京东评论
|
||||
if (!availableComments.isEmpty()) {
|
||||
Collections.shuffle(availableComments);
|
||||
commentToUse = availableComments.get(0);
|
||||
logger.info("使用未使用过的京东评论");
|
||||
}
|
||||
// 2️⃣ 尝试使用未使用过的淘宝评论
|
||||
else {
|
||||
String taobaoProductIdMap = tbMap.getOrDefault(productId, null);
|
||||
if (taobaoProductIdMap != null && !taobaoProductIdMap.isEmpty()) {
|
||||
logger.info("发现淘宝映射ID,尝试获取未使用过的淘宝评论");
|
||||
Comment taobaoComment = generateTaobaoComment(productType, false);
|
||||
if (taobaoComment != null) {
|
||||
commentToUse = taobaoComment;
|
||||
isTb = true;
|
||||
logger.info("使用未使用过的淘宝评论");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3️⃣ 尝试使用已使用过的评论(随机从京东和淘宝中选择)
|
||||
if (commentToUse == null) {
|
||||
// 准备候选评论列表
|
||||
List<Comment> candidateComments = new ArrayList<>();
|
||||
List<String> candidateSources = new ArrayList<>(); // 记录来源,用于标识是京东还是淘宝
|
||||
|
||||
// 添加已使用过的京东评论
|
||||
if (!usedComments.isEmpty()) {
|
||||
Collections.shuffle(usedComments);
|
||||
candidateComments.add(usedComments.get(0));
|
||||
candidateSources.add("JD");
|
||||
logger.info("已添加已使用过的京东评论到候选列表");
|
||||
}
|
||||
|
||||
// 添加已使用过的淘宝评论
|
||||
String taobaoProductIdMap = tbMap.getOrDefault(productId, null);
|
||||
if (taobaoProductIdMap != null && !taobaoProductIdMap.isEmpty()) {
|
||||
Comment taobaoComment = generateTaobaoComment(productType, true);
|
||||
if (taobaoComment != null) {
|
||||
candidateComments.add(taobaoComment);
|
||||
candidateSources.add("TB");
|
||||
logger.info("已添加已使用过的淘宝评论到候选列表");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果候选列表不为空,随机选择
|
||||
if (!candidateComments.isEmpty()) {
|
||||
Random random = new Random();
|
||||
int selectedIndex = random.nextInt(candidateComments.size());
|
||||
commentToUse = candidateComments.get(selectedIndex);
|
||||
String selectedSource = candidateSources.get(selectedIndex);
|
||||
|
||||
if ("TB".equals(selectedSource)) {
|
||||
isTb = true;
|
||||
logger.info("随机选择:使用已使用过的淘宝评论");
|
||||
} else {
|
||||
logger.info("随机选择:使用已使用过的京东评论");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (commentToUse == null) {
|
||||
return error("no comment available");
|
||||
}
|
||||
|
||||
JSONObject item = new JSONObject();
|
||||
item.put("commentText", commentToUse.getCommentText());
|
||||
// 解析图片URL并转换webp格式为jpg
|
||||
List<String> imageUrls = parsePictureUrls(commentToUse.getPictureUrls());
|
||||
List<String> convertedImageUrls = imageConvertService.convertImageUrls(imageUrls);
|
||||
item.put("images", convertedImageUrls);
|
||||
|
||||
JSONArray arr = new JSONArray();
|
||||
arr.add(item);
|
||||
JSONObject resp = new JSONObject();
|
||||
resp.put("productType", productType);
|
||||
resp.put("list", arr);
|
||||
|
||||
// 添加评论统计信息到响应中
|
||||
JSONObject stats = new JSONObject();
|
||||
if (!isTb) {
|
||||
stats.put("source", "京东评论");
|
||||
stats.put("productType", productType);
|
||||
stats.put("newAdded", addCommentCount);
|
||||
stats.put("used", usedCommentCount);
|
||||
stats.put("available", canUseCommentCount);
|
||||
stats.put("total", allCommentCount);
|
||||
stats.put("statisticsText",
|
||||
"京东评论统计:\n" +
|
||||
"型号 " + productType + "\n" +
|
||||
"新增:" + addCommentCount + "\n" +
|
||||
"已使用:" + usedCommentCount + "\n" +
|
||||
"可用:" + canUseCommentCount + "\n" +
|
||||
"总数:" + allCommentCount);
|
||||
} else {
|
||||
stats.put("source", "淘宝评论");
|
||||
stats.put("productType", productType);
|
||||
stats.put("used", usedTbCommentCount);
|
||||
stats.put("available", canUseTbCommentCount);
|
||||
stats.put("total", allTbCommentCount);
|
||||
stats.put("statisticsText",
|
||||
"淘宝评论统计:\n" +
|
||||
"型号 " + productType + "\n" +
|
||||
"已使用:" + usedTbCommentCount + "\n" +
|
||||
"可用:" + canUseTbCommentCount + "\n" +
|
||||
"总数:" + allTbCommentCount);
|
||||
}
|
||||
resp.put("statistics", stats);
|
||||
|
||||
// 标记为已使用(仅当原本未使用且不是淘宝评论时)
|
||||
try {
|
||||
if (!isTb && commentToUse.getId() != null && (commentToUse.getIsUse() == null || commentToUse.getIsUse() == 0)) {
|
||||
commentToUse.setIsUse(1);
|
||||
commentRepository.save(commentToUse);
|
||||
}
|
||||
} catch (Exception ignore) {}
|
||||
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
logger.error("commentGenerate error", e);
|
||||
return error("commentGenerate failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从淘宝评论中生成Comment对象(参考JDUtil.generateTaobaoComment)
|
||||
* @param productType 商品类型
|
||||
* @param includeUsed 是否包含已使用的评论(true=获取已使用的,false=获取未使用的)
|
||||
*/
|
||||
private Comment generateTaobaoComment(String productType, boolean includeUsed) {
|
||||
HashMap<String, String> map = jdUtil.getProductTypeMap(); // 加载京东的 productTypeMap
|
||||
HashMap<String, String> tbMap = jdUtil.getProductTypeMapForTB(); // 加载淘宝的 productTypeMapTB
|
||||
|
||||
String product_id = map.get(productType); // 先查京东SKU
|
||||
|
||||
if (product_id == null) {
|
||||
logger.info("未找到对应的京东商品ID:{}", productType);
|
||||
return null;
|
||||
}
|
||||
|
||||
// ✅ 在这里进行淘宝的 product_id 映射转换
|
||||
String taobaoProductId = tbMap.getOrDefault(product_id, product_id);
|
||||
|
||||
// 根据 includeUsed 参数查询不同的淘宝评论
|
||||
List<TaobaoComment> taobaoComments;
|
||||
if (includeUsed) {
|
||||
// 查询已使用的评论(isUse = 1)
|
||||
taobaoComments = taobaoCommentRepository.findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(taobaoProductId, 0);
|
||||
} else {
|
||||
// 查询未使用的评论(isUse != 1,即0或null)
|
||||
taobaoComments = taobaoCommentRepository.findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(taobaoProductId, 1);
|
||||
}
|
||||
|
||||
logger.info("taobaoComments.size() {} (includeUsed={})", taobaoComments.size(), includeUsed);
|
||||
|
||||
if (!taobaoComments.isEmpty()) {
|
||||
Collections.shuffle(taobaoComments);
|
||||
TaobaoComment selected = taobaoComments.get(0);
|
||||
// 将淘宝评论转换为京东评论返回
|
||||
Comment comment = new Comment();
|
||||
comment.setCommentText(selected.getCommentText());
|
||||
String pictureUrls = selected.getPictureUrls();
|
||||
if (pictureUrls != null) {
|
||||
pictureUrls = pictureUrls.replace("//img.", "https://img.");
|
||||
}
|
||||
comment.setPictureUrls(pictureUrls);
|
||||
comment.setCommentId(selected.getCommentId());
|
||||
comment.setProductId(product_id);
|
||||
comment.setUserName(selected.getUserName());
|
||||
comment.setCreatedAt(selected.getCreatedAt());
|
||||
|
||||
// 只在获取未使用的评论时才标记为已使用
|
||||
if (!includeUsed) {
|
||||
selected.setIsUse(1);
|
||||
taobaoCommentRepository.save(selected);
|
||||
}
|
||||
|
||||
// 返回京东评论
|
||||
return comment;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> parsePictureUrls(String raw) {
|
||||
if (raw == null || raw.trim().isEmpty()) return Collections.emptyList();
|
||||
try {
|
||||
Object parsed = com.alibaba.fastjson2.JSON.parse(raw);
|
||||
if (parsed instanceof JSONArray) {
|
||||
JSONArray ja = (JSONArray) parsed;
|
||||
List<String> list = new ArrayList<>();
|
||||
for (int i = 0; i < ja.size(); i++) {
|
||||
Object v = ja.get(i);
|
||||
if (v != null) list.add(String.valueOf(v));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
} catch (Exception ignore) {}
|
||||
// 非 JSON,按逗号或空白分隔
|
||||
return Arrays.stream(raw.split("[\n,\t ]+"))
|
||||
.filter(s -> s != null && !s.trim().isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/createGiftCoupon")
|
||||
public Object createGiftCoupon(@RequestBody Map<String, Object> body) {
|
||||
String skey = body.get("skey") != null ? String.valueOf(body.get("skey")) : null;
|
||||
if (checkSkey(skey)) {
|
||||
return error("invalid skey");
|
||||
}
|
||||
String skuId = body.get("skuId") != null ? String.valueOf(body.get("skuId")) : null;
|
||||
String materialUrl = body.get("materialUrl") != null ? String.valueOf(body.get("materialUrl")) : null;
|
||||
String owner = body.get("owner") != null ? String.valueOf(body.get("owner")) : "g";
|
||||
String skuName = body.get("skuName") != null ? String.valueOf(body.get("skuName")) : "";
|
||||
double amount = parseDouble(body.get("amount"), -1);
|
||||
int quantity = parseInt(body.get("quantity"), -1);
|
||||
|
||||
String idOrUrl = skuId != null && !skuId.trim().isEmpty() ? skuId : materialUrl;
|
||||
if (idOrUrl == null || idOrUrl.trim().isEmpty()) {
|
||||
return error("skuId or materialUrl is required");
|
||||
}
|
||||
if (amount <= 0 || quantity <= 0) {
|
||||
return error("amount and quantity must be positive");
|
||||
}
|
||||
|
||||
try {
|
||||
String giftKey = jdProductService.createGiftCoupon(idOrUrl, amount, quantity, owner, skuName);
|
||||
|
||||
// 如果giftKey为null,返回错误而不是成功响应
|
||||
if (giftKey == null || giftKey.trim().isEmpty()) {
|
||||
String errorDetail = String.format("礼金创建失败,giftCouponKey为null。参数: idOrUrl=%s, amount=%.2f, quantity=%d, owner=%s, skuName=%s。可能原因:商品不支持创建礼金、商品类型错误、京东API调用失败。请查看JD项目日志获取详细信息。",
|
||||
idOrUrl, amount, quantity, owner, skuName);
|
||||
logger.error("礼金创建失败 - giftKey为null, {}", errorDetail);
|
||||
return error(errorDetail);
|
||||
}
|
||||
|
||||
// 创建成功,保存到Redis
|
||||
jdProductService.saveGiftCouponToRedis(idOrUrl, giftKey, skuName, owner);
|
||||
logger.info("礼金创建成功 - giftKey={}, idOrUrl={}, owner={}, amount={}, quantity={}", giftKey, idOrUrl, owner, amount, quantity);
|
||||
|
||||
JSONObject resp = new JSONObject();
|
||||
resp.put("giftCouponKey", giftKey);
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
logger.error("createGiftCoupon error", e);
|
||||
return error("createGiftCoupon failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/transfer")
|
||||
public Object transfer(@RequestBody Map<String, Object> body) {
|
||||
String skey = body.get("skey") != null ? String.valueOf(body.get("skey")) : null;
|
||||
if (checkSkey(skey)) {
|
||||
return error("invalid skey");
|
||||
}
|
||||
String materialUrl = body.get("materialUrl") != null ? String.valueOf(body.get("materialUrl")) : null;
|
||||
String giftCouponKey = body.get("giftCouponKey") != null ? String.valueOf(body.get("giftCouponKey")) : null;
|
||||
if (materialUrl == null || materialUrl.trim().isEmpty()) {
|
||||
return error("materialUrl is required");
|
||||
}
|
||||
try {
|
||||
String shortUrl = jdProductService.transfer(materialUrl, giftCouponKey);
|
||||
JSONObject resp = new JSONObject();
|
||||
resp.put("shortURL", shortUrl);
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
logger.error("transfer error", e);
|
||||
return error("transfer failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建礼金券并生成包含礼金的推广链接
|
||||
* 入参:{ skey, skuId/materialUrl, amount, quantity, batchSize, owner, skuName }
|
||||
* 返回:{ results: [ {index, success, giftCouponKey, shortURL, error} ], total, successCount, failCount }
|
||||
*/
|
||||
@PostMapping("/batchCreateGiftCoupons")
|
||||
public Object batchCreateGiftCoupons(@RequestBody Map<String, Object> body) {
|
||||
String skey = body.get("skey") != null ? String.valueOf(body.get("skey")) : null;
|
||||
if (checkSkey(skey)) {
|
||||
return error("invalid skey");
|
||||
}
|
||||
|
||||
String skuId = body.get("skuId") != null ? String.valueOf(body.get("skuId")) : null;
|
||||
String materialUrl = body.get("materialUrl") != null ? String.valueOf(body.get("materialUrl")) : null;
|
||||
String owner = body.get("owner") != null ? String.valueOf(body.get("owner")) : "g";
|
||||
String skuName = body.get("skuName") != null ? String.valueOf(body.get("skuName")) : "";
|
||||
double amount = parseDouble(body.get("amount"), 1.8);
|
||||
int quantity = parseInt(body.get("quantity"), 1);
|
||||
int batchSize = parseInt(body.get("batchSize"), 15);
|
||||
|
||||
String idOrUrl = skuId != null && !skuId.trim().isEmpty() ? skuId : materialUrl;
|
||||
if (idOrUrl == null || idOrUrl.trim().isEmpty()) {
|
||||
return error("skuId or materialUrl is required");
|
||||
}
|
||||
if (amount <= 0 || quantity <= 0) {
|
||||
return error("amount and quantity must be positive");
|
||||
}
|
||||
if (batchSize <= 0 || batchSize > 100) {
|
||||
return error("batchSize must be between 1 and 100");
|
||||
}
|
||||
|
||||
logger.info("批量创建礼金券请求 - idOrUrl={}, amount={}, quantity={}, batchSize={}, owner={}, skuName={}",
|
||||
idOrUrl, amount, quantity, batchSize, owner, skuName);
|
||||
|
||||
try {
|
||||
List<Map<String, Object>> results = jdProductService.batchCreateGiftCouponsWithLinks(
|
||||
idOrUrl, amount, quantity, batchSize, owner, skuName);
|
||||
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
for (Map<String, Object> result : results) {
|
||||
if (Boolean.TRUE.equals(result.get("success"))) {
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject resp = new JSONObject();
|
||||
resp.put("results", results);
|
||||
resp.put("total", batchSize);
|
||||
resp.put("successCount", successCount);
|
||||
resp.put("failCount", failCount);
|
||||
|
||||
logger.info("批量创建礼金券完成 - 总数={}, 成功={}, 失败={}", batchSize, successCount, failCount);
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
logger.error("batchCreateGiftCoupons error", e);
|
||||
return error("batchCreateGiftCoupons failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动清理Redis中超过93天的旧数据
|
||||
* 请求参数:{ skey }
|
||||
* 返回:{ message, success }
|
||||
* 注意:请将skey放在请求Body中(JSON格式),不是Query参数
|
||||
*/
|
||||
@PostMapping("/cleanRedisData")
|
||||
public Object cleanRedisData(@RequestBody(required = false) Map<String, Object> body) {
|
||||
// 兼容处理:如果body为空,返回友好提示
|
||||
if (body == null || body.isEmpty()) {
|
||||
JSONObject tips = new JSONObject();
|
||||
tips.put("error", "请求Body不能为空");
|
||||
tips.put("tip", "请在Postman的Body标签中选择raw/JSON格式,并输入: {\"skey\": \"your_skey_here\"}");
|
||||
return tips;
|
||||
}
|
||||
|
||||
String skey = body.get("skey") != null ? String.valueOf(body.get("skey")) : null;
|
||||
if (checkSkey(skey)) {
|
||||
return error("invalid skey");
|
||||
}
|
||||
try {
|
||||
logger.info("手动触发Redis清理任务");
|
||||
jdScheduleJob.manualCleanOldRedisData();
|
||||
JSONObject resp = new JSONObject();
|
||||
resp.put("success", true);
|
||||
resp.put("message", "Redis清理任务已执行完成,详情请查看日志");
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
logger.error("cleanRedisData error", e);
|
||||
return error("cleanRedisData failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static JSONObject error(String msg) {
|
||||
JSONObject o = new JSONObject();
|
||||
o.put("error", msg);
|
||||
return o;
|
||||
}
|
||||
|
||||
private static int parseInt(Object o, int def) {
|
||||
try { return o == null ? def : Integer.parseInt(String.valueOf(o)); } catch (Exception e) { return def; }
|
||||
}
|
||||
|
||||
private static double parseDouble(Object o, double def) {
|
||||
try { return o == null ? def : Double.parseDouble(String.valueOf(o)); } catch (Exception e) { return def; }
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package cn.van.business.controller.jd;
|
||||
|
||||
import cn.van.business.util.JDUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2024/11/7 13:39
|
||||
* @description:
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("order")
|
||||
public class OrderController {
|
||||
|
||||
public static String TOKEN = "cc0313";
|
||||
@Resource
|
||||
private JDUtils jdUtils;
|
||||
|
||||
public boolean checkToken (String token){
|
||||
return TOKEN.equals(token);
|
||||
}
|
||||
|
||||
@RequestMapping("/refreshHistory")
|
||||
@ResponseBody
|
||||
public String refreshHistory(String token) throws Exception {
|
||||
if (checkToken(token)) {
|
||||
|
||||
jdUtils.fetchHistoricalOrders();
|
||||
|
||||
}
|
||||
return "OK";
|
||||
}
|
||||
|
||||
}
|
||||
89
src/main/java/cn/van/business/enums/GroupType.java
Normal file
89
src/main/java/cn/van/business/enums/GroupType.java
Normal file
@@ -0,0 +1,89 @@
|
||||
package cn.van.business.enums;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2023/12/19 0019 上午 10:27
|
||||
* @description:
|
||||
*/
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public enum GroupType implements IEnum {
|
||||
|
||||
/**
|
||||
* 类型,1管理群,2评论群,3线报来源群,4线报推送群
|
||||
* */
|
||||
MANAGEMENT(1, "管理群"),
|
||||
COMMENT(2, "评论群"),
|
||||
LINE_REPORT_SOURCE(3, "线报来源群"),
|
||||
LINE_REPORT_PUSH(4, "线报推送群");
|
||||
private final int key;
|
||||
|
||||
private final String name;
|
||||
|
||||
GroupType(int key, String name) {
|
||||
this.key = key;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static GroupType get(int key) {
|
||||
for (GroupType e : GroupType.values()) {
|
||||
if (e.getKey() == key) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getName(Integer key) {
|
||||
//if (Object.isNotEmpty(key)) {
|
||||
GroupType[] items = GroupType.values();
|
||||
for (GroupType item : items) {
|
||||
if (item.getKey() == key) {
|
||||
return item.getName();
|
||||
}
|
||||
}
|
||||
//}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static Map<String, String> getKeyVlue() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
GroupType[] items = GroupType.values();
|
||||
for (GroupType item : items) {
|
||||
map.put(item.getKey() + "", item.getName());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public static List<Map<String, Object>> getSelectItems() {
|
||||
List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
|
||||
GroupType[] items = GroupType.values();
|
||||
for (GroupType item : items) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("label", item.getName());
|
||||
map.put("value", item.getKey());
|
||||
result.add(map);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonValue
|
||||
public Integer getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.util.Map;
|
||||
* @create 2023/12/19 0019 下午 02:31
|
||||
* @description:
|
||||
*/
|
||||
public enum MsgTypeEnum implements IEnum {
|
||||
public enum MsgType implements IEnum {
|
||||
/**
|
||||
msgType :
|
||||
1|文本 3|图片 34|语音
|
||||
@@ -38,13 +38,13 @@ public enum MsgTypeEnum implements IEnum {
|
||||
|
||||
private final String name;
|
||||
|
||||
MsgTypeEnum(int key, String name) {
|
||||
MsgType(int key, String name) {
|
||||
this.key = key;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static String getName(Integer key) {
|
||||
for (MsgTypeEnum msgTypeEnum : MsgTypeEnum.values()) {
|
||||
for (MsgType msgTypeEnum : MsgType.values()) {
|
||||
if (msgTypeEnum.key == key) {
|
||||
return msgTypeEnum.name;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ public enum MsgTypeEnum implements IEnum {
|
||||
|
||||
public static Map<String, String> getKeyVlue() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (MsgTypeEnum msgTypeEnum : MsgTypeEnum.values()) {
|
||||
for (MsgType msgTypeEnum : MsgType.values()) {
|
||||
map.put(msgTypeEnum.key + "", msgTypeEnum.name);
|
||||
}
|
||||
return map;
|
||||
@@ -62,7 +62,7 @@ public enum MsgTypeEnum implements IEnum {
|
||||
|
||||
public static List<Map<String, Object>> getSelectItems() {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
for (MsgTypeEnum msgTypeEnum : MsgTypeEnum.values()) {
|
||||
for (MsgType msgTypeEnum : MsgType.values()) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("key", msgTypeEnum.key);
|
||||
map.put("value", msgTypeEnum.name);
|
||||
@@ -71,8 +71,8 @@ public enum MsgTypeEnum implements IEnum {
|
||||
return list;
|
||||
}
|
||||
|
||||
public static MsgTypeEnum get(int key) {
|
||||
for (MsgTypeEnum msgTypeEnum : MsgTypeEnum.values()) {
|
||||
public static MsgType get(int key) {
|
||||
for (MsgType msgTypeEnum : MsgType.values()) {
|
||||
if (msgTypeEnum.key == key) {
|
||||
return msgTypeEnum;
|
||||
}
|
||||
@@ -5,8 +5,7 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2024/11/9 下午3:08
|
||||
* @create 2024/11/9 下午3:08
|
||||
* @description:
|
||||
*/
|
||||
public class ValidCodeConverter {
|
||||
|
||||
@@ -44,7 +44,7 @@ public enum WXReqType {
|
||||
*/
|
||||
GET_WX_LIST("getWeChatList", "获取微信列表"),
|
||||
GET_WX_STATUS("checkWeChat", "微信状态检测"),
|
||||
SEND_TEXT_MESSAGE("sendText", "发送文本消息"),
|
||||
SEND_TEXT_MESSAGE("sendText2", "发送文本消息"),
|
||||
UPDATE_DOWNLOAD_IMAGE("Q0002", "修改下载图片"),
|
||||
GET_USER_INFO("Q0003", "获取个人信息"),
|
||||
QUERY_OBJECT_INFO("Q0004", "查询对象信"),
|
||||
@@ -53,7 +53,7 @@ public enum WXReqType {
|
||||
GET_MP_LIST("Q0007", "获取公众号列表"),
|
||||
GET_GROUP_MEMBER_LIST("Q0008", "获取群成员列表"),
|
||||
SEND_CHAT_RECORD("Q0009", "发送聊天记录"),
|
||||
SEND_IMAGE("Q0010", "发送图片"),
|
||||
SEND_IMAGE("sendImage", "发送图片"),
|
||||
SEND_LOCAL_FILE("Q0011", "发送本地文件"),
|
||||
SEND_SHARE_LINK("Q0012", "发送分享链接"),
|
||||
SEND_MINIPROGRAM("Q0013", "发送小程序"),
|
||||
|
||||
61
src/main/java/cn/van/business/model/ApiResponse.java
Normal file
61
src/main/java/cn/van/business/model/ApiResponse.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package cn.van.business.model;
|
||||
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
|
||||
public class ApiResponse<T> {
|
||||
// 状态码(建议使用 HTTP 状态码或自定义业务状态码)
|
||||
private int code;
|
||||
// 返回消息
|
||||
private String message;
|
||||
// 业务数据(使用泛型支持不同类型)
|
||||
private T data;
|
||||
// 时间戳(毫秒)
|
||||
@JSONField(name = "timestamp")
|
||||
private long timestamp;
|
||||
|
||||
// 常用状态码常量
|
||||
public static final int CODE_SUCCESS = 200;
|
||||
public static final int CODE_BAD_REQUEST = 400;
|
||||
public static final int CODE_UNAUTHORIZED = 401;
|
||||
public static final int CODE_SERVER_ERROR = 500;
|
||||
|
||||
// 构造方法私有化,通过静态工厂方法创建
|
||||
private ApiResponse(int code, String message, T data) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// 成功响应(带数据)
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return new ApiResponse<>(CODE_SUCCESS, "success", data);
|
||||
}
|
||||
|
||||
// 成功响应(无数据)
|
||||
public static ApiResponse<?> success() {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
// 错误响应(自定义状态码和消息)
|
||||
public static ApiResponse<?> error(int code, String message) {
|
||||
return new ApiResponse<>(code, message, null);
|
||||
}
|
||||
|
||||
// 快速错误响应(预定义类型)
|
||||
public static ApiResponse<?> badRequest() {
|
||||
return error(CODE_BAD_REQUEST, "Bad Request");
|
||||
}
|
||||
|
||||
public static ApiResponse<?> unauthorized() {
|
||||
return error(CODE_UNAUTHORIZED, "Unauthorized");
|
||||
}
|
||||
|
||||
// getters 需要保留用于序列化
|
||||
public int getCode() { return code; }
|
||||
public String getMessage() { return message; }
|
||||
public T getData() { return data; }
|
||||
public long getTimestamp() { return timestamp; }
|
||||
|
||||
// setters 可省略或设为私有,根据实际需求
|
||||
}
|
||||
36
src/main/java/cn/van/business/model/cj/XbGroup.java
Normal file
36
src/main/java/cn/van/business/model/cj/XbGroup.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package cn.van.business.model.cj;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "xb_group")
|
||||
public class XbGroup {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id", nullable = false)
|
||||
private Integer id;
|
||||
|
||||
@Size(max = 255)
|
||||
@Column(name = "name", length = 255)
|
||||
private String name;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "wxid", length = 100)
|
||||
private String wxid;
|
||||
|
||||
@Column(name = "is_active")
|
||||
private Boolean active;
|
||||
|
||||
@Column(name = "create_date")
|
||||
private LocalDateTime createDate;
|
||||
|
||||
@Column(name = "update_date")
|
||||
private LocalDateTime updateDate;
|
||||
}
|
||||
46
src/main/java/cn/van/business/model/cj/XbMessage.java
Normal file
46
src/main/java/cn/van/business/model/cj/XbMessage.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package cn.van.business.model.cj;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "xb_message")
|
||||
public class XbMessage {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id", nullable = false)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "create_date")
|
||||
private LocalDateTime createDate;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "skuid", length = 100)
|
||||
private String skuid;
|
||||
|
||||
@Lob
|
||||
@Column(name = "tip_original_message")
|
||||
private String tipOriginalMessage;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "from_wxid", length = 100)
|
||||
private String fromWxid;
|
||||
|
||||
@Size(max = 1024)
|
||||
@Column(name = "first_line", length = 1024)
|
||||
private String firstLine;
|
||||
|
||||
@Size(max = 1024)
|
||||
@Column(name = "first_sku_name", length = 1024)
|
||||
private String firstSkuName;
|
||||
|
||||
@Column(name = "first_price")
|
||||
private Double firstPrice;
|
||||
|
||||
}
|
||||
71
src/main/java/cn/van/business/model/cj/XbMessageItem.java
Normal file
71
src/main/java/cn/van/business/model/cj/XbMessageItem.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package cn.van.business.model.cj;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "xb_message_item")
|
||||
public class XbMessageItem {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id", nullable = false)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "create_date")
|
||||
private LocalDateTime createDate;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "skuid", length = 100)
|
||||
private String skuid;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "xb_message_id", length = 100)
|
||||
private String xbMessageId;
|
||||
|
||||
@Lob
|
||||
@Column(name = "json_query_result")
|
||||
private String jsonQueryResult;
|
||||
|
||||
@Lob
|
||||
@Column(name = "json_coupon_list")
|
||||
private String jsonCouponList;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "spuid", length = 100)
|
||||
private String spuid;
|
||||
|
||||
@Lob
|
||||
@Column(name = "sku_name")
|
||||
private String skuName;
|
||||
|
||||
@Lob
|
||||
@Column(name = "json_shop_info")
|
||||
private String jsonShopInfo;
|
||||
|
||||
@Lob
|
||||
@Column(name = "price_info")
|
||||
private String priceInfo;
|
||||
|
||||
@Lob
|
||||
@Column(name = "json_commission_info")
|
||||
private String jsonCommissionInfo;
|
||||
|
||||
@Lob
|
||||
@Column(name = "json_image_list")
|
||||
private String jsonImageList;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "owner", length = 100)
|
||||
private String owner;
|
||||
|
||||
@Lob
|
||||
@Column(name = "json_category_info")
|
||||
private String jsonCategoryInfo;
|
||||
|
||||
}
|
||||
@@ -2,8 +2,6 @@ package cn.van.business.model.jd;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package cn.van.business.model.jd;
|
||||
|
||||
import javax.persistence.*;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
|
||||
34
src/main/java/cn/van/business/model/jd/JDOrder.java
Normal file
34
src/main/java/cn/van/business/model/jd/JDOrder.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package cn.van.business.model.jd;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Entity
|
||||
@Table(name = "jd_order")
|
||||
@Data
|
||||
public class JDOrder {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String remark; // 单据备注(如日期编号)
|
||||
private String distributionMark; // 分销标记
|
||||
private String modelNumber; // 型号
|
||||
private String link; // 链接
|
||||
private Double paymentAmount; // 下单付款金额
|
||||
private Double rebateAmount; // 后返金额
|
||||
private String address; // 地址
|
||||
private String logisticsLink; // 物流链接
|
||||
private String orderId; // 订单号
|
||||
private String buyer; // 下单人
|
||||
private Date orderTime; // 下单时间
|
||||
private String status;
|
||||
|
||||
@Column(updatable = false, insertable = false, columnDefinition = "DATETIME DEFAULT CURRENT_TIMESTAMP")
|
||||
private Date createTime;
|
||||
|
||||
@Column(columnDefinition = "DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -6,9 +6,9 @@ package cn.van.business.model.jd;
|
||||
* @create 2024/11/6 10:52
|
||||
* @description:
|
||||
*/
|
||||
import com.jd.open.api.sdk.domain.kplunion.OrderService.response.query.GoodsInfo;
|
||||
|
||||
import javax.persistence.*;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Entity
|
||||
@@ -164,9 +164,9 @@ public class OrderRow {
|
||||
@Column(name = "rid")
|
||||
private Long rid;
|
||||
|
||||
@OneToOne(cascade = CascadeType.ALL)
|
||||
@JoinColumn(name = "goods_info_id", referencedColumnName = "id")
|
||||
private GoodsInfoVO goodsInfo;
|
||||
//@OneToOne(cascade = CascadeType.ALL)
|
||||
//@JoinColumn(name = "goods_info_id", referencedColumnName = "id")
|
||||
//private GoodsInfoVO goodsInfo;
|
||||
|
||||
|
||||
@Column(name = "express_status")
|
||||
@@ -548,13 +548,13 @@ public class OrderRow {
|
||||
this.rid = rid;
|
||||
}
|
||||
|
||||
public GoodsInfoVO getGoodsInfo() {
|
||||
return goodsInfo;
|
||||
}
|
||||
|
||||
public void setGoodsInfo(GoodsInfoVO goodsInfo) {
|
||||
this.goodsInfo = goodsInfo;
|
||||
}
|
||||
//public GoodsInfoVO getGoodsInfo() {
|
||||
// return goodsInfo;
|
||||
//}
|
||||
//
|
||||
//public void setGoodsInfo(GoodsInfoVO goodsInfo) {
|
||||
// this.goodsInfo = goodsInfo;
|
||||
//}
|
||||
|
||||
public Integer getExpressStatus() {
|
||||
return expressStatus;
|
||||
|
||||
99
src/main/java/cn/van/business/model/jd/ProductOrder.java
Normal file
99
src/main/java/cn/van/business/model/jd/ProductOrder.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package cn.van.business.model.jd;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 实体类,用于存储商品订单信息。
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "product_order")
|
||||
@Setter
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ProductOrder {
|
||||
|
||||
/**
|
||||
* 主键,自增ID。
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 商品名称。
|
||||
*/
|
||||
@Column(name = "sku_name")
|
||||
private String skuName;
|
||||
|
||||
/**
|
||||
* 商品类型。
|
||||
*/
|
||||
@Column(name = "sku_type")
|
||||
private Integer skuType;
|
||||
|
||||
/**
|
||||
* 订单号。
|
||||
*/
|
||||
@Column(name = "order_id")
|
||||
private String orderId;
|
||||
|
||||
/**
|
||||
* 下单时间。
|
||||
*/
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "order_time")
|
||||
private Date orderTime;
|
||||
|
||||
/**
|
||||
* 下单账号。
|
||||
*/
|
||||
@Column(name = "order_account")
|
||||
private String orderAccount;
|
||||
|
||||
/**
|
||||
* 是否晒图登记。
|
||||
*/
|
||||
@Column(name = "is_reviewed")
|
||||
private Boolean isReviewed;
|
||||
|
||||
/**
|
||||
* 晒图时间。
|
||||
*/
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "review_time")
|
||||
private Date reviewTime;
|
||||
|
||||
/**
|
||||
* 是否返现到账。
|
||||
*/
|
||||
@Column(name = "is_cashback_received")
|
||||
private Boolean isCashbackReceived;
|
||||
|
||||
/**
|
||||
* 到账时间。
|
||||
*/
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "cashback_time")
|
||||
private Date cashbackTime;
|
||||
//收货信息
|
||||
@Column(name = "recipient_name")
|
||||
private String recipientName;
|
||||
@Column(name = "recipient_phone")
|
||||
private String recipientPhone;
|
||||
@Column(name = "recipient_address")
|
||||
private String recipientAddress;
|
||||
// 谁的单
|
||||
@Column(name = "who_order")
|
||||
private String whoOrder;
|
||||
@Column(name = "from_wxid")
|
||||
private String fromWxid;
|
||||
|
||||
}
|
||||
30
src/main/java/cn/van/business/model/jd/SkuInfo.java
Normal file
30
src/main/java/cn/van/business/model/jd/SkuInfo.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package cn.van.business.model.jd;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/**
|
||||
* sku对应的商品信息
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sku_Info")
|
||||
public class SkuInfo {
|
||||
|
||||
/**
|
||||
* 主键,自增ID。
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 商品类型名称。
|
||||
*/
|
||||
@Column(name = "sku_name")
|
||||
private String skuName;
|
||||
|
||||
@Column(name = "sku_id")
|
||||
private String skuId;
|
||||
|
||||
|
||||
}
|
||||
55
src/main/java/cn/van/business/model/jd/SkuType.java
Normal file
55
src/main/java/cn/van/business/model/jd/SkuType.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package cn.van.business.model.jd;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/**
|
||||
* 实体类,用于存储商品类型信息。
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sku_type")
|
||||
public class SkuType {
|
||||
|
||||
/**
|
||||
* 主键,自增ID。
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 商品类型名称。
|
||||
*/
|
||||
@Column(name = "type_name", unique = true, nullable = false)
|
||||
private String typeName;
|
||||
|
||||
/**
|
||||
* 与该商品类型关联的商品ID集合。
|
||||
*/
|
||||
@Column(name = "sku_id")
|
||||
private String skuId;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTypeName() {
|
||||
return typeName;
|
||||
}
|
||||
|
||||
public void setTypeName(String typeName) {
|
||||
this.typeName = typeName;
|
||||
}
|
||||
|
||||
public String getSkuId() {
|
||||
return skuId;
|
||||
}
|
||||
|
||||
public void setSkuId(String skuId) {
|
||||
this.skuId = skuId;
|
||||
}
|
||||
}
|
||||
19
src/main/java/cn/van/business/model/jd/update-schema.sql
Normal file
19
src/main/java/cn/van/business/model/jd/update-schema.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE product_order
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT NOT NULL,
|
||||
sku_name VARCHAR(255) NULL,
|
||||
sku_type INT NULL,
|
||||
order_id VARCHAR(255) NULL,
|
||||
order_time datetime NULL,
|
||||
order_account VARCHAR(255) NULL,
|
||||
is_reviewed BIT(1) NULL,
|
||||
review_time datetime NULL,
|
||||
is_cashback_received BIT(1) NULL,
|
||||
cashback_time datetime NULL,
|
||||
recipient_name VARCHAR(255) NULL,
|
||||
recipient_phone VARCHAR(255) NULL,
|
||||
recipient_address VARCHAR(255) NULL,
|
||||
who_order VARCHAR(255) NULL,
|
||||
from_wxid VARCHAR(255) NULL,
|
||||
CONSTRAINT pk_product_order PRIMARY KEY (id)
|
||||
);
|
||||
50
src/main/java/cn/van/business/model/pl/Comment.java
Normal file
50
src/main/java/cn/van/business/model/pl/Comment.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package cn.van.business.model.pl;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2025/6/4
|
||||
* @description:评论实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "comments")
|
||||
@Data
|
||||
public class Comment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "product_id", nullable = false, columnDefinition = "TEXT")
|
||||
private String productId;
|
||||
|
||||
@Column(name = "user_name", columnDefinition = "TEXT")
|
||||
private String userName;
|
||||
|
||||
@Lob
|
||||
@Column(name = "comment_text")
|
||||
private String commentText;
|
||||
|
||||
@Column(name = "comment_id", length = 64, unique = true)
|
||||
private String commentId;
|
||||
|
||||
@Column(name = "picture_urls", columnDefinition = "TEXT")
|
||||
private String pictureUrls;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "comment_date")
|
||||
private LocalDateTime commentDate;
|
||||
|
||||
@Column(name = "is_use")
|
||||
private Integer isUse;
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
}
|
||||
67
src/main/java/cn/van/business/model/pl/ImageConversion.java
Normal file
67
src/main/java/cn/van/business/model/pl/ImageConversion.java
Normal file
@@ -0,0 +1,67 @@
|
||||
package cn.van.business.model.pl;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 图片转换记录实体类
|
||||
* 用于记录webp格式图片转换为jpg格式的映射关系
|
||||
*
|
||||
* @author System
|
||||
* @version 1.0
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "image_conversions", indexes = {
|
||||
@Index(name = "idx_original_url", columnList = "original_url"),
|
||||
@Index(name = "idx_converted_url", columnList = "converted_url")
|
||||
})
|
||||
@Data
|
||||
public class ImageConversion {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 原始webp图片URL
|
||||
*/
|
||||
@Column(name = "original_url", nullable = false, length = 2048, unique = true)
|
||||
private String originalUrl;
|
||||
|
||||
/**
|
||||
* 转换后的jpg图片URL或本地路径
|
||||
*/
|
||||
@Column(name = "converted_url", nullable = false, length = 2048)
|
||||
private String convertedUrl;
|
||||
|
||||
/**
|
||||
* 转换时间
|
||||
*/
|
||||
@Column(name = "converted_at", nullable = false)
|
||||
private LocalDateTime convertedAt;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
@Column(name = "file_size")
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
if (convertedAt == null) {
|
||||
convertedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
src/main/java/cn/van/business/model/pl/TaobaoComment.java
Normal file
48
src/main/java/cn/van/business/model/pl/TaobaoComment.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package cn.van.business.model.pl;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* @author Van
|
||||
* @version 1.0
|
||||
* @create 2025/7/6
|
||||
* @description 淘宝评论实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "taobao_comments")
|
||||
@Data
|
||||
public class TaobaoComment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "product_id", nullable = false, length = 255)
|
||||
private String productId;
|
||||
|
||||
@Column(name = "user_name", length = 255)
|
||||
private String userName;
|
||||
|
||||
@Lob
|
||||
@Column(name = "comment_text")
|
||||
private String commentText;
|
||||
|
||||
@Column(name = "comment_id", length = 255, unique = false)
|
||||
private String commentId;
|
||||
|
||||
@Lob
|
||||
@Column(name = "picture_urls")
|
||||
private String pictureUrls;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "comment_date", length = 255)
|
||||
private String commentDate;
|
||||
|
||||
@Column(name = "is_use", columnDefinition = "int default 0")
|
||||
private Integer isUse;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package cn.van.business.model.wx;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
|
||||
108
src/main/java/cn/van/business/model/wx/SuperAdmin.java
Normal file
108
src/main/java/cn/van/business/model/wx/SuperAdmin.java
Normal file
@@ -0,0 +1,108 @@
|
||||
package cn.van.business.model.wx;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.proxy.HibernateProxy;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 超级管理员实体类
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Entity
|
||||
@Table(name = "super_admin")
|
||||
public class SuperAdmin {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 微信ID
|
||||
*/
|
||||
@Column(name = "wxid", nullable = false, length = 64)
|
||||
private String wxid;
|
||||
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 50)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 联盟ID
|
||||
*/
|
||||
@Column(name = "union_id", nullable = false, length = 64)
|
||||
private String unionId;
|
||||
|
||||
/**
|
||||
* 应用密钥
|
||||
*/
|
||||
@Column(name = "app_key", nullable = false, length = 100)
|
||||
private String appKey;
|
||||
|
||||
/**
|
||||
* 秘密密钥
|
||||
*/
|
||||
@Column(name = "secret_key", nullable = false, length = 100)
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* 是否激活: 0-否, 1-是
|
||||
*/
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Integer isActive = 1;
|
||||
|
||||
/**
|
||||
* 接收人(企业微信用户ID,多个用逗号分隔)
|
||||
*/
|
||||
@Column(name = "touser", length = 500)
|
||||
private String touser;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private java.sql.Timestamp createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private java.sql.Timestamp updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = new java.sql.Timestamp(System.currentTimeMillis());
|
||||
updatedAt = new java.sql.Timestamp(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = new java.sql.Timestamp(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null) return false;
|
||||
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
|
||||
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
|
||||
if (thisEffectiveClass != oEffectiveClass) return false;
|
||||
SuperAdmin that = (SuperAdmin) o;
|
||||
return getId() != null && Objects.equals(getId(), that.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int hashCode() {
|
||||
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package cn.van.business.model.wx;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package cn.van.business.model.wx;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Getter
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package cn.van.business.model.wx;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Getter
|
||||
|
||||
77
src/main/java/cn/van/business/mq/MessageConsumerService.java
Normal file
77
src/main/java/cn/van/business/mq/MessageConsumerService.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package cn.van.business.mq;
|
||||
|
||||
import cn.van.business.util.WxtsUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.apache.rocketmq.spring.annotation.ConsumeMode;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2024/12/1 上午2:06
|
||||
* @description:
|
||||
*/
|
||||
@Service
|
||||
@RocketMQMessageListener(topic = "wx-message", consumerGroup = "${rocketmq.consumer.group}", nameServer = "${rocketmq.name-server}", consumeMode = ConsumeMode.ORDERLY, // 顺序消费模式
|
||||
consumeThreadNumber = 4)
|
||||
public class MessageConsumerService implements RocketMQListener<JSONObject> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MessageConsumerService.class);
|
||||
//private static final RateLimiter rateLimiter = RateLimiter.create(4, // 1 QPS
|
||||
// 1, // 预热期 5 秒
|
||||
// TimeUnit.SECONDS);
|
||||
|
||||
private final WxtsUtil wxtsUtil;
|
||||
|
||||
|
||||
@Autowired
|
||||
public MessageConsumerService(WxtsUtil wxtsUtil) {
|
||||
this.wxtsUtil = wxtsUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(JSONObject message) {
|
||||
try {
|
||||
logger.info("消费消息:{}", message);
|
||||
|
||||
// 解析消息类型和数据
|
||||
//String type = message.getString("type");
|
||||
JSONObject data = message.getJSONObject("data");
|
||||
//
|
||||
//if (data == null) {
|
||||
// logger.error("消息数据为空:{}", message);
|
||||
// return;
|
||||
//}
|
||||
//
|
||||
String wxid = data.getString("wxid");
|
||||
//if (wxid == null || wxid.isEmpty()) {
|
||||
// logger.error("消息缺少wxid字段:{}", message);
|
||||
// return;
|
||||
//}
|
||||
|
||||
// 根据消息类型调用不同的wxts接口
|
||||
|
||||
// 发送文本消息
|
||||
String content = data.getString("msg");
|
||||
Integer msgType = data.getInteger("msgType");
|
||||
String fromWxid = data.getString("fromWxid");
|
||||
Boolean hiddenTime = data.getBoolean("hiddenTime");
|
||||
String touser = data.getString("touser"); // 获取接收人参数
|
||||
|
||||
wxtsUtil.sendWxTextMessage(wxid, content, msgType, fromWxid, hiddenTime, touser);
|
||||
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("消息处理失败,原始消息:{}", message, e);
|
||||
wxtsUtil.sendNotify("系统异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
58
src/main/java/cn/van/business/mq/MessageProducerService.java
Normal file
58
src/main/java/cn/van/business/mq/MessageProducerService.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package cn.van.business.mq;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.apache.rocketmq.spring.support.RocketMQHeaders;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Service
|
||||
public class MessageProducerService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MessageProducerService.class);
|
||||
private static final String topic = "wx-message";
|
||||
private final RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
public MessageProducerService(RocketMQTemplate rocketMQTemplate
|
||||
) {
|
||||
|
||||
this.rocketMQTemplate = rocketMQTemplate;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
if (rocketMQTemplate == null) {
|
||||
throw new IllegalStateException("RocketMQTemplate not initialized! ");
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void sendMessage(JSONObject data) {
|
||||
// 消息结构校验
|
||||
if (!data.containsKey("type") || !data.containsKey("data")) {
|
||||
logger.error("非法消息格式:{}", data);
|
||||
throw new IllegalArgumentException("消息必须包含type和data字段");
|
||||
}
|
||||
// 新增校验
|
||||
if (!data.getJSONObject("data").containsKey("wxid")) {
|
||||
throw new IllegalArgumentException("消息必须包含wxid字段");
|
||||
}
|
||||
// 构建Spring Message
|
||||
Message<String> message = MessageBuilder
|
||||
.withPayload(new String(data.toJSONString().getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8))
|
||||
.setHeader(RocketMQHeaders.TAGS, "wx")
|
||||
.build();
|
||||
|
||||
// 发送消息
|
||||
rocketMQTemplate.send(topic, message);
|
||||
|
||||
logger.debug("消息已发送:{}", data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.van.business.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import cn.van.business.model.pl.Comment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2025/5/2 19:16
|
||||
* @description:评论数据访问层接口
|
||||
*/
|
||||
@Repository
|
||||
public interface CommentRepository extends JpaRepository<Comment, Integer> {
|
||||
|
||||
List<Comment> findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(String productId, Integer isUse);
|
||||
|
||||
List<Comment> findByProductIdAndPictureUrlsIsNotNull(String productId);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.van.business.repository;
|
||||
|
||||
import cn.van.business.model.pl.ImageConversion;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 图片转换记录Repository
|
||||
*
|
||||
* @author System
|
||||
*/
|
||||
@Repository
|
||||
public interface ImageConversionRepository extends JpaRepository<ImageConversion, Long> {
|
||||
|
||||
/**
|
||||
* 根据原始URL查找转换记录
|
||||
*
|
||||
* @param originalUrl 原始webp图片URL
|
||||
* @return 转换记录
|
||||
*/
|
||||
Optional<ImageConversion> findByOriginalUrl(String originalUrl);
|
||||
|
||||
/**
|
||||
* 检查是否已存在转换记录
|
||||
*
|
||||
* @param originalUrl 原始webp图片URL
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean existsByOriginalUrl(String originalUrl);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.van.business.repository;
|
||||
|
||||
import cn.van.business.model.jd.JDOrder;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JD订单数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface JDOrderRepository extends JpaRepository<JDOrder, Long> {
|
||||
/**
|
||||
* 根据订单号查询订单
|
||||
*
|
||||
* @param remark 订单号
|
||||
* @return JDOrder 实体
|
||||
*/
|
||||
JDOrder findByRemark(String remark);
|
||||
|
||||
/**
|
||||
* 根据下单人查询订单列表
|
||||
*
|
||||
* @param buyer 下单人
|
||||
* @return 订单列表
|
||||
*/
|
||||
List<JDOrder> findByBuyer(String buyer);
|
||||
|
||||
/**
|
||||
* 根据分销标记查询订单列表
|
||||
*
|
||||
* @param distributionMark 分销标记
|
||||
* @return 订单列表
|
||||
*/
|
||||
List<JDOrder> findByDistributionMark(String distributionMark);
|
||||
|
||||
List<JDOrder> findByOrderTimeBetween(Date startTime, Date endTime);
|
||||
|
||||
// 并且按日期降序排序
|
||||
List<JDOrder> findByAddressOrderByOrderTimeDesc(String address);
|
||||
|
||||
//select * from jd_order jo where jo.address like "%江苏%" or jo.remark like "%150%" or jo.order_id like "%320%" or jo.buyer like "兽"
|
||||
@Query("select jo from JDOrder jo where jo.address like %?1% or jo.remark like %?1% or jo.orderId like %?1% or jo.buyer like %?1%")
|
||||
List<JDOrder> searchOrder(String order);
|
||||
}
|
||||
@@ -9,8 +9,12 @@ package cn.van.business.repository;
|
||||
|
||||
import cn.van.business.model.jd.OrderRow;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@@ -27,6 +31,39 @@ public interface OrderRowRepository extends JpaRepository<OrderRow, String> {
|
||||
List<OrderRow> findByValidCode(int validCode);
|
||||
|
||||
// 查找 validCode != 15 或者 !=-1 的订单行 ,并且按orderTime 降序
|
||||
List<OrderRow> findByValidCodeNotInOrderByOrderTimeDesc(int[] validCodes);
|
||||
@Query("SELECT o FROM OrderRow o "
|
||||
+ "WHERE o.validCode NOT IN :validCodes "
|
||||
+ "AND o.unionId IN :unionIds "
|
||||
+ "ORDER BY o.orderTime DESC")
|
||||
List<OrderRow> findByValidCodeNotInAndUnionIdIn(
|
||||
@Param("validCodes") int[] validCodes,
|
||||
@Param("unionIds") List<Long> unionIds
|
||||
);
|
||||
|
||||
@Query("SELECT o FROM OrderRow o "
|
||||
+ "WHERE o.validCode NOT IN :validCodes "
|
||||
+ "AND o.orderTime >= :startDate "
|
||||
+ "ORDER BY o.orderTime DESC")
|
||||
List<OrderRow> findByValidCodeNotInAndOrderTimeAfter(
|
||||
@Param("validCodes") int[] validCodes,
|
||||
@Param("startDate") @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate
|
||||
);
|
||||
// 修改后支持传入unionId列表
|
||||
@Query("SELECT o FROM OrderRow o "
|
||||
+ "WHERE o.validCode NOT IN :validCodes "
|
||||
+ "AND o.skuId = :skuId "
|
||||
+ "AND o.unionId IN :unionIds "
|
||||
+ "ORDER BY o.orderTime DESC")
|
||||
List<OrderRow> findBySkuIdAndUnionIdIn(
|
||||
@Param("validCodes") int[] validCodes,
|
||||
@Param("skuId") long skuId,
|
||||
@Param("unionIds") List<Long> unionIds
|
||||
);
|
||||
|
||||
List<OrderRow> findByUnionId(long l);
|
||||
|
||||
//// 在OrderRowRepository中添加模糊查询方法
|
||||
//// 模糊查询收件人姓名或地址(包含分页)
|
||||
//@Query("SELECT o FROM OrderRow o WHERE " + "o.recipientName LIKE %:keyword% OR " + "o.address LIKE %:keyword% " + "ORDER BY o.orderTime DESC")
|
||||
//Page<OrderRow> searchByRecipientOrAddress(@Param("keyword") String keyword, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package cn.van.business.repository;
|
||||
|
||||
import cn.van.business.model.jd.ProductOrder;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ProductOrderRepository extends JpaRepository<ProductOrder, Long> {
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.van.business.repository;
|
||||
|
||||
import cn.van.business.model.wx.SuperAdmin;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface SuperAdminRepository extends JpaRepository<SuperAdmin, Long> {
|
||||
|
||||
/**
|
||||
* 根据微信ID查找超级管理员
|
||||
* @param wxid 微信ID
|
||||
* @return 超级管理员对象
|
||||
*/
|
||||
Optional<SuperAdmin> findByWxid(String wxid);
|
||||
|
||||
/**
|
||||
* 根据联盟ID查找超级管理员
|
||||
* @param unionId 联盟ID
|
||||
* @return 超级管理员对象
|
||||
*/
|
||||
Optional<SuperAdmin> findByUnionId(String unionId);
|
||||
|
||||
/**
|
||||
* 根据应用密钥查找超级管理员
|
||||
* @param appKey 应用密钥
|
||||
* @return 超级管理员对象
|
||||
*/
|
||||
Optional<SuperAdmin> findByAppKey(String appKey);
|
||||
|
||||
/**
|
||||
* 根据名称查找超级管理员列表
|
||||
* @param name 名称
|
||||
* @return 超级管理员列表
|
||||
*/
|
||||
List<SuperAdmin> findByName(String name);
|
||||
|
||||
/**
|
||||
* 查找所有激活状态的超级管理员
|
||||
* @param isActive 是否激活
|
||||
* @return 超级管理员列表
|
||||
*/
|
||||
List<SuperAdmin> findByIsActive(Integer isActive);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.van.business.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import cn.van.business.model.pl.TaobaoComment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Van
|
||||
* @version 1.0
|
||||
* @create 2025/7/6
|
||||
* @description 淘宝评论数据访问层接口
|
||||
*/
|
||||
@Repository
|
||||
public interface TaobaoCommentRepository extends JpaRepository<TaobaoComment, Integer> {
|
||||
|
||||
List<TaobaoComment> findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(String productId, Integer isUse);
|
||||
|
||||
List<TaobaoComment> findByProductIdAndPictureUrlsIsNotNull(String productId);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.van.business.repository;
|
||||
|
||||
import cn.van.business.model.cj.XbMessageItem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2025/5/2 19:16
|
||||
* @description:评论数据访问层接口
|
||||
*/
|
||||
@Repository
|
||||
public interface XbMessageItemRepository extends JpaRepository<XbMessageItem, Integer> {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.van.business.repository;
|
||||
|
||||
import cn.van.business.model.cj.XbMessage;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2025/5/2 19:16
|
||||
* @description:评论数据访问层接口
|
||||
*/
|
||||
@Repository
|
||||
public interface XbMessageRepository extends JpaRepository<XbMessage, Integer> {
|
||||
|
||||
|
||||
|
||||
}
|
||||
401
src/main/java/cn/van/business/service/ImageConvertService.java
Normal file
401
src/main/java/cn/van/business/service/ImageConvertService.java
Normal file
@@ -0,0 +1,401 @@
|
||||
package cn.van.business.service;
|
||||
|
||||
import cn.van.business.model.pl.ImageConversion;
|
||||
import cn.van.business.repository.ImageConversionRepository;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import net.coobird.thumbnailator.Thumbnails;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* WebP图片IO支持工具类
|
||||
* 尝试使用可用的方式读取webp图片
|
||||
*/
|
||||
class WebPImageIO {
|
||||
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(WebPImageIO.class);
|
||||
private static volatile boolean webpSupported = false;
|
||||
private static volatile boolean checked = false;
|
||||
|
||||
/**
|
||||
* 检查是否支持webp格式
|
||||
*/
|
||||
static synchronized boolean isWebPSupported() {
|
||||
if (checked) {
|
||||
return webpSupported;
|
||||
}
|
||||
|
||||
try {
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("webp");
|
||||
webpSupported = readers.hasNext();
|
||||
if (webpSupported) {
|
||||
log.info("WebP图片格式支持已启用");
|
||||
} else {
|
||||
log.warn("未检测到WebP图片格式支持,webp图片转换将被跳过");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("检查WebP支持时出错: {}", e.getMessage());
|
||||
webpSupported = false;
|
||||
}
|
||||
|
||||
checked = true;
|
||||
return webpSupported;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片转换服务类
|
||||
* 负责将webp格式的图片转换为jpg格式,并缓存转换结果
|
||||
*
|
||||
* @author System
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ImageConvertService {
|
||||
|
||||
@Autowired
|
||||
private ImageConversionRepository imageConversionRepository;
|
||||
|
||||
/**
|
||||
* 图片存储根目录,默认使用系统临时目录
|
||||
*/
|
||||
@Value("${image.convert.storage-path:${java.io.tmpdir}/comment-images}")
|
||||
private String storagePath;
|
||||
|
||||
/**
|
||||
* 图片访问基础URL,用于生成转换后图片的访问地址
|
||||
* 如果为空,则返回本地文件路径
|
||||
*/
|
||||
@Value("${image.convert.base-url:}")
|
||||
private String baseUrl;
|
||||
|
||||
/**
|
||||
* 转换图片URL列表,将webp格式转换为jpg
|
||||
*
|
||||
* @param imageUrls 原始图片URL列表
|
||||
* @return 转换后的图片URL列表(webp已转换为jpg,其他格式保持不变)
|
||||
*/
|
||||
public List<String> convertImageUrls(List<String> imageUrls) {
|
||||
if (imageUrls == null || imageUrls.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
log.info("开始转换图片URL列表,共{}张图片", imageUrls.size());
|
||||
List<String> convertedUrls = new ArrayList<>();
|
||||
int successCount = 0;
|
||||
int skipCount = 0;
|
||||
|
||||
for (String imageUrl : imageUrls) {
|
||||
if (StrUtil.isBlank(imageUrl)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.debug("处理图片URL: {}", imageUrl);
|
||||
try {
|
||||
String convertedUrl = convertImageUrl(imageUrl);
|
||||
if (!convertedUrl.equals(imageUrl)) {
|
||||
successCount++;
|
||||
log.debug("图片转换成功: {} -> {}", imageUrl, convertedUrl);
|
||||
} else {
|
||||
skipCount++;
|
||||
log.debug("图片无需转换(非webp格式): {}", imageUrl);
|
||||
}
|
||||
convertedUrls.add(convertedUrl);
|
||||
} catch (Exception e) {
|
||||
// 转换失败时使用原URL,不中断流程
|
||||
skipCount++;
|
||||
log.warn("图片转换失败,使用原URL: {}. 错误: {}", imageUrl, e.getMessage());
|
||||
convertedUrls.add(imageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("图片URL转换完成,共{}张,成功转换{}张,跳过/失败{}张",
|
||||
imageUrls.size(), successCount, skipCount);
|
||||
return convertedUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换单个图片URL
|
||||
*
|
||||
* @param originalUrl 原始图片URL
|
||||
* @return 转换后的图片URL(如果不需要转换则返回原URL)
|
||||
* @throws Exception 转换过程中的异常(调用方会捕获并返回原URL)
|
||||
*/
|
||||
private String convertImageUrl(String originalUrl) throws Exception {
|
||||
if (StrUtil.isBlank(originalUrl)) {
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
// 规范化URL:处理协议相对URL(//开头)
|
||||
String normalizedUrl = normalizeUrl(originalUrl);
|
||||
|
||||
// 检查是否为webp格式
|
||||
if (!isWebpFormat(normalizedUrl)) {
|
||||
return originalUrl; // 返回原URL,保持一致性
|
||||
}
|
||||
|
||||
// 检查系统是否支持webp转换
|
||||
if (!WebPImageIO.isWebPSupported()) {
|
||||
log.warn("系统不支持webp格式,跳过转换: {}", normalizedUrl);
|
||||
throw new IOException("系统不支持webp格式转换");
|
||||
}
|
||||
|
||||
// 使用规范化后的URL进行缓存查询和转换
|
||||
// 检查是否已转换(使用规范化URL作为key)
|
||||
Optional<ImageConversion> existing = imageConversionRepository.findByOriginalUrl(normalizedUrl);
|
||||
if (existing.isPresent()) {
|
||||
ImageConversion conversion = existing.get();
|
||||
log.debug("使用缓存的转换结果: {} -> {}", normalizedUrl, conversion.getConvertedUrl());
|
||||
return conversion.getConvertedUrl();
|
||||
}
|
||||
|
||||
// 执行转换(使用规范化URL)
|
||||
log.info("开始转换webp图片: {}", normalizedUrl);
|
||||
String convertedUrl = performConversion(normalizedUrl);
|
||||
log.info("图片转换成功: {} -> {}", normalizedUrl, convertedUrl);
|
||||
return convertedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化URL:处理协议相对URL等特殊情况
|
||||
*
|
||||
* @param url 原始URL
|
||||
* @return 规范化后的URL
|
||||
*/
|
||||
private String normalizeUrl(String url) {
|
||||
if (StrUtil.isBlank(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// 清理特殊字符(如零宽字符)
|
||||
String cleanUrl = url.trim().replaceAll("[\\u200B-\\u200D\\uFEFF]", "");
|
||||
|
||||
// 处理协议相对URL(//开头)
|
||||
if (cleanUrl.startsWith("//")) {
|
||||
cleanUrl = "https:" + cleanUrl;
|
||||
log.debug("转换协议相对URL: {} -> {}", url, cleanUrl);
|
||||
}
|
||||
|
||||
return cleanUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查URL是否为webp格式
|
||||
*
|
||||
* @param url 图片URL
|
||||
* @return 是否为webp格式
|
||||
*/
|
||||
private boolean isWebpFormat(String url) {
|
||||
if (StrUtil.isBlank(url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 清理URL中的特殊字符(如零宽字符)
|
||||
String cleanUrl = url.trim().replaceAll("[\\u200B-\\u200D\\uFEFF]", "");
|
||||
|
||||
// 检查URL中是否包含.webp扩展名(不区分大小写)
|
||||
String lowerUrl = cleanUrl.toLowerCase();
|
||||
// 检查URL参数或路径中是否包含webp
|
||||
boolean isWebp = lowerUrl.contains(".webp") ||
|
||||
lowerUrl.contains("format=webp") ||
|
||||
lowerUrl.contains("?webp") ||
|
||||
lowerUrl.contains("&webp");
|
||||
|
||||
if (isWebp) {
|
||||
log.debug("检测到webp格式图片: {}", cleanUrl);
|
||||
}
|
||||
|
||||
return isWebp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行图片转换
|
||||
*
|
||||
* @param originalUrl 原始webp图片URL
|
||||
* @return 转换后的jpg图片URL或路径
|
||||
*/
|
||||
private String performConversion(String originalUrl) throws IOException {
|
||||
// 确保存储目录存在
|
||||
Path storageDir = Paths.get(storagePath);
|
||||
if (!Files.exists(storageDir)) {
|
||||
Files.createDirectories(storageDir);
|
||||
}
|
||||
|
||||
// 生成转换后的文件名(基于原URL的MD5)
|
||||
String fileName = generateFileName(originalUrl);
|
||||
Path outputPath = storageDir.resolve(fileName);
|
||||
|
||||
// 如果转换后的文件已存在,直接返回
|
||||
if (Files.exists(outputPath)) {
|
||||
String convertedUrl = generateConvertedUrl(fileName);
|
||||
// 如果数据库中没有记录,保存记录
|
||||
if (!imageConversionRepository.existsByOriginalUrl(originalUrl)) {
|
||||
saveConversionRecord(originalUrl, convertedUrl, Files.size(outputPath));
|
||||
}
|
||||
return convertedUrl;
|
||||
}
|
||||
|
||||
// 下载原始图片
|
||||
byte[] imageData = downloadImage(originalUrl);
|
||||
if (imageData == null || imageData.length == 0) {
|
||||
throw new IOException("下载图片失败或图片数据为空: " + originalUrl);
|
||||
}
|
||||
|
||||
// 转换图片格式
|
||||
BufferedImage bufferedImage = null;
|
||||
try {
|
||||
// 检查是否支持webp格式
|
||||
boolean webpSupported = WebPImageIO.isWebPSupported();
|
||||
|
||||
if (webpSupported) {
|
||||
// 如果支持webp,尝试使用ImageIO读取
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(imageData)) {
|
||||
bufferedImage = ImageIO.read(bais);
|
||||
}
|
||||
|
||||
// 如果ImageIO无法读取,尝试使用WebP特定的读取器
|
||||
if (bufferedImage == null) {
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("webp");
|
||||
if (readers.hasNext()) {
|
||||
ImageReader reader = readers.next();
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(imageData);
|
||||
ImageInputStream iis = ImageIO.createImageInputStream(bais)) {
|
||||
reader.setInput(iis);
|
||||
bufferedImage = reader.read(0);
|
||||
} finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果不支持webp,尝试使用Thumbnailator转换
|
||||
// 注意:Thumbnailator内部也使用ImageIO,所以通常也无法处理webp
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(imageData)) {
|
||||
bufferedImage = Thumbnails.of(bais)
|
||||
.scale(1.0)
|
||||
.asBufferedImage();
|
||||
} catch (Exception e) {
|
||||
log.debug("Thumbnailator无法读取webp图片: {}", e.getMessage());
|
||||
// 如果无法转换,抛出异常,后续会返回原URL
|
||||
throw new IOException("当前系统不支持webp格式转换。图片将保持原格式返回。", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (bufferedImage == null) {
|
||||
throw new IOException("无法读取webp图片格式。当前系统不支持webp格式转换。");
|
||||
}
|
||||
|
||||
// 如果是RGBA格式,转换为RGB
|
||||
if (bufferedImage.getType() == BufferedImage.TYPE_4BYTE_ABGR ||
|
||||
bufferedImage.getType() == BufferedImage.TYPE_INT_ARGB) {
|
||||
BufferedImage rgbImage = new BufferedImage(
|
||||
bufferedImage.getWidth(),
|
||||
bufferedImage.getHeight(),
|
||||
BufferedImage.TYPE_INT_RGB
|
||||
);
|
||||
rgbImage.createGraphics().drawImage(bufferedImage, 0, 0, null);
|
||||
bufferedImage = rgbImage;
|
||||
}
|
||||
|
||||
// 保存为jpg格式
|
||||
boolean success = ImageIO.write(bufferedImage, "jpg", outputPath.toFile());
|
||||
if (!success) {
|
||||
throw new IOException("无法写入jpg格式");
|
||||
}
|
||||
|
||||
String convertedUrl = generateConvertedUrl(fileName);
|
||||
long fileSize = Files.size(outputPath);
|
||||
|
||||
// 保存转换记录
|
||||
saveConversionRecord(originalUrl, convertedUrl, fileSize);
|
||||
|
||||
log.info("图片转换完成: {} -> {} (大小: {} bytes)", originalUrl, convertedUrl, fileSize);
|
||||
return convertedUrl;
|
||||
|
||||
} finally {
|
||||
if (bufferedImage != null) {
|
||||
bufferedImage.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载图片
|
||||
*
|
||||
* @param url 图片URL
|
||||
* @return 图片字节数组
|
||||
*/
|
||||
private byte[] downloadImage(String url) {
|
||||
try {
|
||||
return HttpUtil.downloadBytes(url);
|
||||
} catch (Exception e) {
|
||||
log.error("下载图片失败: {}, 错误: {}", url, e.getMessage());
|
||||
throw new RuntimeException("下载图片失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件名(基于原URL的MD5)
|
||||
*
|
||||
* @param originalUrl 原始URL
|
||||
* @return 文件名(不含扩展名)
|
||||
*/
|
||||
private String generateFileName(String originalUrl) {
|
||||
// 使用URL的MD5作为文件名,避免重复和特殊字符问题
|
||||
String md5 = cn.hutool.crypto.digest.DigestUtil.md5Hex(originalUrl);
|
||||
return md5 + ".jpg";
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成转换后的URL
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @return 转换后的URL或本地路径
|
||||
*/
|
||||
private String generateConvertedUrl(String fileName) {
|
||||
if (StrUtil.isNotBlank(baseUrl)) {
|
||||
// 如果配置了baseUrl,返回HTTP访问地址
|
||||
return baseUrl.endsWith("/") ? baseUrl + fileName : baseUrl + "/" + fileName;
|
||||
} else {
|
||||
// 否则返回本地文件路径
|
||||
return Paths.get(storagePath, fileName).toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存转换记录
|
||||
*
|
||||
* @param originalUrl 原始URL
|
||||
* @param convertedUrl 转换后的URL
|
||||
* @param fileSize 文件大小
|
||||
*/
|
||||
private void saveConversionRecord(String originalUrl, String convertedUrl, long fileSize) {
|
||||
ImageConversion conversion = new ImageConversion();
|
||||
conversion.setOriginalUrl(originalUrl);
|
||||
conversion.setConvertedUrl(convertedUrl);
|
||||
conversion.setFileSize(fileSize);
|
||||
conversion.setConvertedAt(LocalDateTime.now());
|
||||
|
||||
try {
|
||||
imageConversionRepository.save(conversion);
|
||||
} catch (Exception e) {
|
||||
log.warn("保存转换记录失败: {}, 错误: {}", originalUrl, e.getMessage());
|
||||
// 不抛出异常,因为转换本身已成功
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
433
src/main/java/cn/van/business/service/MarketingImageService.java
Normal file
433
src/main/java/cn/van/business/service/MarketingImageService.java
Normal file
@@ -0,0 +1,433 @@
|
||||
package cn.van.business.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.van.business.util.ds.DeepSeekClientUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.coobird.thumbnailator.Thumbnails;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 营销图片合成服务
|
||||
* 用于生成小红书等平台的营销对比图
|
||||
*
|
||||
* @author System
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MarketingImageService {
|
||||
|
||||
@Autowired
|
||||
private DeepSeekClientUtil deepSeekClientUtil;
|
||||
|
||||
// 输出图片尺寸
|
||||
private static final int OUTPUT_WIDTH = 1080;
|
||||
private static final int OUTPUT_HEIGHT = 1080;
|
||||
|
||||
// 字体配置(支持回退)
|
||||
private static final String[] FONT_NAMES = {"Microsoft YaHei", "SimHei", "Arial", Font.SANS_SERIF}; // 字体优先级
|
||||
private static final int ORIGINAL_PRICE_FONT_SIZE = 36; // 官网价字体大小
|
||||
private static final int FINAL_PRICE_FONT_SIZE = 72; // 到手价字体大小
|
||||
private static final int PRODUCT_NAME_FONT_SIZE = 32; // 商品名称字体大小
|
||||
|
||||
/**
|
||||
* 获取可用字体
|
||||
*/
|
||||
private Font getAvailableFont(int style, int size) {
|
||||
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
|
||||
String[] availableFonts = ge.getAvailableFontFamilyNames();
|
||||
|
||||
for (String fontName : FONT_NAMES) {
|
||||
for (String available : availableFonts) {
|
||||
if (available.equals(fontName)) {
|
||||
return new Font(fontName, style, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果都不可用,使用默认字体
|
||||
return new Font(Font.SANS_SERIF, style, size);
|
||||
}
|
||||
|
||||
// 颜色配置
|
||||
private static final Color ORIGINAL_PRICE_COLOR = new Color(153, 153, 153); // 灰色 #999999
|
||||
private static final Color FINAL_PRICE_COLOR = new Color(255, 0, 0); // 红色 #FF0000
|
||||
private static final Color PRODUCT_NAME_COLOR = new Color(51, 51, 51); // 深灰色 #333333
|
||||
private static final Color BACKGROUND_COLOR = Color.WHITE; // 背景色
|
||||
|
||||
/**
|
||||
* 生成营销图片
|
||||
*
|
||||
* @param productImageUrl 商品主图URL
|
||||
* @param originalPrice 官网价
|
||||
* @param finalPrice 到手价
|
||||
* @param productName 商品名称(可选,如果为空则使用AI提取)
|
||||
* @return Base64编码的图片
|
||||
*/
|
||||
public String generateMarketingImage(String productImageUrl, Double originalPrice, Double finalPrice, String productName) {
|
||||
try {
|
||||
log.info("开始生成营销图片: productImageUrl={}, originalPrice={}, finalPrice={}, productName={}",
|
||||
productImageUrl, originalPrice, finalPrice, productName);
|
||||
|
||||
// 1. 加载商品主图
|
||||
BufferedImage productImage = loadProductImage(productImageUrl);
|
||||
if (productImage == null) {
|
||||
throw new IOException("无法加载商品主图: " + productImageUrl);
|
||||
}
|
||||
|
||||
// 2. 提取商品标题关键部分(如果未提供)
|
||||
String keyProductName = productName;
|
||||
if (StrUtil.isBlank(keyProductName)) {
|
||||
// 如果未提供商品名称,则无法提取,留空
|
||||
keyProductName = "";
|
||||
} else {
|
||||
// 如果提供了完整商品名称,提取关键部分
|
||||
keyProductName = extractKeyProductName(keyProductName);
|
||||
}
|
||||
|
||||
// 3. 创建画布
|
||||
BufferedImage canvas = new BufferedImage(OUTPUT_WIDTH, OUTPUT_HEIGHT, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = canvas.createGraphics();
|
||||
|
||||
// 设置抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
|
||||
// 4. 绘制背景
|
||||
g2d.setColor(BACKGROUND_COLOR);
|
||||
g2d.fillRect(0, 0, OUTPUT_WIDTH, OUTPUT_HEIGHT);
|
||||
|
||||
// 5. 缩放并绘制商品主图(居中,保持比例)
|
||||
int productImageSize = 800; // 商品图尺寸
|
||||
int productImageX = (OUTPUT_WIDTH - productImageSize) / 2;
|
||||
int productImageY = 80; // 顶部留白
|
||||
|
||||
BufferedImage scaledProductImage = Thumbnails.of(productImage)
|
||||
.size(productImageSize, productImageSize)
|
||||
.asBufferedImage();
|
||||
|
||||
g2d.drawImage(scaledProductImage, productImageX, productImageY, null);
|
||||
|
||||
// 6. 绘制商品名称(如果有)
|
||||
int textStartY = productImageY + productImageSize + 40;
|
||||
if (StrUtil.isNotBlank(keyProductName)) {
|
||||
drawProductName(g2d, keyProductName, textStartY);
|
||||
textStartY += 60; // 增加间距
|
||||
}
|
||||
|
||||
// 7. 绘制官网价(带删除线,在上方)
|
||||
int originalPriceY = textStartY + 80;
|
||||
drawOriginalPrice(g2d, originalPrice, originalPriceY);
|
||||
|
||||
// 8. 绘制向下箭头
|
||||
int arrowY = originalPriceY + 60;
|
||||
drawDownArrow(g2d, arrowY);
|
||||
|
||||
// 9. 绘制到手价(大红色,在下方)
|
||||
int finalPriceY = arrowY + 80;
|
||||
drawFinalPrice(g2d, finalPrice, finalPriceY);
|
||||
|
||||
// 10. 绘制爆炸贴图装饰(右下角)
|
||||
drawExplosionDecoration(g2d);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
// 11. 转换为Base64
|
||||
String base64Image = imageToBase64(canvas, "jpg");
|
||||
log.info("营销图片生成成功");
|
||||
return base64Image;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成营销图片失败", e);
|
||||
throw new RuntimeException("生成营销图片失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载商品主图
|
||||
*/
|
||||
private BufferedImage loadProductImage(String imageUrl) throws IOException {
|
||||
try {
|
||||
byte[] imageData = HttpUtil.downloadBytes(imageUrl);
|
||||
if (imageData == null || imageData.length == 0) {
|
||||
throw new IOException("下载图片失败或图片数据为空");
|
||||
}
|
||||
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(imageData)) {
|
||||
return ImageIO.read(bais);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("加载商品主图失败: {}", imageUrl, e);
|
||||
throw new IOException("加载商品主图失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取商品标题关键部分(使用AI)
|
||||
*
|
||||
* @param fullProductName 完整商品名称
|
||||
* @return 提取的关键部分
|
||||
*/
|
||||
public String extractKeyProductName(String fullProductName) {
|
||||
if (StrUtil.isBlank(fullProductName)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用DeepSeek提取商品标题关键部分
|
||||
String prompt = String.format(
|
||||
"请从以下商品标题中提取最关键的3-8个字作为核心卖点,只返回提取的关键词,不要其他内容:\n%s",
|
||||
fullProductName
|
||||
);
|
||||
|
||||
String extracted = deepSeekClientUtil.getDeepSeekResponse(prompt);
|
||||
if (StrUtil.isNotBlank(extracted)) {
|
||||
// 清理可能的换行和多余空格
|
||||
extracted = extracted.trim().replaceAll("\\s+", "");
|
||||
// 限制长度
|
||||
if (extracted.length() > 12) {
|
||||
extracted = extracted.substring(0, 12);
|
||||
}
|
||||
log.info("提取商品标题关键部分成功: {} -> {}", fullProductName, extracted);
|
||||
return extracted;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("使用AI提取商品标题关键部分失败,使用简单截取: {}", fullProductName, e);
|
||||
}
|
||||
|
||||
// 降级方案:简单截取前部分
|
||||
return simpleExtractKeyName(fullProductName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单提取商品名称关键部分(降级方案)
|
||||
*/
|
||||
private String simpleExtractKeyName(String fullName) {
|
||||
if (StrUtil.isBlank(fullName)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 移除常见的规格信息(如XL、175/96A等)
|
||||
String cleaned = fullName
|
||||
.replaceAll("\\s*XL|L|M|S|XXL\\s*", "")
|
||||
.replaceAll("\\s*\\d+/\\d+[A-Z]?\\s*", "")
|
||||
.replaceAll("\\s*【.*?】\\s*", "")
|
||||
.replaceAll("\\s*\\(.*?\\)\\s*", "");
|
||||
|
||||
// 提取前10-15个字符
|
||||
if (cleaned.length() > 15) {
|
||||
cleaned = cleaned.substring(0, 15);
|
||||
}
|
||||
|
||||
return cleaned.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制商品名称
|
||||
*/
|
||||
private void drawProductName(Graphics2D g2d, String productName, int y) {
|
||||
Font font = getAvailableFont(Font.BOLD, PRODUCT_NAME_FONT_SIZE);
|
||||
g2d.setFont(font);
|
||||
g2d.setColor(PRODUCT_NAME_COLOR);
|
||||
|
||||
// 计算文字宽度,如果太长则截断
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
String displayName = productName;
|
||||
int maxWidth = OUTPUT_WIDTH - 80; // 左右各留40px
|
||||
|
||||
if (fm.stringWidth(displayName) > maxWidth) {
|
||||
// 截断并添加省略号
|
||||
while (fm.stringWidth(displayName + "...") > maxWidth && displayName.length() > 0) {
|
||||
displayName = displayName.substring(0, displayName.length() - 1);
|
||||
}
|
||||
displayName += "...";
|
||||
}
|
||||
|
||||
int textWidth = fm.stringWidth(displayName);
|
||||
int x = (OUTPUT_WIDTH - textWidth) / 2; // 居中
|
||||
|
||||
g2d.drawString(displayName, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制官网价(带删除线)
|
||||
*/
|
||||
private void drawOriginalPrice(Graphics2D g2d, Double originalPrice, int y) {
|
||||
Font font = getAvailableFont(Font.BOLD, ORIGINAL_PRICE_FONT_SIZE);
|
||||
g2d.setFont(font);
|
||||
g2d.setColor(ORIGINAL_PRICE_COLOR);
|
||||
|
||||
String priceText = "官网价:¥" + String.format("%.0f", originalPrice);
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
int textWidth = fm.stringWidth(priceText);
|
||||
int x = (OUTPUT_WIDTH - textWidth) / 2; // 居中
|
||||
|
||||
// 绘制文字
|
||||
g2d.drawString(priceText, x, y);
|
||||
|
||||
// 绘制删除线
|
||||
int lineY = y - fm.getAscent() / 2;
|
||||
g2d.setStroke(new BasicStroke(3.0f)); // 3px粗的删除线
|
||||
g2d.drawLine(x, lineY, x + textWidth, lineY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制向下箭头
|
||||
*/
|
||||
private void drawDownArrow(Graphics2D g2d, int y) {
|
||||
int centerX = OUTPUT_WIDTH / 2;
|
||||
int arrowSize = 40;
|
||||
|
||||
g2d.setColor(new Color(200, 200, 200)); // 浅灰色箭头
|
||||
g2d.setStroke(new BasicStroke(3.0f));
|
||||
|
||||
// 绘制竖线
|
||||
g2d.drawLine(centerX, y, centerX, y + arrowSize);
|
||||
|
||||
// 绘制箭头(向下)
|
||||
int[] xPoints = {centerX, centerX - 15, centerX + 15};
|
||||
int[] yPoints = {y + arrowSize, y + arrowSize - 20, y + arrowSize - 20};
|
||||
g2d.fillPolygon(xPoints, yPoints, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制到手价(大红色)
|
||||
*/
|
||||
private void drawFinalPrice(Graphics2D g2d, Double finalPrice, int y) {
|
||||
Font font = getAvailableFont(Font.BOLD, FINAL_PRICE_FONT_SIZE);
|
||||
g2d.setFont(font);
|
||||
g2d.setColor(FINAL_PRICE_COLOR);
|
||||
|
||||
String priceText = "到手价:¥" + String.format("%.0f", finalPrice);
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
int textWidth = fm.stringWidth(priceText);
|
||||
int x = (OUTPUT_WIDTH - textWidth) / 2; // 居中
|
||||
|
||||
g2d.drawString(priceText, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制爆炸贴图装饰(右下角)
|
||||
*/
|
||||
private void drawExplosionDecoration(Graphics2D g2d) {
|
||||
// 绘制简单的爆炸形状(星形)
|
||||
int centerX = OUTPUT_WIDTH - 120;
|
||||
int centerY = OUTPUT_HEIGHT - 120;
|
||||
int radius = 50;
|
||||
|
||||
g2d.setColor(new Color(255, 200, 0)); // 金黄色
|
||||
g2d.setStroke(new BasicStroke(4.0f));
|
||||
|
||||
// 绘制星形爆炸效果
|
||||
int points = 8;
|
||||
int[] xPoints = new int[points * 2];
|
||||
int[] yPoints = new int[points * 2];
|
||||
|
||||
for (int i = 0; i < points * 2; i++) {
|
||||
double angle = Math.PI * i / points;
|
||||
int r = (i % 2 == 0) ? radius : radius / 2;
|
||||
xPoints[i] = (int) (centerX + r * Math.cos(angle));
|
||||
yPoints[i] = (int) (centerY + r * Math.sin(angle));
|
||||
}
|
||||
|
||||
g2d.fillPolygon(xPoints, yPoints, points * 2);
|
||||
|
||||
// 绘制内部圆形
|
||||
g2d.setColor(new Color(255, 100, 0)); // 橙红色
|
||||
g2d.fillOval(centerX - radius / 2, centerY - radius / 2, radius, radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将BufferedImage转换为Base64字符串
|
||||
*/
|
||||
private String imageToBase64(BufferedImage image, String format) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, format, baos);
|
||||
byte[] imageBytes = baos.toByteArray();
|
||||
return "data:image/" + format + ";base64," + Base64.getEncoder().encodeToString(imageBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成营销图片
|
||||
*
|
||||
* @param requests 批量请求列表
|
||||
* @return 结果列表,每个元素包含base64图片
|
||||
*/
|
||||
public Map<String, Object> batchGenerateMarketingImages(java.util.List<Map<String, Object>> requests) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
java.util.List<Map<String, Object>> results = new java.util.ArrayList<>();
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (int i = 0; i < requests.size(); i++) {
|
||||
Map<String, Object> request = requests.get(i);
|
||||
Map<String, Object> itemResult = new HashMap<>();
|
||||
|
||||
try {
|
||||
String productImageUrl = (String) request.get("productImageUrl");
|
||||
Double originalPrice = getDoubleValue(request.get("originalPrice"));
|
||||
Double finalPrice = getDoubleValue(request.get("finalPrice"));
|
||||
String productName = (String) request.get("productName");
|
||||
|
||||
if (productImageUrl == null || originalPrice == null || finalPrice == null) {
|
||||
throw new IllegalArgumentException("缺少必要参数: productImageUrl, originalPrice, finalPrice");
|
||||
}
|
||||
|
||||
String base64Image = generateMarketingImage(productImageUrl, originalPrice, finalPrice, productName);
|
||||
|
||||
itemResult.put("success", true);
|
||||
itemResult.put("imageBase64", base64Image);
|
||||
itemResult.put("index", i);
|
||||
successCount++;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("批量生成第{}张图片失败", i, e);
|
||||
itemResult.put("success", false);
|
||||
itemResult.put("error", e.getMessage());
|
||||
itemResult.put("index", i);
|
||||
failCount++;
|
||||
}
|
||||
|
||||
results.add(itemResult);
|
||||
}
|
||||
|
||||
result.put("results", results);
|
||||
result.put("total", requests.size());
|
||||
result.put("successCount", successCount);
|
||||
result.put("failCount", failCount);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取Double值
|
||||
*/
|
||||
private Double getDoubleValue(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Double) {
|
||||
return (Double) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
}
|
||||
try {
|
||||
return Double.parseDouble(value.toString());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
347
src/main/java/cn/van/business/service/SocialMediaService.java
Normal file
347
src/main/java/cn/van/business/service/SocialMediaService.java
Normal file
@@ -0,0 +1,347 @@
|
||||
package cn.van.business.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.van.business.util.ds.DeepSeekClientUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 小红书/抖音内容生成服务
|
||||
* 提供关键词提取、文案生成等功能
|
||||
*
|
||||
* @author System
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SocialMediaService {
|
||||
|
||||
@Autowired
|
||||
private DeepSeekClientUtil deepSeekClientUtil;
|
||||
|
||||
@Autowired
|
||||
private MarketingImageService marketingImageService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private StringRedisTemplate redisTemplate;
|
||||
|
||||
// Redis Key 前缀
|
||||
private static final String REDIS_KEY_PREFIX = "social_media:prompt:";
|
||||
|
||||
// 默认提示词模板
|
||||
private static final String DEFAULT_KEYWORDS_PROMPT =
|
||||
"请从以下商品标题中提取3-5个最核心的关键词,这些关键词要能突出商品的核心卖点和特色。\n" +
|
||||
"要求:\n" +
|
||||
"1. 每个关键词2-4个字\n" +
|
||||
"2. 关键词要能吸引小红书/抖音用户\n" +
|
||||
"3. 用逗号分隔,只返回关键词,不要其他内容\n" +
|
||||
"商品标题:%s";
|
||||
|
||||
private static final String DEFAULT_CONTENT_PROMPT_XHS =
|
||||
"请为小红书平台生成一篇商品推广文案,要求:\n" +
|
||||
"1. 风格:真实、种草、有温度\n" +
|
||||
"2. 开头:用emoji或感叹句吸引注意\n" +
|
||||
"3. 内容:突出商品亮点、使用场景、性价比\n" +
|
||||
"4. 结尾:引导行动(如:快冲、闭眼入等)\n" +
|
||||
"5. 长度:150-300字\n" +
|
||||
"6. 适当使用emoji和换行\n" +
|
||||
"\n商品信息:\n" +
|
||||
"商品名称:%s\n" +
|
||||
"%s" + // 价格信息
|
||||
"%s" + // 关键词
|
||||
"\n请直接生成文案内容,不要添加其他说明:";
|
||||
|
||||
private static final String DEFAULT_CONTENT_PROMPT_DOUYIN =
|
||||
"请为抖音平台生成一篇商品推广文案,要求:\n" +
|
||||
"1. 风格:直接、有冲击力、吸引眼球\n" +
|
||||
"2. 开头:用疑问句或对比句抓住注意力\n" +
|
||||
"3. 内容:强调价格优势、限时优惠、稀缺性\n" +
|
||||
"4. 结尾:制造紧迫感,引导立即行动\n" +
|
||||
"5. 长度:100-200字\n" +
|
||||
"6. 使用短句,节奏感强\n" +
|
||||
"\n商品信息:\n" +
|
||||
"商品名称:%s\n" +
|
||||
"%s" + // 价格信息
|
||||
"%s" + // 关键词
|
||||
"\n请直接生成文案内容,不要添加其他说明:";
|
||||
|
||||
private static final String DEFAULT_CONTENT_PROMPT_BOTH =
|
||||
"请生成一篇适合小红书和抖音平台的商品推广文案,要求:\n" +
|
||||
"1. 风格:真实、有吸引力\n" +
|
||||
"2. 突出商品亮点和价格优势\n" +
|
||||
"3. 长度:150-250字\n" +
|
||||
"\n商品信息:\n" +
|
||||
"商品名称:%s\n" +
|
||||
"%s" + // 价格信息
|
||||
"%s" + // 关键词
|
||||
"\n请直接生成文案内容,不要添加其他说明:";
|
||||
|
||||
/**
|
||||
* 提取商品标题关键词
|
||||
*
|
||||
* @param productName 商品名称
|
||||
* @return 关键词列表
|
||||
*/
|
||||
public Map<String, Object> extractKeywords(String productName) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
if (StrUtil.isBlank(productName)) {
|
||||
result.put("success", false);
|
||||
result.put("error", "商品名称不能为空");
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
// 从 Redis 读取提示词模板,如果没有则使用默认模板
|
||||
String promptTemplate = getPromptTemplate("keywords", DEFAULT_KEYWORDS_PROMPT);
|
||||
String prompt = String.format(promptTemplate, productName);
|
||||
|
||||
String response = deepSeekClientUtil.getDeepSeekResponse(prompt);
|
||||
|
||||
if (StrUtil.isNotBlank(response)) {
|
||||
// 解析关键词
|
||||
String[] keywords = response.trim()
|
||||
.replaceAll("[,,]", ",")
|
||||
.split(",");
|
||||
|
||||
List<String> keywordList = new ArrayList<>();
|
||||
for (String keyword : keywords) {
|
||||
String cleaned = keyword.trim();
|
||||
if (StrUtil.isNotBlank(cleaned) && cleaned.length() <= 6) {
|
||||
keywordList.add(cleaned);
|
||||
}
|
||||
}
|
||||
|
||||
// 限制数量
|
||||
if (keywordList.size() > 5) {
|
||||
keywordList = keywordList.subList(0, 5);
|
||||
}
|
||||
|
||||
result.put("success", true);
|
||||
result.put("keywords", keywordList);
|
||||
result.put("keywordsText", String.join("、", keywordList));
|
||||
log.info("提取关键词成功: {} -> {}", productName, keywordList);
|
||||
} else {
|
||||
throw new Exception("AI返回结果为空");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("提取关键词失败", e);
|
||||
result.put("success", false);
|
||||
result.put("error", "提取关键词失败: " + e.getMessage());
|
||||
// 降级方案:简单提取
|
||||
result.put("keywords", simpleExtractKeywords(productName));
|
||||
result.put("keywordsText", String.join("、", simpleExtractKeywords(productName)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成小红书/抖音文案
|
||||
*
|
||||
* @param productName 商品名称
|
||||
* @param originalPrice 原价
|
||||
* @param finalPrice 到手价
|
||||
* @param keywords 关键词(可选)
|
||||
* @param style 文案风格:xhs(小红书)、douyin(抖音)、both(通用)
|
||||
* @return 生成的文案
|
||||
*/
|
||||
public Map<String, Object> generateContent(String productName, Double originalPrice,
|
||||
Double finalPrice, String keywords, String style) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
if (StrUtil.isBlank(productName)) {
|
||||
result.put("success", false);
|
||||
result.put("error", "商品名称不能为空");
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建价格信息
|
||||
StringBuilder priceInfo = new StringBuilder();
|
||||
if (originalPrice != null && originalPrice > 0) {
|
||||
priceInfo.append("原价:¥").append(String.format("%.0f", originalPrice)).append("\n");
|
||||
}
|
||||
if (finalPrice != null && finalPrice > 0) {
|
||||
priceInfo.append("到手价:¥").append(String.format("%.0f", finalPrice)).append("\n");
|
||||
}
|
||||
|
||||
// 构建关键词信息
|
||||
String keywordsInfo = "";
|
||||
if (StrUtil.isNotBlank(keywords)) {
|
||||
keywordsInfo = "关键词:" + keywords + "\n";
|
||||
}
|
||||
|
||||
// 从 Redis 读取提示词模板,如果没有则使用默认模板
|
||||
String promptTemplate;
|
||||
if ("xhs".equals(style)) {
|
||||
promptTemplate = getPromptTemplate("content:xhs", DEFAULT_CONTENT_PROMPT_XHS);
|
||||
} else if ("douyin".equals(style)) {
|
||||
promptTemplate = getPromptTemplate("content:douyin", DEFAULT_CONTENT_PROMPT_DOUYIN);
|
||||
} else {
|
||||
promptTemplate = getPromptTemplate("content:both", DEFAULT_CONTENT_PROMPT_BOTH);
|
||||
}
|
||||
|
||||
String prompt = String.format(promptTemplate, productName, priceInfo.toString(), keywordsInfo);
|
||||
|
||||
String content = deepSeekClientUtil.getDeepSeekResponse(prompt.toString());
|
||||
|
||||
if (StrUtil.isNotBlank(content)) {
|
||||
result.put("success", true);
|
||||
result.put("content", content.trim());
|
||||
log.info("生成文案成功: {}", productName);
|
||||
} else {
|
||||
throw new Exception("AI返回结果为空");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成文案失败", e);
|
||||
result.put("success", false);
|
||||
result.put("error", "生成文案失败: " + e.getMessage());
|
||||
// 降级方案:生成简单文案
|
||||
result.put("content", generateSimpleContent(productName, originalPrice, finalPrice));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键生成完整内容(关键词 + 文案 + 图片)
|
||||
*
|
||||
* @param productImageUrl 商品主图URL
|
||||
* @param productName 商品名称
|
||||
* @param originalPrice 原价
|
||||
* @param finalPrice 到手价
|
||||
* @param style 文案风格
|
||||
* @return 完整内容
|
||||
*/
|
||||
public Map<String, Object> generateCompleteContent(String productImageUrl, String productName,
|
||||
Double originalPrice, Double finalPrice, String style) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 1. 提取关键词
|
||||
Map<String, Object> keywordResult = extractKeywords(productName);
|
||||
List<String> keywords = (List<String>) keywordResult.get("keywords");
|
||||
String keywordsText = (String) keywordResult.get("keywordsText");
|
||||
|
||||
// 2. 生成文案
|
||||
Map<String, Object> contentResult = generateContent(
|
||||
productName, originalPrice, finalPrice, keywordsText, style
|
||||
);
|
||||
String content = (String) contentResult.get("content");
|
||||
|
||||
// 3. 生成营销图片
|
||||
String imageBase64 = null;
|
||||
if (StrUtil.isNotBlank(productImageUrl)) {
|
||||
try {
|
||||
// 使用提取的关键词作为商品名称显示
|
||||
String displayName = keywords != null && !keywords.isEmpty()
|
||||
? keywords.get(0)
|
||||
: productName;
|
||||
imageBase64 = marketingImageService.generateMarketingImage(
|
||||
productImageUrl, originalPrice, finalPrice, displayName
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.warn("生成营销图片失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
result.put("success", true);
|
||||
result.put("keywords", keywords);
|
||||
result.put("keywordsText", keywordsText);
|
||||
result.put("content", content);
|
||||
result.put("imageBase64", imageBase64);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成完整内容失败", e);
|
||||
result.put("success", false);
|
||||
result.put("error", "生成完整内容失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单提取关键词(降级方案)
|
||||
*/
|
||||
private List<String> simpleExtractKeywords(String productName) {
|
||||
List<String> keywords = new ArrayList<>();
|
||||
|
||||
// 移除常见规格信息
|
||||
String cleaned = productName
|
||||
.replaceAll("\\s*XL|L|M|S|XXL\\s*", "")
|
||||
.replaceAll("\\s*\\d+/\\d+[A-Z]?\\s*", "")
|
||||
.replaceAll("\\s*【.*?】\\s*", "")
|
||||
.replaceAll("\\s*\\(.*?\\)\\s*", "");
|
||||
|
||||
// 提取前几个词
|
||||
String[] words = cleaned.split("\\s+");
|
||||
for (String word : words) {
|
||||
if (word.length() >= 2 && word.length() <= 6 && keywords.size() < 5) {
|
||||
keywords.add(word);
|
||||
}
|
||||
}
|
||||
|
||||
return keywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Redis 获取提示词模板,如果没有则返回默认模板
|
||||
*
|
||||
* @param templateKey 模板键名(如:keywords, content:xhs, content:douyin, content:both)
|
||||
* @param defaultTemplate 默认模板
|
||||
* @return 提示词模板
|
||||
*/
|
||||
private String getPromptTemplate(String templateKey, String defaultTemplate) {
|
||||
if (redisTemplate == null) {
|
||||
log.debug("Redis未配置,使用默认模板: {}", templateKey);
|
||||
return defaultTemplate;
|
||||
}
|
||||
|
||||
try {
|
||||
String redisKey = REDIS_KEY_PREFIX + templateKey;
|
||||
String template = redisTemplate.opsForValue().get(redisKey);
|
||||
if (StrUtil.isNotBlank(template)) {
|
||||
log.debug("从Redis读取模板: {}", templateKey);
|
||||
return template;
|
||||
} else {
|
||||
log.debug("Redis中未找到模板,使用默认模板: {}", templateKey);
|
||||
return defaultTemplate;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("读取Redis模板失败,使用默认模板: {}", templateKey, e);
|
||||
return defaultTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成简单文案(降级方案)
|
||||
*/
|
||||
private String generateSimpleContent(String productName, Double originalPrice, Double finalPrice) {
|
||||
StringBuilder content = new StringBuilder();
|
||||
|
||||
content.append("🔥 ").append(productName).append("\n\n");
|
||||
|
||||
if (originalPrice != null && finalPrice != null && originalPrice > finalPrice) {
|
||||
content.append("💰 原价:¥").append(String.format("%.0f", originalPrice)).append("\n");
|
||||
content.append("💸 到手价:¥").append(String.format("%.0f", finalPrice)).append("\n");
|
||||
double discount = ((originalPrice - finalPrice) / originalPrice) * 100;
|
||||
content.append("✨ 立省:¥").append(String.format("%.0f", originalPrice - finalPrice))
|
||||
.append("(").append(String.format("%.0f", discount)).append("%)\n\n");
|
||||
}
|
||||
|
||||
content.append("💡 超值好物,不容错过!\n");
|
||||
content.append("🎁 限时优惠,先到先得!");
|
||||
|
||||
return content.toString();
|
||||
}
|
||||
}
|
||||
|
||||
1056
src/main/java/cn/van/business/util/DateUtil.java
Normal file
1056
src/main/java/cn/van/business/util/DateUtil.java
Normal file
File diff suppressed because it is too large
Load Diff
649
src/main/java/cn/van/business/util/JDProductService.java
Normal file
649
src/main/java/cn/van/business/util/JDProductService.java
Normal file
@@ -0,0 +1,649 @@
|
||||
package cn.van.business.util;
|
||||
|
||||
import cn.van.business.model.cj.XbMessage;
|
||||
import cn.van.business.model.cj.XbMessageItem;
|
||||
import cn.van.business.repository.XbMessageItemRepository;
|
||||
import cn.van.business.repository.XbMessageRepository;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.jd.open.api.sdk.DefaultJdClient;
|
||||
import com.jd.open.api.sdk.JdClient;
|
||||
import com.jd.open.api.sdk.domain.kplunion.CouponService.request.get.CreateGiftCouponReq;
|
||||
import com.jd.open.api.sdk.domain.kplunion.GoodsService.request.query.GoodsReq;
|
||||
import com.jd.open.api.sdk.domain.kplunion.GoodsService.response.query.GoodsQueryResult;
|
||||
import com.jd.open.api.sdk.domain.kplunion.GoodsService.response.query.UrlInfo;
|
||||
import com.jd.open.api.sdk.domain.kplunion.promotionbysubunioni.PromotionService.request.get.PromotionCodeReq;
|
||||
import com.jd.open.api.sdk.request.kplunion.UnionOpenCouponGiftGetRequest;
|
||||
import com.jd.open.api.sdk.request.kplunion.UnionOpenGoodsQueryRequest;
|
||||
import com.jd.open.api.sdk.request.kplunion.UnionOpenPromotionBysubunionidGetRequest;
|
||||
import com.jd.open.api.sdk.response.kplunion.UnionOpenCouponGiftGetResponse;
|
||||
import com.jd.open.api.sdk.response.kplunion.UnionOpenGoodsQueryResponse;
|
||||
import com.jd.open.api.sdk.response.kplunion.UnionOpenPromotionBysubunionidGetResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static cn.van.business.util.JDUtil.*;
|
||||
|
||||
/**
|
||||
* 京东商品服务类,抽取来源于JDUtil
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class JDProductService {
|
||||
|
||||
// 自己的98e21c89ae5610240ec3f5f575f86a59
|
||||
private static final String LPF_APP_KEY_WZ = "34407d6cae6d43eca740370b8e12b01e";
|
||||
// 自己的3dcb6b23a1104639ac433fd07adb6dfb
|
||||
private static final String LPF_SECRET_KEY_WZ = "ad4966e1df3348a185fe6b33aa679a69";
|
||||
private static final String SERVER_URL = "https://api.jd.com/routerjson";
|
||||
private static final String ACCESS_TOKEN = "";
|
||||
private static final Logger logger = LoggerFactory.getLogger(JDProductService.class);
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final XbMessageRepository xbMessageRepository;
|
||||
private final XbMessageItemRepository xbMessageItemRepository;
|
||||
|
||||
@Autowired
|
||||
public JDProductService(StringRedisTemplate redisTemplate,
|
||||
XbMessageRepository xbMessageRepository,
|
||||
XbMessageItemRepository xbMessageItemRepository) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.xbMessageRepository = xbMessageRepository;
|
||||
this.xbMessageItemRepository = xbMessageItemRepository;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成转链和方案的方法(JSON数组格式)
|
||||
*
|
||||
* @param message 方案内容,包含商品链接
|
||||
* @return 处理后的方案,以标准JSON数组格式返回,每个商品及其文案为一个独立对象
|
||||
*/
|
||||
public synchronized JSONArray generatePromotionContentAsJsonArray(String message) {
|
||||
JSONArray resultArray = new JSONArray();
|
||||
List<String> urls = extractUJDUrls(message);
|
||||
if (urls.isEmpty()) {
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put("error", "方案中未找到有效的商品链接,请检查格式是否正确。");
|
||||
resultArray.add(errorObj);
|
||||
return resultArray;
|
||||
}
|
||||
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒");
|
||||
|
||||
for (String url : urls) {
|
||||
try {
|
||||
String format = dateFormat.format(new Date());
|
||||
String originalUrlInText = url;
|
||||
String normalizedUrl = normalizeJdUrl(originalUrlInText);
|
||||
if (normalizedUrl == null) {
|
||||
log.warn("检测到的链接无法识别为合法京东链接,跳过处理: {}", originalUrlInText);
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put("url", originalUrlInText);
|
||||
errorObj.put("error", "链接格式不支持或识别失败");
|
||||
resultArray.add(errorObj);
|
||||
continue;
|
||||
}
|
||||
|
||||
GoodsQueryResult productInfo = queryProductInfoByUJDUrl(normalizedUrl);
|
||||
if (productInfo == null || productInfo.getCode() != 200 || productInfo.getData() == null || productInfo.getData().length == 0) {
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put("url", originalUrlInText);
|
||||
errorObj.put("error", "链接查询失败");
|
||||
resultArray.add(errorObj);
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject productObj = new JSONObject();
|
||||
productObj.put("originalUrl", originalUrlInText);
|
||||
productObj.put("normalizedUrl", normalizedUrl);
|
||||
|
||||
// 商品基本信息
|
||||
productObj.put("materialUrl", productInfo.getData()[0].getMaterialUrl());
|
||||
productObj.put("oriItemId", productInfo.getData()[0].getOriItemId());
|
||||
productObj.put("owner", productInfo.getData()[0].getOwner());
|
||||
productObj.put("shopId", String.valueOf(productInfo.getData()[0].getShopInfo().getShopId()));
|
||||
productObj.put("shopName", productInfo.getData()[0].getShopInfo().getShopName());
|
||||
productObj.put("skuName", productInfo.getData()[0].getSkuName());
|
||||
String cleanSkuName = productInfo.getData()[0].getSkuName().replaceAll("以旧|政府|换新|领取|国家|补贴|15%|20%|国补|立减|【|】", "");
|
||||
productObj.put("cleanSkuName", cleanSkuName);
|
||||
productObj.put("spuid", String.valueOf(productInfo.getData()[0].getSpuid()));
|
||||
productObj.put("skuId", String.valueOf(productInfo.getData()[0].getSkuId()));
|
||||
productObj.put("commission", String.valueOf(productInfo.getData()[0].getCommissionInfo().getCommission()));
|
||||
productObj.put("commissionShare", String.valueOf(productInfo.getData()[0].getCommissionInfo().getCommissionShare()));
|
||||
if (productInfo.getData()[0].getPriceInfo() != null && productInfo.getData()[0].getPriceInfo().getPrice() != null) {
|
||||
productObj.put("price", String.valueOf(productInfo.getData()[0].getPriceInfo().getPrice()));
|
||||
}
|
||||
|
||||
JSONArray imageArray = new JSONArray();
|
||||
if (productInfo.getData()[0].getImageInfo() != null &&
|
||||
productInfo.getData()[0].getImageInfo().getImageList() != null) {
|
||||
for (UrlInfo image : productInfo.getData()[0].getImageInfo().getImageList()) {
|
||||
imageArray.add(image.getUrl());
|
||||
}
|
||||
}
|
||||
productObj.put("images", imageArray);
|
||||
|
||||
// 生成转链后的短链
|
||||
try {
|
||||
String shortUrl = transfer(normalizedUrl, null);
|
||||
String effectiveUrl = normalizedUrl;
|
||||
if (shortUrl != null && !shortUrl.isEmpty()) {
|
||||
productObj.put("shortUrl", shortUrl);
|
||||
productObj.put("transferSuccess", true);
|
||||
effectiveUrl = shortUrl;
|
||||
} else {
|
||||
productObj.put("shortUrl", normalizedUrl); // 如果转链失败,使用归一化后的链接
|
||||
productObj.put("transferSuccess", false);
|
||||
log.warn("转链失败,使用原链接: {}", normalizedUrl);
|
||||
}
|
||||
productObj.put("effectiveUrl", effectiveUrl);
|
||||
} catch (Exception e) {
|
||||
log.error("生成转链时发生异常: {}", normalizedUrl, e);
|
||||
productObj.put("shortUrl", normalizedUrl); // 转链异常时使用原链接
|
||||
productObj.put("transferSuccess", false);
|
||||
productObj.put("transferError", e.getMessage());
|
||||
productObj.put("effectiveUrl", normalizedUrl);
|
||||
}
|
||||
|
||||
// 文案信息
|
||||
JSONArray wenanArray = new JSONArray();
|
||||
|
||||
String title = "";
|
||||
try {
|
||||
if (!message.equals(url)) {
|
||||
String[] lines = message.split("\\r?\\n");
|
||||
if (lines.length > 0) {
|
||||
title = lines[0];
|
||||
if (lines.length > 1 && lines[1].length() > 3 && !lines[1].contains("u.jd")) {
|
||||
title = title + lines[1];
|
||||
}
|
||||
title = title.replaceAll("@|所有人", "");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("文案首行异常", e);
|
||||
}
|
||||
|
||||
// 生成各种文案
|
||||
JSONObject wenan1 = new JSONObject();
|
||||
wenan1.put("type", "标价到手-方案1");
|
||||
wenan1.put("content", "(标价到手) " + title + cleanSkuName + "\n" + WENAN_FANAN_LQD.replaceAll("更新", format + "更新"));
|
||||
wenanArray.add(wenan1);
|
||||
|
||||
JSONObject wenan2 = new JSONObject();
|
||||
wenan2.put("type", "一键代下");
|
||||
wenan2.put("content", "(一键代下) " + title + cleanSkuName + "\n" + WENAN_ZCXS);
|
||||
wenanArray.add(wenan2);
|
||||
|
||||
JSONObject wenan3 = new JSONObject();
|
||||
wenan3.put("type", "标价到手-方案2");
|
||||
wenan3.put("content", "(标价到手) " + title + cleanSkuName + "\n" + WENAN_FANAN_HG.replaceAll("更新", format + "更新"));
|
||||
wenanArray.add(wenan3);
|
||||
|
||||
JSONObject wenan4 = new JSONObject();
|
||||
wenan4.put("type", "教你下单");
|
||||
wenan4.put("content", "【教你下单】 " + title + cleanSkuName + "\n" + WENAN_FANAN_BX.replaceAll("信息更新日期:", "信息更新日期:" + format));
|
||||
wenanArray.add(wenan4);
|
||||
|
||||
|
||||
JSONObject wenan5 = new JSONObject();
|
||||
wenan5.put("type", "羽绒服专属-标价到手"); // type明确产品类型+下单方式,与其他方案区分
|
||||
wenan5.put("content", "(羽绒服专属) " + title + cleanSkuName + "\n" + WENAN_YURONGFU.replaceAll("更新", format + "更新"));
|
||||
wenanArray.add(wenan5);
|
||||
|
||||
productObj.put("wenan", wenanArray);
|
||||
|
||||
// 添加通用文案 - 使用转链后的短链替换原始链接
|
||||
JSONObject commonWenan = new JSONObject();
|
||||
commonWenan.put("type", "通用文案");
|
||||
// 将原始消息中的链接替换为转链后的短链
|
||||
String targetUrl = productObj.getString("effectiveUrl");
|
||||
String normalizedForReplace = productObj.getString("normalizedUrl");
|
||||
String messageWithShortUrl = message;
|
||||
if (targetUrl != null) {
|
||||
String replaced = message.replace(originalUrlInText, targetUrl);
|
||||
if (replaced.equals(message) && normalizedForReplace != null) {
|
||||
replaced = replaced.replace(normalizedForReplace, targetUrl);
|
||||
}
|
||||
messageWithShortUrl = replaced;
|
||||
}
|
||||
commonWenan.put("content", format + FANAN_COMMON + messageWithShortUrl);
|
||||
wenanArray.add(commonWenan);
|
||||
|
||||
productObj.put("wenan", wenanArray);
|
||||
resultArray.add(productObj);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理商品链接时发生异常:{}", url, e);
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put("url", url);
|
||||
errorObj.put("error", "处理商品链接时发生异常:" + e.getMessage());
|
||||
resultArray.add(errorObj);
|
||||
}
|
||||
}
|
||||
|
||||
return resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询商品信息
|
||||
*
|
||||
* @param uJDUrl 京东商品链接
|
||||
* @return 商品查询结果
|
||||
* @throws Exception 查询异常
|
||||
*/
|
||||
public GoodsQueryResult queryProductInfoByUJDUrl(String uJDUrl) throws Exception {
|
||||
UnionOpenGoodsQueryResponse response = getUnionOpenGoodsQueryRequest(uJDUrl);
|
||||
if (response == null || response.getQueryResult() == null) return null;
|
||||
GoodsQueryResult queryResult = response.getQueryResult();
|
||||
if (queryResult.getCode() != 200) return null;
|
||||
return queryResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用京东开放平台接口查询商品信息
|
||||
*
|
||||
* @param uJDUrl 京东商品链接
|
||||
* @return 京东商品查询响应
|
||||
* @throws Exception 查询异常
|
||||
*/
|
||||
public UnionOpenGoodsQueryResponse getUnionOpenGoodsQueryRequest(String uJDUrl) throws Exception {
|
||||
JdClient client = new DefaultJdClient(SERVER_URL, ACCESS_TOKEN, LPF_APP_KEY_WZ, LPF_SECRET_KEY_WZ);
|
||||
|
||||
UnionOpenGoodsQueryRequest request = new UnionOpenGoodsQueryRequest();
|
||||
GoodsReq goodsReq = new GoodsReq();
|
||||
goodsReq.setKeyword(uJDUrl);
|
||||
goodsReq.setSceneId(1);
|
||||
request.setGoodsReqDTO(goodsReq);
|
||||
request.setVersion("1.0");
|
||||
request.setSignmethod("md5");
|
||||
request.setTimestamp(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
||||
return client.execute(request);
|
||||
}
|
||||
|
||||
public String createGiftCoupon(String skuId, double amount, int quantity, String owner, String skuName) throws Exception {
|
||||
log.debug("准备创建礼金:SKU={}, 金额={}元,数量={}, Owner={}", skuId, amount, quantity, owner);
|
||||
if (skuId == null || skuId.trim().isEmpty() || amount <= 0 || quantity <= 0) {
|
||||
log.error("礼金创建失败:参数错误,SKU={}, 金额={}元,数量={}", skuId, amount, quantity);
|
||||
return null;
|
||||
}
|
||||
owner = (owner != null && !owner.isEmpty()) ? owner : "g";
|
||||
|
||||
JdClient client = new DefaultJdClient(SERVER_URL, ACCESS_TOKEN, LPF_APP_KEY_WZ, LPF_SECRET_KEY_WZ);
|
||||
|
||||
UnionOpenCouponGiftGetRequest request = new UnionOpenCouponGiftGetRequest();
|
||||
CreateGiftCouponReq couponReq = new CreateGiftCouponReq();
|
||||
couponReq.setSkuMaterialId(skuId);
|
||||
couponReq.setDiscount(amount);
|
||||
couponReq.setAmount(quantity);
|
||||
|
||||
String startTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH"));
|
||||
String endTime;
|
||||
if ("pop".equalsIgnoreCase(owner)) {
|
||||
endTime = LocalDateTime.now().plusDays(6).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH"));
|
||||
couponReq.setEffectiveDays(7);
|
||||
} else {
|
||||
endTime = LocalDateTime.now().plusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH"));
|
||||
couponReq.setEffectiveDays(1);
|
||||
}
|
||||
|
||||
couponReq.setReceiveStartTime(startTime);
|
||||
couponReq.setReceiveEndTime(endTime);
|
||||
couponReq.setIsSpu(1);
|
||||
couponReq.setExpireType(1);
|
||||
couponReq.setShare(-1);
|
||||
|
||||
if (skuName == null) skuName = "";
|
||||
if (skuName.length() > 25) skuName = skuName.substring(0, 25);
|
||||
skuName = skuName + " " + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
couponReq.setCouponTitle(skuName);
|
||||
couponReq.setContentMatch(1);
|
||||
|
||||
request.setCouponReq(couponReq);
|
||||
request.setVersion("1.0");
|
||||
|
||||
UnionOpenCouponGiftGetResponse response = client.execute(request);
|
||||
if ("0".equals(response.getCode()) && response.getGetResult() != null && response.getGetResult().getCode() == 200) {
|
||||
String giftKey = response.getGetResult().getData().getGiftCouponKey();
|
||||
log.debug("礼金创建成功:giftKey={}", giftKey);
|
||||
return giftKey;
|
||||
}
|
||||
// 详细记录失败信息
|
||||
String errorCode = response != null ? response.getCode() : "null";
|
||||
String errorMsg = response != null ? response.getMsg() : "null";
|
||||
Integer resultCode = response != null && response.getGetResult() != null ? response.getGetResult().getCode() : null;
|
||||
|
||||
// 尝试解析response.getMsg()中的详细错误信息(JSON格式)
|
||||
String detailErrorMsg = errorMsg;
|
||||
Integer detailCode = resultCode;
|
||||
boolean parsedDetail = false;
|
||||
|
||||
try {
|
||||
if (errorMsg != null && errorMsg.startsWith("{") && errorMsg.contains("message")) {
|
||||
// 解析外层JSON
|
||||
JSONObject outerJson = JSON.parseObject(errorMsg);
|
||||
if (outerJson.containsKey("jd_union_open_coupon_gift_get_responce")) {
|
||||
JSONObject innerObj = outerJson.getJSONObject("jd_union_open_coupon_gift_get_responce");
|
||||
if (innerObj.containsKey("getResult")) {
|
||||
String getResultStr = innerObj.getString("getResult");
|
||||
// getResult是一个JSON字符串,需要再次解析
|
||||
if (getResultStr != null && getResultStr.startsWith("{")) {
|
||||
JSONObject resultJson = JSON.parseObject(getResultStr);
|
||||
if (resultJson.containsKey("message")) {
|
||||
detailErrorMsg = resultJson.getString("message");
|
||||
Integer parsedDetailCode = resultJson.getInteger("code");
|
||||
if (parsedDetailCode != null) {
|
||||
detailCode = parsedDetailCode;
|
||||
}
|
||||
parsedDetail = true;
|
||||
log.error("礼金创建失败 - 京东API错误: code={}, message={}, SKU={}, owner={}, amount={}, quantity={}",
|
||||
detailCode, detailErrorMsg, skuId, owner, amount, quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception parseEx) {
|
||||
// JSON解析失败,使用原始错误信息
|
||||
log.warn("解析错误信息失败,使用原始信息: {}", errorMsg);
|
||||
}
|
||||
|
||||
// 记录日志并抛出包含详细错误信息的异常
|
||||
if (!parsedDetail) {
|
||||
log.error("礼金创建失败 - response.code={}, response.msg={}, result.code={}, SKU={}, owner={}, amount={}, quantity={}",
|
||||
errorCode, errorMsg, resultCode, skuId, owner, amount, quantity);
|
||||
}
|
||||
|
||||
// 构造错误消息
|
||||
String finalErrorMsg;
|
||||
if (parsedDetail) {
|
||||
finalErrorMsg = String.format("礼金创建失败:%s (错误码:%d)", detailErrorMsg, detailCode);
|
||||
} else {
|
||||
finalErrorMsg = String.format("礼金创建失败:京东API返回错误 (response.code=%s, result.code=%s, msg=%s)",
|
||||
errorCode, resultCode != null ? resultCode : "null", detailErrorMsg);
|
||||
}
|
||||
|
||||
throw new Exception(finalErrorMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转链接口:通过商品链接、领券链接、活动链接获取普通推广链接或优惠券二合一推广链接
|
||||
*
|
||||
* @param url 原始链接
|
||||
* @param giftCouponKey 礼金Key(可选)
|
||||
* @return 转换后的短链接
|
||||
*/
|
||||
public String transfer(String url, String giftCouponKey) {
|
||||
JdClient client = new DefaultJdClient(SERVER_URL, ACCESS_TOKEN, LPF_APP_KEY_WZ, LPF_SECRET_KEY_WZ);
|
||||
|
||||
UnionOpenPromotionBysubunionidGetRequest request = new UnionOpenPromotionBysubunionidGetRequest();
|
||||
PromotionCodeReq promotionCodeReq = new PromotionCodeReq();
|
||||
promotionCodeReq.setSceneId(1);
|
||||
promotionCodeReq.setMaterialId(url);
|
||||
if (giftCouponKey != null && !giftCouponKey.isEmpty()) {
|
||||
promotionCodeReq.setGiftCouponKey(giftCouponKey);
|
||||
}
|
||||
|
||||
request.setPromotionCodeReq(promotionCodeReq);
|
||||
request.setVersion("1.0");
|
||||
|
||||
try {
|
||||
UnionOpenPromotionBysubunionidGetResponse response = client.execute(request);
|
||||
if (response != null && "0".equals(response.getCode()) &&
|
||||
response.getGetResult() != null && response.getGetResult().getCode() == 200) {
|
||||
return response.getGetResult().getData().getShortURL();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将礼金信息写入Redis
|
||||
*
|
||||
* @param skuId 商品SKU ID
|
||||
* @param giftKey 礼金Key
|
||||
* @param skuName 商品名称
|
||||
* @param owner 商品所有者
|
||||
*/
|
||||
public void saveGiftCouponToRedis(String skuId, String giftKey, String skuName, String owner) {
|
||||
String key = "gift_coupon:" + skuId;
|
||||
String hashKey = giftKey;
|
||||
LocalDateTime expireTime = LocalDateTime.now().plus("g".equals(owner) ? 0 : 7, java.time.temporal.ChronoUnit.DAYS);
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("giftKey", giftKey);
|
||||
data.put("skuName", skuName);
|
||||
data.put("owner", owner);
|
||||
data.put("expireTime", expireTime.format(DateTimeFormatter.ISO_DATE_TIME));
|
||||
|
||||
// 存入 Redis Hash
|
||||
redisTemplate.opsForHash().put(key, hashKey, JSON.toJSONString(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取商品价格信息
|
||||
*
|
||||
* @param productInfo 商品查询结果
|
||||
* @return 包含价格信息的Map
|
||||
*/
|
||||
public Map<String, Object> extractPriceInfo(GoodsQueryResult productInfo) {
|
||||
Map<String, Object> priceMap = new HashMap<>();
|
||||
if (productInfo == null || productInfo.getData() == null || productInfo.getData().length == 0) {
|
||||
priceMap.put("error", "商品信息为空");
|
||||
return priceMap;
|
||||
}
|
||||
try {
|
||||
var priceInfo = productInfo.getData()[0].getPriceInfo();
|
||||
if (priceInfo != null) {
|
||||
priceMap.put("price", priceInfo.getPrice());
|
||||
priceMap.put("lowestPrice", priceInfo.getLowestPrice());
|
||||
priceMap.put("lowestCouponPrice", priceInfo.getLowestCouponPrice());
|
||||
priceMap.put("lowestPriceType", priceInfo.getLowestPriceType());
|
||||
if (priceInfo.getPrice() != null && priceInfo.getLowestCouponPrice() != null) {
|
||||
double discount = priceInfo.getPrice() - priceInfo.getLowestCouponPrice();
|
||||
priceMap.put("discount", discount);
|
||||
}
|
||||
priceMap.put("priceFormatted", "¥" + (priceInfo.getPrice() != null ? priceInfo.getPrice() / 100.0 : 0));
|
||||
priceMap.put("lowestPriceFormatted", "¥" + (priceInfo.getLowestPrice() != null ? priceInfo.getLowestPrice() / 100.0 : 0));
|
||||
priceMap.put("lowestCouponPriceFormatted", "¥" + (priceInfo.getLowestCouponPrice() != null ? priceInfo.getLowestCouponPrice() / 100.0 : 0));
|
||||
} else {
|
||||
priceMap.put("error", "价格信息为空");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("提取价格信息时发生异常", e);
|
||||
priceMap.put("error", "提取价格信息时发生异常: " + e.getMessage());
|
||||
}
|
||||
return priceMap;
|
||||
}
|
||||
|
||||
public Map<String, Object> extractPriceInfoByUrl(String uJDUrl) {
|
||||
try {
|
||||
GoodsQueryResult productInfo = queryProductInfoByUJDUrl(uJDUrl);
|
||||
return extractPriceInfo(productInfo);
|
||||
} catch (Exception e) {
|
||||
log.error("通过链接提取价格信息时发生异常: {}", uJDUrl, e);
|
||||
Map<String, Object> errorMap = new HashMap<>();
|
||||
errorMap.put("error", "通过链接提取价格信息时发生异常: " + e.getMessage());
|
||||
return errorMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建礼金券并生成包含礼金的推广链接
|
||||
*
|
||||
* @param skuId 商品SKU ID或materialUrl
|
||||
* @param amount 礼金金额(单位:元)
|
||||
* @param quantity 每个礼金券的数量
|
||||
* @param batchSize 批量创建的个数(默认20)
|
||||
* @param owner 商品类型(g=自营,pop=POP)
|
||||
* @param skuName 商品名称
|
||||
* @return 批量创建结果列表,包含giftCouponKey和shortURL
|
||||
*/
|
||||
public List<Map<String, Object>> batchCreateGiftCouponsWithLinks(String skuId, double amount, int quantity, int batchSize, String owner, String skuName) {
|
||||
log.info("开始批量创建礼金券 - SKU={}, 金额={}元, 数量={}, 批次大小={}, Owner={}", skuId, amount, quantity, batchSize, owner);
|
||||
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (int i = 0; i < batchSize; i++) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("index", i + 1);
|
||||
result.put("success", false);
|
||||
|
||||
try {
|
||||
// 创建礼金券
|
||||
String giftCouponKey = createGiftCoupon(skuId, amount, quantity, owner, skuName);
|
||||
|
||||
if (giftCouponKey == null || giftCouponKey.trim().isEmpty()) {
|
||||
log.error("批量创建礼金券失败 [{}/{}] - giftCouponKey为空", i + 1, batchSize);
|
||||
result.put("error", "礼金创建失败,giftCouponKey为空");
|
||||
result.put("giftCouponKey", null);
|
||||
result.put("shortURL", null);
|
||||
failCount++;
|
||||
} else {
|
||||
log.info("批量创建礼金券成功 [{}/{}] - giftCouponKey={}", i + 1, batchSize, giftCouponKey);
|
||||
|
||||
// 保存到Redis
|
||||
try {
|
||||
saveGiftCouponToRedis(skuId, giftCouponKey, skuName, owner);
|
||||
} catch (Exception e) {
|
||||
log.warn("保存礼金到Redis失败,但礼金创建成功 - giftCouponKey={}, error={}", giftCouponKey, e.getMessage());
|
||||
}
|
||||
|
||||
// 生成包含礼金的推广链接
|
||||
String shortURL = null;
|
||||
try {
|
||||
// 使用materialUrl或skuId作为原始链接
|
||||
String originalUrl = skuId;
|
||||
// transfer方法支持SKU ID或materialUrl直接传入
|
||||
shortURL = transfer(originalUrl, giftCouponKey);
|
||||
|
||||
if (shortURL == null || shortURL.trim().isEmpty()) {
|
||||
log.warn("生成推广链接失败 - giftCouponKey={}, 礼金创建成功但转链失败", giftCouponKey);
|
||||
} else {
|
||||
log.info("生成推广链接成功 [{}/{}] - giftCouponKey={}, shortURL={}", i + 1, batchSize, giftCouponKey, shortURL);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("生成推广链接异常 - giftCouponKey={}, error={}", giftCouponKey, e.getMessage(), e);
|
||||
}
|
||||
|
||||
result.put("success", true);
|
||||
result.put("giftCouponKey", giftCouponKey);
|
||||
result.put("shortURL", shortURL);
|
||||
successCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("批量创建礼金券异常 [{}/{}] - error={}", i + 1, batchSize, e.getMessage(), e);
|
||||
result.put("error", "创建异常: " + e.getMessage());
|
||||
result.put("giftCouponKey", null);
|
||||
result.put("shortURL", null);
|
||||
failCount++;
|
||||
}
|
||||
|
||||
results.add(result);
|
||||
|
||||
// 避免请求过快,每创建一张礼金后稍作延迟
|
||||
if (i < batchSize - 1) {
|
||||
try {
|
||||
Thread.sleep(100); // 延迟100ms
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("批量创建礼金券延迟被中断");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("批量创建礼金券完成 - 总数={}, 成功={}, 失败={}", batchSize, successCount, failCount);
|
||||
return results;
|
||||
}
|
||||
|
||||
private static final Pattern UJD_LINK_PATTERN = Pattern.compile("^https?://u\\.jd\\.com/[A-Za-z0-9]+[A-Za-z0-9_-]*$", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern JINGFEN_LINK_PATTERN = Pattern.compile("^https?://jingfen\\.jd\\.com/detail/[A-Za-z0-9]+\\.html$", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern TRAILING_SYMBOLS_PATTERN = Pattern.compile("[))】>》。,;;!!??“”\"'、…—\\s]+$");
|
||||
|
||||
private static String normalizeJdUrl(String rawUrl) {
|
||||
if (rawUrl == null || rawUrl.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = rawUrl.trim();
|
||||
|
||||
// 截断常见中文/英文括号后的内容
|
||||
int cutoffIndex = findCutoffIndex(trimmed);
|
||||
if (cutoffIndex > -1) {
|
||||
trimmed = trimmed.substring(0, cutoffIndex);
|
||||
}
|
||||
|
||||
// 去掉末尾的标点符号
|
||||
trimmed = TRAILING_SYMBOLS_PATTERN.matcher(trimmed).replaceAll("");
|
||||
if (trimmed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) {
|
||||
trimmed = "https://" + trimmed;
|
||||
}
|
||||
|
||||
if (UJD_LINK_PATTERN.matcher(trimmed).matches() || JINGFEN_LINK_PATTERN.matcher(trimmed).matches()) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
// 针对 u.jd.com 链接,尝试进一步截断到第一个不合法字符
|
||||
if (trimmed.contains("u.jd.com/")) {
|
||||
int schemeEnd = trimmed.indexOf("u.jd.com/") + "u.jd.com/".length();
|
||||
StringBuilder sb = new StringBuilder(trimmed.substring(0, schemeEnd));
|
||||
for (int i = schemeEnd; i < trimmed.length(); i++) {
|
||||
char c = trimmed.charAt(i);
|
||||
if (isAllowedShortLinkChar(c)) {
|
||||
sb.append(c);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String candidate = sb.toString();
|
||||
if (UJD_LINK_PATTERN.matcher(candidate).matches()) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int findCutoffIndex(String text) {
|
||||
char[] stopChars = new char[]{'(', '(', '[', '【', '<', '《', '「', '『'};
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (Character.isWhitespace(c)) {
|
||||
return i;
|
||||
}
|
||||
for (char stopChar : stopChars) {
|
||||
if (c == stopChar) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean isAllowedShortLinkChar(char c) {
|
||||
return (c >= 'A' && c <= 'Z')
|
||||
|| (c >= 'a' && c <= 'z')
|
||||
|| (c >= '0' && c <= '9')
|
||||
|| c == '-' || c == '_' || c == '.';
|
||||
}
|
||||
}
|
||||
718
src/main/java/cn/van/business/util/JDScheduleJob.java
Normal file
718
src/main/java/cn/van/business/util/JDScheduleJob.java
Normal file
@@ -0,0 +1,718 @@
|
||||
package cn.van.business.util;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.van.business.model.jd.OrderRow;
|
||||
import cn.van.business.model.pl.Comment;
|
||||
import cn.van.business.model.wx.SuperAdmin;
|
||||
import cn.van.business.repository.CommentRepository;
|
||||
import cn.van.business.repository.OrderRowRepository;
|
||||
import cn.van.business.util.jdReq.*;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.alibaba.fastjson2.util.DateUtils;
|
||||
import com.jd.open.api.sdk.DefaultJdClient;
|
||||
import com.jd.open.api.sdk.JdClient;
|
||||
import com.jd.open.api.sdk.domain.kplunion.OrderService.request.query.OrderRowReq;
|
||||
import com.jd.open.api.sdk.domain.kplunion.OrderService.response.query.OrderRowResp;
|
||||
import com.jd.open.api.sdk.request.kplunion.UnionOpenOrderRowQueryRequest;
|
||||
import com.jd.open.api.sdk.response.kplunion.UnionOpenOrderRowQueryResponse;
|
||||
import lombok.Getter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.HashOperations;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static cn.van.business.util.JDUtil.DATE_TIME_FORMATTER;
|
||||
import static cn.van.business.util.WXUtil.super_admins;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2025/3/16 17:01
|
||||
* @description:
|
||||
*/
|
||||
@Component
|
||||
public class JDScheduleJob {
|
||||
private static final Logger logger = LoggerFactory.getLogger(JDScheduleJob.class);
|
||||
private static final String SERVER_URL = "https://api.jd.com/routerjson";
|
||||
//accessToken
|
||||
private static final String ACCESS_TOKEN = "";
|
||||
|
||||
// 标记是否拉取过小时的订单,空订单会set 一个 tag,避免重复拉取
|
||||
private static final String JD_REFRESH_TAG = "jd:refresh:tag:";
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final OrderRowRepository orderRowRepository;
|
||||
private final OrderUtil orderUtil;
|
||||
private final JDUtil jdUtil;
|
||||
private final CommentRepository commentRepository;
|
||||
private final WXUtil wxUtil;
|
||||
|
||||
|
||||
@Getter
|
||||
@Value("${isRunning.wx}")
|
||||
private String isRunning_wx;
|
||||
|
||||
@Getter
|
||||
@Value("${isRunning.jd}")
|
||||
private String isRunning_jd;
|
||||
|
||||
// 构造函数中注入StringRedisTemplate
|
||||
@Autowired
|
||||
public JDScheduleJob(WXUtil wxUtil, StringRedisTemplate redisTemplate, OrderRowRepository orderRowRepository, OrderUtil orderUtil, JDUtil jdUtil, CommentRepository commentRepository) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.orderRowRepository = orderRowRepository;
|
||||
this.orderUtil = orderUtil;
|
||||
this.jdUtil = jdUtil;
|
||||
this.commentRepository = commentRepository;
|
||||
this.wxUtil = wxUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 响应参数转化为 OrderRow,并返回
|
||||
*/
|
||||
private OrderRow createOrderRow(OrderRowResp orderRowResp) {
|
||||
OrderRow orderRow = new OrderRow();
|
||||
orderRow.setOrderId(orderRowResp.getOrderId());
|
||||
orderRow.setSkuId(orderRowResp.getSkuId());
|
||||
orderRow.setSkuName(orderRowResp.getSkuName());
|
||||
orderRow.setItemId(orderRowResp.getItemId());
|
||||
orderRow.setSkuNum(orderRowResp.getSkuNum());
|
||||
orderRow.setPrice(orderRowResp.getPrice());
|
||||
orderRow.setActualCosPrice(orderRowResp.getActualCosPrice());
|
||||
orderRow.setActualFee(orderRowResp.getActualFee());
|
||||
orderRow.setEstimateCosPrice(orderRowResp.getEstimateCosPrice());
|
||||
orderRow.setEstimateFee(orderRowResp.getEstimateFee());
|
||||
orderRow.setSubSideRate(orderRowResp.getSubSideRate());
|
||||
orderRow.setSubsidyRate(orderRowResp.getSubsidyRate());
|
||||
orderRow.setCommissionRate(orderRowResp.getCommissionRate());
|
||||
orderRow.setFinalRate(orderRowResp.getFinalRate());
|
||||
|
||||
orderRow.setOrderTime(DateUtils.parseDate(orderRowResp.getOrderTime()));
|
||||
orderRow.setFinishTime(DateUtils.parseDate(orderRowResp.getFinishTime()));
|
||||
orderRow.setOrderTag(orderRowResp.getOrderTag());
|
||||
orderRow.setOrderEmt(orderRowResp.getOrderEmt());
|
||||
orderRow.setUnionId(orderRowResp.getUnionId());
|
||||
orderRow.setUnionRole(orderRowResp.getUnionRole());
|
||||
orderRow.setUnionAlias(orderRowResp.getUnionAlias());
|
||||
orderRow.setUnionTag(orderRowResp.getUnionTag());
|
||||
orderRow.setTraceType(orderRowResp.getTraceType());
|
||||
orderRow.setValidCode(orderRowResp.getValidCode());
|
||||
orderRow.setPayMonth(orderRowResp.getPayMonth());
|
||||
orderRow.setSiteId(orderRowResp.getSiteId());
|
||||
orderRow.setParentId(orderRowResp.getParentId());
|
||||
//GoodsInfo goodsInfo = orderRowResp.getGoodsInfo();
|
||||
//GoodsInfoVO goodsInfoVO = new GoodsInfoVO();
|
||||
//goodsInfoVO.setShopId(String.valueOf(goodsInfo.getShopId()));
|
||||
//goodsInfoVO.setShopName(goodsInfo.getShopName());
|
||||
//goodsInfoVO.setOwner(goodsInfo.getOwner());
|
||||
//goodsInfoVO.setProductId(String.valueOf(goodsInfo.getProductId()));
|
||||
//goodsInfoVO.setImageUrl(goodsInfo.getImageUrl());
|
||||
//orderRow.setGoodsInfo(goodsInfoVO);
|
||||
orderRow.setCallerItemId(orderRowResp.getCallerItemId());
|
||||
orderRow.setPid(orderRowResp.getPid());
|
||||
orderRow.setCid1(orderRowResp.getCid1());
|
||||
orderRow.setCid2(orderRowResp.getCid2());
|
||||
orderRow.setCid3(orderRowResp.getCid3());
|
||||
orderRow.setChannelId(orderRowResp.getChannelId());
|
||||
orderRow.setProPriceAmount(orderRowResp.getProPriceAmount());
|
||||
orderRow.setSkuFrozenNum(orderRowResp.getSkuFrozenNum());
|
||||
orderRow.setSkuReturnNum(orderRowResp.getSkuReturnNum());
|
||||
orderRow.setSkuTag(orderRowResp.getSkuTag());
|
||||
orderRow.setPositionId(orderRowResp.getPositionId());
|
||||
orderRow.setPopId(orderRowResp.getPopId());
|
||||
orderRow.setRid(orderRowResp.getRid());
|
||||
orderRow.setPlus(orderRowResp.getPlus());
|
||||
orderRow.setCpActId(orderRowResp.getCpActId());
|
||||
orderRow.setGiftCouponKey(orderRowResp.getGiftCouponKey());
|
||||
|
||||
orderRow.setModifyTime(new Date());
|
||||
orderRow.setSign(orderRowResp.getSign());
|
||||
orderRow.setBalanceExt(orderRowResp.getBalanceExt());
|
||||
orderRow.setExpressStatus(orderRowResp.getExpressStatus());
|
||||
orderRow.setExt1(orderRowResp.getExt1());
|
||||
orderRow.setSubUnionId(orderRowResp.getSubUnionId());
|
||||
orderRow.setGiftCouponOcsAmount(orderRowResp.getGiftCouponOcsAmount());
|
||||
orderRow.setTraceType(orderRowResp.getTraceType());
|
||||
orderRow.setExpressStatus(orderRowResp.getExpressStatus());
|
||||
orderRow.setTraceType(orderRowResp.getTraceType());
|
||||
orderRow.setId(orderRowResp.getId());
|
||||
orderRow.setValidCode(orderRowResp.getValidCode());
|
||||
orderRow.setExpressStatus(orderRowResp.getExpressStatus());
|
||||
orderRow.setTraceType(orderRowResp.getTraceType());
|
||||
return orderRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定的日期时间拉取订单
|
||||
*
|
||||
* @param startTime 开始时间
|
||||
* isRealTime 是否是实时订单 是的话不会判断是否拉取过
|
||||
* page 分页页码
|
||||
* isRealTime 是否是分钟级订单 分钟的每次加10分钟,小时每小时加1小时
|
||||
*/
|
||||
public UnionOpenOrderRowQueryResponse fetchOrdersForDateTime(LocalDateTime startTime, boolean isRealTime, Integer page, boolean isMinutes, String appKey, String secretKey) {
|
||||
|
||||
LocalDateTime endTime = isMinutes ? startTime.plusMinutes(10) : startTime.plusHours(1);
|
||||
String hourMinuteTag = isRealTime ? "minute" : "hour";
|
||||
//String oldTimeTag = JD_REFRESH_TAG + startTime.format(DATE_TIME_FORMATTER);
|
||||
|
||||
String newTimeTag = JD_REFRESH_TAG + appKey + ":" + startTime.format(DATE_TIME_FORMATTER);
|
||||
|
||||
HashOperations<String, String, String> hashOps = redisTemplate.opsForHash();
|
||||
|
||||
// 检查这个小时或分钟是否已经被处理过
|
||||
if (hashOps.hasKey(newTimeTag, hourMinuteTag)) {
|
||||
// 0007需要暴力拉取
|
||||
if (!isMinutes && !isRealTime) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 调用 API 以拉取订单
|
||||
try {
|
||||
UnionOpenOrderRowQueryResponse unionOpenOrderRowQueryResponse = getUnionOpenOrderRowQueryResponse(startTime, endTime, page, appKey, secretKey);
|
||||
// 历史的订单才进行标记为已拉取,小时分钟的都进行拉取并且标记
|
||||
if (!isRealTime) {
|
||||
// 只有没有订单的才进行标记为已拉取
|
||||
if (unionOpenOrderRowQueryResponse != null && unionOpenOrderRowQueryResponse.getQueryResult() != null && unionOpenOrderRowQueryResponse.getQueryResult().getData() == null) {
|
||||
hashOps.put(newTimeTag, hourMinuteTag, "done");
|
||||
logger.info(" 账号 {} -- 没有订单 -- 开始时间:{} --- 结束时间:{}", appKey.substring(appKey.length() - 4), startTime.format(DATE_TIME_FORMATTER), endTime.format(DATE_TIME_FORMATTER));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 打印方法调用和开始结束时间
|
||||
//if (isRealTime && (LocalDateTime.now().getMinute() % 10 == 0)) {
|
||||
// logger.debug(" {} --- 拉取订单, 分钟还是秒 {} , 开始时间:{} --- 结束时间:{}", appKey.substring(appKey.length() - 4), hourMinuteTag, startTime.format(DATE_TIME_FORMATTER), endTime.format(DATE_TIME_FORMATTER));
|
||||
//}
|
||||
|
||||
return unionOpenOrderRowQueryResponse;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 响应校验方法
|
||||
private boolean isValidResponse(UnionOpenOrderRowQueryResponse response) {
|
||||
return response != null && response.getQueryResult() != null && response.getQueryResult().getCode() == 200 && response.getQueryResult().getData() != null;
|
||||
}
|
||||
|
||||
// 订单处理方法
|
||||
private void processOrderResponse(UnionOpenOrderRowQueryResponse response, SuperAdmin admin) {
|
||||
Arrays.stream(response.getQueryResult().getData()).parallel().map(this::createOrderRow).forEach(orderRowRepository::save);
|
||||
}
|
||||
|
||||
public int fetchOrders(OrderFetchStrategy strategy, String appKey, String secretKey) {
|
||||
|
||||
TimeRange range = strategy.calculateRange(LocalDateTime.now());
|
||||
int count = 0;
|
||||
|
||||
// 复用原有的抓取逻辑
|
||||
LocalDateTime current = range.getStart();
|
||||
while (!current.isAfter(range.getEnd())) {
|
||||
// 调用分页抓取API...
|
||||
Integer pageIndex = 1;
|
||||
boolean hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
try {
|
||||
// 30-60 天 ,非实时,非分钟
|
||||
UnionOpenOrderRowQueryResponse response = fetchOrdersForDateTime(current, strategy.isRealTime(), pageIndex, false, appKey, secretKey);
|
||||
if (response != null && response.getQueryResult() != null) {
|
||||
if (response.getQueryResult().getCode() == 200) {
|
||||
OrderRowResp[] orderRowResps = response.getQueryResult().getData();
|
||||
if (orderRowResps != null) {
|
||||
for (OrderRowResp orderRowResp : orderRowResps) {
|
||||
if (orderRowResp != null) { // Check each orderRowResp is not null
|
||||
OrderRow orderRow = createOrderRow(orderRowResp);
|
||||
// Ensure orderRow is not null after creation
|
||||
orderRowRepository.save(orderRow);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
hasMore = Boolean.TRUE.equals(response.getQueryResult().getHasMore());
|
||||
} else {
|
||||
hasMore = false;
|
||||
}
|
||||
} else {
|
||||
hasMore = false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
hasMore = false; // Optionally break out of the while loop if required
|
||||
}
|
||||
if (hasMore) pageIndex++;
|
||||
}
|
||||
current = current.plusHours(1);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时刷新最近10分钟的订单(Resilience4j限流集成)
|
||||
*/
|
||||
@Scheduled(cron = "0 * * * * ?")
|
||||
public void fetchLatestOrder() {
|
||||
if (isRunning_jd.equals("true")) {
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime startTime = now.minusMinutes(10).withSecond(0).withNano(0);
|
||||
|
||||
super_admins.values().parallelStream().forEach(admin -> {
|
||||
if (Util.isAnyEmpty(admin.getAppKey(), admin.getSecretKey())) return;
|
||||
|
||||
try {
|
||||
UnionOpenOrderRowQueryResponse response = fetchOrdersForDateTime(startTime, true, 1, true, admin.getAppKey(), admin.getSecretKey());
|
||||
|
||||
if (isValidResponse(response)) {
|
||||
processOrderResponse(response, admin);
|
||||
}
|
||||
} catch (JDUtil.RateLimitExceededException e) {
|
||||
logger.warn("[限流] {} 请求频率受限", admin.getName());
|
||||
} catch (Exception e) {
|
||||
logger.error("{} 订单抓取异常: {}", admin.getName(), e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描订单发送到微信
|
||||
* 每分钟的30秒执行一次
|
||||
*/
|
||||
@Scheduled(cron = "3 * * * * ?")
|
||||
public void sendOrderToWx() {
|
||||
if (isRunning_wx.equals("true")) {
|
||||
//long start = System.currentTimeMillis();
|
||||
int[] validCodes = {-1};
|
||||
// 只要三个月的,更多的也刷新不出来的
|
||||
Date threeMonthsAgo = Date.from(LocalDateTime.now().minusMonths(3).atZone(ZoneId.systemDefault()).toInstant());
|
||||
List<OrderRow> orderRows = orderRowRepository.findByValidCodeNotInAndOrderTimeAfter(validCodes, threeMonthsAgo);
|
||||
|
||||
for (OrderRow orderRow : orderRows) {
|
||||
|
||||
orderUtil.orderToWx(orderRow, true, false);
|
||||
|
||||
}
|
||||
|
||||
//logger.info("扫描订单发送到微信耗时:{} ms, 订单数:{} ", System.currentTimeMillis() - start, orderRows.size());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 一天拉取三次 30天到60天前的订单
|
||||
*/
|
||||
@Scheduled(cron = "0 0 */4 * * ?")
|
||||
public void fetchHistoricalOrders3090() {
|
||||
if (isRunning_jd.equals("true")) {
|
||||
try {
|
||||
OrderFetchStrategy strategy = new Days3090Strategy();
|
||||
for (SuperAdmin admin : super_admins.values()) {
|
||||
try {
|
||||
if (Util.isAnyEmpty(admin.getAppKey(), admin.getSecretKey())) {
|
||||
continue;
|
||||
}
|
||||
int count = fetchOrders(strategy, admin.getAppKey(), admin.getSecretKey());
|
||||
logger.info("账号{} 3090订单拉取完成,新增{}条", admin.getName(), count);
|
||||
} catch (Exception e) {
|
||||
logger.error("账号 {} 拉取异常: {}", admin.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.error("策略执行异常", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 一天拉取6次 14天到30天前的订单
|
||||
*/
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
public void fetchHistoricalOrders1430() {
|
||||
if (isRunning_jd.equals("true")) {
|
||||
|
||||
try {
|
||||
OrderFetchStrategy strategy = new Days1430Strategy(); // 需补充Days1430Strategy实现
|
||||
for (SuperAdmin admin : super_admins.values()) {
|
||||
try {
|
||||
if (Util.isAnyEmpty(admin.getAppKey(), admin.getSecretKey())) {
|
||||
continue;
|
||||
}
|
||||
int count = fetchOrders(strategy, admin.getAppKey(), admin.getSecretKey());
|
||||
logger.info("账号{} 1430订单拉取完成,新增{}条", admin.getName(), count);
|
||||
} catch (Exception e) {
|
||||
logger.error("账号 {} 拉取异常: {}", admin.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.error("1430策略执行异常", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每10分钟拉取07-14天的订单
|
||||
*/
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
public void fetchHistoricalOrders0714() {
|
||||
if (isRunning_jd.equals("true")) {
|
||||
|
||||
try {
|
||||
OrderFetchStrategy strategy = new Days0714Strategy();
|
||||
super_admins.values().parallelStream().forEach(admin -> {
|
||||
if (Util.isAnyEmpty(admin.getAppKey(), admin.getSecretKey())) return;
|
||||
try {
|
||||
int count = fetchOrders(strategy, admin.getAppKey(), admin.getSecretKey());
|
||||
logger.info("账号{} 0714订单拉取完成,新增{}条", admin.getName(), count);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("账号 {} 0714拉取异常: {}", admin.getName(), e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
logger.error("0714策略执行异常", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Scheduled(cron = "30 */10 * * * ?")
|
||||
public void fetchHistoricalOrders0007() {
|
||||
if (isRunning_jd.equals("true")) {
|
||||
|
||||
try {
|
||||
OrderFetchStrategy strategy = new Days0007Strategy();
|
||||
super_admins.values().parallelStream().forEach(admin -> {
|
||||
if (Util.isAnyEmpty(admin.getAppKey(), admin.getSecretKey())) return;
|
||||
|
||||
try {
|
||||
int count = fetchOrders(strategy, admin.getAppKey(), admin.getSecretKey());
|
||||
|
||||
logger.info("账号{} 0007订单拉取完成,新增{}条", admin.getName(), count);
|
||||
} catch (JDUtil.RateLimitExceededException e) {
|
||||
logger.warn("[限流] {} 0007请求受限", admin.getName());
|
||||
} catch (Exception e) {
|
||||
logger.error("账号{}0007拉取异常: {}", admin.getName(), e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
logger.error("0007策略执行异常", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*
|
||||
* @param start 开始时间
|
||||
* @param end 结束时间
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public UnionOpenOrderRowQueryResponse getUnionOpenOrderRowQueryResponse(LocalDateTime start, LocalDateTime end, Integer pageIndex, String appKey, String secretKey) throws Exception {
|
||||
String startTime = start.format(DATE_TIME_FORMATTER);
|
||||
String endTime = end.format(DATE_TIME_FORMATTER);
|
||||
// 模拟 API 调用
|
||||
//System.out.println("调用API - 从 " + startTime
|
||||
// + " 到 " + endTime);
|
||||
// 实际的 API 调用逻辑应在这里进行
|
||||
JdClient client = new DefaultJdClient(SERVER_URL, ACCESS_TOKEN, appKey, secretKey);
|
||||
UnionOpenOrderRowQueryRequest request = new UnionOpenOrderRowQueryRequest();
|
||||
OrderRowReq orderReq = new OrderRowReq();
|
||||
orderReq.setPageIndex(pageIndex);
|
||||
orderReq.setPageSize(200);
|
||||
orderReq.setStartTime(startTime);
|
||||
orderReq.setEndTime(endTime);
|
||||
orderReq.setType(1);
|
||||
|
||||
|
||||
request.setOrderReq(orderReq);
|
||||
request.setVersion("1.0");
|
||||
request.setSignmethod("md5");
|
||||
// 时间戳,格式为yyyy-MM-dd HH:mm:ss,时区为GMT+8。API服务端允许客户端请求最大时间误差为10分钟
|
||||
Date date = new Date();
|
||||
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
request.setTimestamp(simpleDateFormat.format(date));
|
||||
|
||||
|
||||
return client.execute(request);
|
||||
}
|
||||
|
||||
//@Scheduled(cron = "0 0 8-20 * * ?") // 每天从 8:00 到 20:00,每小时执行一次
|
||||
public void fetchPL() {
|
||||
logger.info("开始执行fetchPL任务");
|
||||
// 设置每天最多执行 3 次
|
||||
Set<String> executedHours = getExecutedHoursFromRedis(); // 从 Redis 获取已执行的小时数
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String currentHour = String.valueOf(now.getHour());
|
||||
|
||||
// 如果今天已经执行了3次,则跳过
|
||||
if (executedHours.size() >= 2) {
|
||||
logger.info("今天已经执行了2次,跳过本次任务");
|
||||
return;
|
||||
}
|
||||
|
||||
// 随机决定是否执行本次任务(例如 50% 概率)
|
||||
if (new Random().nextBoolean()) {
|
||||
logger.info("执行fetchPL任务");
|
||||
// 执行任务逻辑
|
||||
executeFetchPL();
|
||||
// 记录该小时已执行
|
||||
executedHours.add(currentHour);
|
||||
saveExecutedHoursToRedis(executedHours); // 存入 Redis
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void executeFetchPL() {
|
||||
HashMap<String, String> productTypeMap = jdUtil.getProductTypeMap();
|
||||
int usedCommentCount;
|
||||
int canUseComentCount;
|
||||
int addCommentCount;
|
||||
for (Map.Entry<String, String> entry : productTypeMap.entrySet()) {
|
||||
|
||||
// 随机睡眠1-5分钟
|
||||
int sleepTime = new Random().nextInt(3000) + 60;
|
||||
try {
|
||||
Thread.sleep(sleepTime * 1000);
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("线程中断", e);
|
||||
}
|
||||
|
||||
String product_id = entry.getKey();
|
||||
List<Comment> availableComments = commentRepository.findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(product_id, 1);
|
||||
List<Comment> usedComments = commentRepository.findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(product_id, 0);
|
||||
|
||||
canUseComentCount = availableComments.size();
|
||||
usedCommentCount = usedComments.size();
|
||||
if (canUseComentCount > 5) {
|
||||
logger.info("商品{} 评论可用数量大于5:{}", product_id, canUseComentCount);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
String fetchUrl = "http://192.168.8.6:5000/fetch_comments?product_id=" + product_id;
|
||||
// 用hutool发起post请求
|
||||
HttpResponse response = HttpRequest.post(fetchUrl).timeout(60000).execute();
|
||||
|
||||
logger.info("fetchUrl: {}", fetchUrl);
|
||||
// code = 200 表示成功,-200 表示失败
|
||||
if (response.getStatus() == 200) {
|
||||
// ✅ 关键修改:重新从数据库中查询,而不是使用内存中的 fetchedComments
|
||||
availableComments = commentRepository.findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(product_id, 1);
|
||||
if (!availableComments.isEmpty()) {
|
||||
addCommentCount = availableComments.size() - canUseComentCount;
|
||||
logger.info("自动刷新并且获取评论成功");
|
||||
logger.info("型号{} 总评论数量 {} 可用数量 {} 新增评论数量:{}", entry.getValue(), availableComments.size() + usedCommentCount, canUseComentCount, addCommentCount);
|
||||
}
|
||||
} else if (response.getStatus() == -200) {
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("调用外部接口获取评论失败", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getExecutedHoursFromRedis() {
|
||||
String key = "fetchPL:executedHours";
|
||||
HashOperations<String, String, String> hashOps = redisTemplate.opsForHash();
|
||||
return hashOps.entries(key).keySet();
|
||||
}
|
||||
|
||||
private void saveExecutedHoursToRedis(Set<String> hours) {
|
||||
String key = "fetchPL:executedHours";
|
||||
HashOperations<String, String, String> hashOps = redisTemplate.opsForHash();
|
||||
hashOps.putAll(key, hours.stream().collect(Collectors.toMap(h -> h, h -> "1")));
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
public void checkGiftCouponsExpiry() {
|
||||
Set<String> keys = redisTemplate.keys("gift_coupon:*");
|
||||
if (keys.isEmpty()) return;
|
||||
|
||||
for (String key : keys) {
|
||||
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
|
||||
if (entries.isEmpty()) continue;
|
||||
|
||||
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
|
||||
String giftJson = (String) entry.getValue();
|
||||
JSONObject gift = JSON.parseObject(giftJson);
|
||||
String giftKey = gift.getString("giftKey");
|
||||
String skuName = gift.getString("skuName");
|
||||
String owner = gift.getString("owner");
|
||||
LocalDateTime expireTime = LocalDateTime.parse(gift.getString("expireTime"), DateTimeFormatter.ISO_DATE_TIME);
|
||||
|
||||
boolean isAboutToExpire = false;
|
||||
|
||||
if ("g".equals(owner)) {
|
||||
// 自营:当天过期
|
||||
isAboutToExpire = !expireTime.isAfter(LocalDateTime.now().with(LocalTime.MAX));
|
||||
} else if ("p".equals(owner)) {
|
||||
// POP:7天后过期,提前一天提醒
|
||||
isAboutToExpire = ChronoUnit.HOURS.between(LocalDateTime.now(), expireTime) <= 24;
|
||||
}
|
||||
|
||||
if (isAboutToExpire) {
|
||||
String message = String.format("[礼金提醒]\n商品:%s\n礼金Key:%s\n类型:%s\n将在 %s 过期", skuName, giftKey, "g".equals(owner) ? "自营" : "POP", expireTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
|
||||
wxUtil.sendTextMessage(WXUtil.default_super_admin_wxid, message, 1, "bot", false);
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 清理三个月前的Redis hash数据
|
||||
* 修复了时间解析异常的问题
|
||||
*/
|
||||
@Scheduled(cron = "0 45 11 * * ?") // 每月1日的凌晨3点执行
|
||||
public void cleanOldRedisHashData() {
|
||||
try {
|
||||
// 获取三个月前的时间
|
||||
LocalDateTime threeMonthsAgo = LocalDateTime.now().minusMonths(3);
|
||||
|
||||
// 获取所有以JD_REFRESH_TAG开头的键
|
||||
Set<String> keys = redisTemplate.keys(JD_REFRESH_TAG + "*");
|
||||
|
||||
if (keys != null && !keys.isEmpty()) {
|
||||
for (String key : keys) {
|
||||
try {
|
||||
// 提取时间部分,处理两种格式:
|
||||
// 1. jd:refresh:tag:hash值:2025-02-02 16:00:00
|
||||
// 2. jd:refresh:tag:2024-11-30 09:26:00
|
||||
String timePart;
|
||||
|
||||
// 使用正则表达式统一提取时间部分(避免lastIndexOf在时间字符串中找到错误的冒号)
|
||||
String timePattern = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}";
|
||||
Pattern pattern = Pattern.compile(timePattern);
|
||||
Matcher matcher = pattern.matcher(key);
|
||||
if (matcher.find()) {
|
||||
timePart = matcher.group();
|
||||
} else {
|
||||
logger.warn("无法识别Redis键格式:{}", key);
|
||||
continue;
|
||||
}
|
||||
|
||||
LocalDateTime time;
|
||||
try {
|
||||
// 解析为完整的时间格式 yyyy-MM-dd HH:mm:ss
|
||||
time = LocalDateTime.parse(timePart, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
} catch (DateTimeParseException e) {
|
||||
logger.warn("无法解析Redis键时间:{},时间部分:{}", key, timePart);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否在三个月前
|
||||
if (time.isBefore(threeMonthsAgo)) {
|
||||
redisTemplate.delete(key);
|
||||
logger.info("已删除过期的Redis键:{}", key);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("解析Redis键时间失败:{}", key, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("清理Redis hash数据时发生错误", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理tag:hash:时间 格式的Redis键(按小时),删除93天前的数据
|
||||
* 可以手动调用或定时执行
|
||||
*/
|
||||
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
|
||||
public void cleanOldTagRedisData() {
|
||||
try {
|
||||
// 获取93天前的时间
|
||||
LocalDateTime ninetyThreeDaysAgo = LocalDateTime.now().minusDays(93);
|
||||
int deletedCount = 0;
|
||||
|
||||
logger.info("开始清理93天前的tag键数据,截止时间:{}", ninetyThreeDaysAgo.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
|
||||
// 获取所有以"tag:"开头的键
|
||||
Set<String> tagKeys = redisTemplate.keys("tag:*");
|
||||
|
||||
if (tagKeys != null && !tagKeys.isEmpty()) {
|
||||
logger.info("找到 {} 个tag相关的键", tagKeys.size());
|
||||
|
||||
for (String key : tagKeys) {
|
||||
try {
|
||||
// 处理格式:tag:hash值:YYYY-MM-DD HH
|
||||
// 例如:tag:01381d95e4936f1f3fe643bba2171894:2025-01-12 00
|
||||
if (key.matches("tag:[a-f0-9]{32}:[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}")) {
|
||||
// 提取时间部分(最后一个冒号之后)
|
||||
String timePart = key.substring(key.lastIndexOf(":") + 1);
|
||||
|
||||
LocalDateTime time;
|
||||
try {
|
||||
// 解析为小时级别的时间格式 yyyy-MM-dd HH
|
||||
time = LocalDateTime.parse(timePart + ":00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
} catch (DateTimeParseException e) {
|
||||
logger.warn("无法解析Redis键时间:{},时间部分:{}", key, timePart);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否在93天前
|
||||
if (time.isBefore(ninetyThreeDaysAgo)) {
|
||||
redisTemplate.delete(key);
|
||||
deletedCount++;
|
||||
if (deletedCount % 100 == 0) {
|
||||
logger.info("已删除 {} 个过期的tag键", deletedCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("解析Redis tag键时间失败:{}", key, e);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("tag键清理完成,共删除 {} 个过期键", deletedCount);
|
||||
} else {
|
||||
logger.info("未找到tag相关的键");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("清理tag Redis数据时发生错误", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动执行清理方法(通过接口调用)
|
||||
* 清理所有超过93天的tag键和jd:refresh:tag键
|
||||
*/
|
||||
public void manualCleanOldRedisData() {
|
||||
logger.info("=== 手动触发Redis键清理 ===");
|
||||
cleanOldTagRedisData();
|
||||
cleanOldRedisHashData();
|
||||
logger.info("=== Redis键清理完成 ===");
|
||||
}
|
||||
|
||||
}
|
||||
3072
src/main/java/cn/van/business/util/JDUtil.java
Normal file
3072
src/main/java/cn/van/business/util/JDUtil.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,762 +0,0 @@
|
||||
package cn.van.business.util;
|
||||
|
||||
|
||||
import cn.van.business.enums.ValidCodeConverter;
|
||||
import cn.van.business.model.jd.GoodsInfoVO;
|
||||
import cn.van.business.model.jd.OrderRow;
|
||||
import cn.van.business.repository.OrderRowRepository;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.util.DateUtils;
|
||||
import com.jd.open.api.sdk.DefaultJdClient;
|
||||
import com.jd.open.api.sdk.JdClient;
|
||||
import com.jd.open.api.sdk.domain.kplunion.OrderService.request.query.OrderRowReq;
|
||||
import com.jd.open.api.sdk.domain.kplunion.OrderService.response.query.GoodsInfo;
|
||||
import com.jd.open.api.sdk.domain.kplunion.OrderService.response.query.OrderRowResp;
|
||||
import com.jd.open.api.sdk.domain.kplunion.promotioncommon.PromotionService.request.get.PromotionCodeReq;
|
||||
import com.jd.open.api.sdk.request.kplunion.UnionOpenOrderRowQueryRequest;
|
||||
import com.jd.open.api.sdk.request.kplunion.UnionOpenPromotionCommonGetRequest;
|
||||
import com.jd.open.api.sdk.response.kplunion.UnionOpenOrderRowQueryResponse;
|
||||
import com.jd.open.api.sdk.response.kplunion.UnionOpenPromotionCommonGetResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.SetOperations;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2024/11/5 17:40
|
||||
* @description:
|
||||
*/
|
||||
@Component
|
||||
public class JDUtils {
|
||||
private static final String SERVER_URL =
|
||||
"https://api.jd.com/routerjson";
|
||||
// van论坛
|
||||
private static final String APP_KEY =
|
||||
"98e21c89ae5610240ec3f5f575f86a59";
|
||||
private static final String SECRET_KEY =
|
||||
"3dcb6b23a1104639ac433fd07adb6dfb";
|
||||
// 导购的
|
||||
//private static final String APP_KEY = "faf410cb9587dc80dc7b31e321d7d322";
|
||||
//private static final String SECRET_KEY =
|
||||
// "a4fb15d7bedd4316b97b4e96e4effc1c";
|
||||
//accessToken
|
||||
private static final String ACCESS_TOKEN = "";
|
||||
//标记唯一订单行:订单+sku维度的唯一标识
|
||||
private static final String ORDER_ROW_KEY = "jd:order:row:";
|
||||
private static final Logger logger = LoggerFactory.getLogger(JDUtils.class);
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
@Autowired
|
||||
private StringRedisTemplate redisTemplate;
|
||||
@Autowired
|
||||
private OrderRowRepository orderRowRepository;
|
||||
@Resource
|
||||
private WXUtil wxUtil;
|
||||
|
||||
/**
|
||||
* 将 响应参数转化为 OrderRow,并返回
|
||||
*/
|
||||
private static OrderRow createOrderRow(OrderRowResp orderRowResp) {
|
||||
OrderRow orderRow = new OrderRow();
|
||||
orderRow.setOrderId(orderRowResp.getOrderId());
|
||||
orderRow.setSkuId(orderRowResp.getSkuId());
|
||||
orderRow.setSkuName(orderRowResp.getSkuName());
|
||||
orderRow.setItemId(orderRowResp.getItemId());
|
||||
orderRow.setSkuNum(orderRowResp.getSkuNum());
|
||||
orderRow.setPrice(orderRowResp.getPrice());
|
||||
orderRow.setActualCosPrice(orderRowResp.getActualCosPrice());
|
||||
orderRow.setActualFee(orderRowResp.getActualFee());
|
||||
orderRow.setEstimateCosPrice(orderRowResp.getEstimateCosPrice());
|
||||
orderRow.setEstimateFee(orderRowResp.getEstimateFee());
|
||||
orderRow.setSubSideRate(orderRowResp.getSubSideRate());
|
||||
orderRow.setSubsidyRate(orderRowResp.getSubsidyRate());
|
||||
orderRow.setCommissionRate(orderRowResp.getCommissionRate());
|
||||
orderRow.setFinalRate(orderRowResp.getFinalRate());
|
||||
|
||||
orderRow.setOrderTime(DateUtils.parseDate(orderRowResp.getOrderTime()));
|
||||
orderRow.setFinishTime(DateUtils.parseDate(orderRowResp.getFinishTime()));
|
||||
orderRow.setOrderTag(orderRowResp.getOrderTag());
|
||||
orderRow.setOrderEmt(orderRowResp.getOrderEmt());
|
||||
orderRow.setUnionId(orderRowResp.getUnionId());
|
||||
orderRow.setUnionRole(orderRowResp.getUnionRole());
|
||||
orderRow.setUnionAlias(orderRowResp.getUnionAlias());
|
||||
orderRow.setUnionTag(orderRowResp.getUnionTag());
|
||||
orderRow.setTraceType(orderRowResp.getTraceType());
|
||||
orderRow.setValidCode(orderRowResp.getValidCode());
|
||||
orderRow.setPayMonth(orderRowResp.getPayMonth());
|
||||
orderRow.setSiteId(orderRowResp.getSiteId());
|
||||
orderRow.setParentId(orderRowResp.getParentId());
|
||||
GoodsInfo goodsInfo = orderRowResp.getGoodsInfo();
|
||||
GoodsInfoVO goodsInfoVO = new GoodsInfoVO();
|
||||
goodsInfoVO.setShopId(String.valueOf(goodsInfo.getShopId()));
|
||||
goodsInfoVO.setShopName(goodsInfo.getShopName());
|
||||
goodsInfoVO.setOwner(goodsInfo.getOwner());
|
||||
goodsInfoVO.setProductId(String.valueOf(goodsInfo.getProductId()));
|
||||
goodsInfoVO.setImageUrl(goodsInfo.getImageUrl());
|
||||
orderRow.setGoodsInfo(goodsInfoVO);
|
||||
orderRow.setCallerItemId(orderRowResp.getCallerItemId());
|
||||
orderRow.setPid(orderRowResp.getPid());
|
||||
orderRow.setCid1(orderRowResp.getCid1());
|
||||
orderRow.setCid2(orderRowResp.getCid2());
|
||||
orderRow.setCid3(orderRowResp.getCid3());
|
||||
orderRow.setChannelId(orderRowResp.getChannelId());
|
||||
orderRow.setProPriceAmount(orderRowResp.getProPriceAmount());
|
||||
orderRow.setSkuFrozenNum(orderRowResp.getSkuFrozenNum());
|
||||
orderRow.setSkuReturnNum(orderRowResp.getSkuReturnNum());
|
||||
orderRow.setSkuTag(orderRowResp.getSkuTag());
|
||||
orderRow.setPositionId(orderRowResp.getPositionId());
|
||||
orderRow.setPopId(orderRowResp.getPopId());
|
||||
orderRow.setRid(orderRowResp.getRid());
|
||||
orderRow.setPlus(orderRowResp.getPlus());
|
||||
orderRow.setCpActId(orderRowResp.getCpActId());
|
||||
orderRow.setGiftCouponKey(orderRowResp.getGiftCouponKey());
|
||||
|
||||
orderRow.setModifyTime(new Date());
|
||||
orderRow.setSign(orderRowResp.getSign());
|
||||
orderRow.setBalanceExt(orderRowResp.getBalanceExt());
|
||||
orderRow.setExpressStatus(orderRowResp.getExpressStatus());
|
||||
orderRow.setExt1(orderRowResp.getExt1());
|
||||
orderRow.setSubUnionId(orderRowResp.getSubUnionId());
|
||||
orderRow.setGiftCouponOcsAmount(orderRowResp.getGiftCouponOcsAmount());
|
||||
orderRow.setTraceType(orderRowResp.getTraceType());
|
||||
orderRow.setExpressStatus(orderRowResp.getExpressStatus());
|
||||
orderRow.setTraceType(orderRowResp.getTraceType());
|
||||
orderRow.setId(orderRowResp.getId());
|
||||
orderRow.setValidCode(orderRowResp.getValidCode());
|
||||
orderRow.setExpressStatus(orderRowResp.getExpressStatus());
|
||||
orderRow.setTraceType(orderRowResp.getTraceType());
|
||||
return orderRow;
|
||||
}
|
||||
|
||||
private static List<OrderRow> filterOrdersByDate(List<OrderRow> orderRows, int daysBack) {
|
||||
LocalDate now = LocalDate.now();
|
||||
|
||||
return orderRows.stream()
|
||||
.filter(order -> {
|
||||
// 将 Date 转换为 LocalDate
|
||||
LocalDate orderDate = order.getOrderTime().toInstant()
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate();
|
||||
|
||||
// 计算是否在给定的天数内
|
||||
return !orderDate.isBefore(now.minusDays(daysBack)) && !orderDate.isAfter(now);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static Stream<OrderRow> getStreamForWeiGui(List<OrderRow> todayOrders) {
|
||||
return todayOrders.stream().filter(
|
||||
orderRow -> orderRow.getValidCode() == 13
|
||||
|| orderRow.getValidCode() == 25
|
||||
|| orderRow.getValidCode() == 26
|
||||
|| orderRow.getValidCode() == 27
|
||||
|| orderRow.getValidCode() == 28
|
||||
|| orderRow.getValidCode() == 29);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取最新的订单 1440分钟
|
||||
*/
|
||||
@Scheduled(cron = "0 * * * * ?") // 每分钟执行一次
|
||||
public void fetchLatestOrder() throws Exception {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime lastMinute = now.minusMinutes(10).withSecond(0).withNano(0);
|
||||
|
||||
UnionOpenOrderRowQueryResponse response = fetchOrdersForDateTime(lastMinute, true, 1); // 真实代表实时订单
|
||||
if (response != null) {
|
||||
|
||||
int code = response.getQueryResult().getCode();
|
||||
if (code == 200) {
|
||||
if (response.getQueryResult().getCode() == 200) {
|
||||
OrderRowResp[] orderRowResps = response.getQueryResult().getData();
|
||||
if (orderRowResps == null) {
|
||||
return;
|
||||
}
|
||||
for (OrderRowResp orderRowResp : orderRowResps) {
|
||||
// 固化到数据库
|
||||
OrderRow orderRow = createOrderRow(orderRowResp);
|
||||
// 订单号不存在就保存,存在就更新订单状态
|
||||
orderRowRepository.save(orderRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描订单发送到微信
|
||||
* 每分钟的30秒执行一次
|
||||
*/
|
||||
@Scheduled(cron = "30 * * * * ?")
|
||||
public void sendOrderToWx() {
|
||||
int[] parm = {-1};
|
||||
List<OrderRow> orderRows = orderRowRepository.findByValidCodeNotInOrderByOrderTimeDesc(parm);
|
||||
if (!orderRows.isEmpty()) {
|
||||
for (OrderRow orderRow : orderRows) {
|
||||
orderToWx(orderRow, true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 每小时拉取过去两个月的订单
|
||||
* 因为有的延迟发货,而接口只能拉取两个月前的数据
|
||||
*/
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
public void fetchHistoricalOrders() throws Exception {
|
||||
System.out.println("开始拉取历史订单");
|
||||
|
||||
// 获取当前时间,并调整为整点开始
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime lastHour = now.truncatedTo(ChronoUnit.HOURS);
|
||||
LocalDateTime startDate = lastHour.minusMonths(2).truncatedTo(ChronoUnit.HOURS);
|
||||
|
||||
// 循环从两个月前到现在最近的整点
|
||||
while (!startDate.isEqual(lastHour)) {
|
||||
Integer pageIndex = 1;
|
||||
Boolean hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
UnionOpenOrderRowQueryResponse response = fetchOrdersForDateTime(startDate, false, pageIndex); // 假的代表历史订单
|
||||
|
||||
if (response != null && response.getQueryResult().getCode() == 200) {
|
||||
OrderRowResp[] orderRowResps = response.getQueryResult().getData();
|
||||
|
||||
if (orderRowResps != null) {
|
||||
for (OrderRowResp orderRowResp : orderRowResps) {
|
||||
// 固化到数据库
|
||||
OrderRow orderRow = createOrderRow(orderRowResp);
|
||||
// 订单号不存在就保存,存在就更新订单状态
|
||||
orderRowRepository.save(orderRow);
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否有更多页面
|
||||
hasMore = response.getQueryResult().getHasMore();
|
||||
if (hasMore) {
|
||||
pageIndex++;
|
||||
}
|
||||
} else {
|
||||
hasMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 每处理完一个小时的数据,递增到下一个小时
|
||||
startDate = startDate.plusHours(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动调用 将订单发送到微信
|
||||
*/
|
||||
private void orderToWx(OrderRow orderRow, Boolean isAutoFlush) {
|
||||
// 查询订单状态
|
||||
Integer newValidCode = orderRow.getValidCode();
|
||||
String oldValidCode = redisTemplate.opsForValue().get(ORDER_ROW_KEY + orderRow.getId());
|
||||
Integer lastValidCode = 0;
|
||||
// 更新 Redis 状态
|
||||
redisTemplate.opsForValue().set(ORDER_ROW_KEY + orderRow.getId(), String.valueOf(orderRow.getValidCode()));
|
||||
|
||||
if (Util.isNotEmpty(oldValidCode)) {
|
||||
lastValidCode = Integer.valueOf(oldValidCode);
|
||||
}
|
||||
// 如果订单状态没变化,就不发送
|
||||
if (isAutoFlush && lastValidCode.equals(newValidCode)) {
|
||||
} else {
|
||||
String content;
|
||||
content = getFormattedOrderInfo(orderRow, Util.isEmpty(oldValidCode) ? -100 : Integer.parseInt(oldValidCode));
|
||||
// 推送
|
||||
wxUtil.sendTextMessage(WXUtil.super_admin_wxid, content, 1, WXUtil.super_admin_wxid);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据库订单转化成微信所需要文本
|
||||
*/
|
||||
public String getFormattedOrderInfo(OrderRow orderRow, Integer oldValidCode) {
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
ValidCodeConverter converter = new ValidCodeConverter();
|
||||
String orderInfo =
|
||||
//+ "订单+sku:" + orderRow.getId() + "\r"
|
||||
"订单号:" + orderRow.getOrderId() + "(" + (orderRow.getPlus() == 1 ? "plus" : "非plus") + ")\r" +
|
||||
|
||||
"最新订单状态:" + (converter.getCodeDescription(orderRow.getValidCode())) + "\r" +
|
||||
|
||||
"商品名称:" + orderRow.getSkuName() + "\r"
|
||||
+ "商品单价:" + orderRow.getPrice() + "\r"
|
||||
+ "商品数量:" + orderRow.getSkuNum() + "\r"
|
||||
+ "商品总价:" + (orderRow.getPrice() * orderRow.getSkuNum()) + "\r"
|
||||
+ "预估计佣金额:" + orderRow.getEstimateCosPrice() + "\n"
|
||||
|
||||
+ "佣金比例:" + orderRow.getCommissionRate() + "%\r\r"
|
||||
+ "推客的预估佣金:" + orderRow.getEstimateFee() + "\r"
|
||||
+ "实际计算佣金的金额:" + orderRow.getActualCosPrice() + "\r"
|
||||
+ "下单时间:" + formatter.format(orderRow.getOrderTime()) + "\r"
|
||||
+ "完成时间:" + (orderRow.getFinishTime() != null ? formatter.format(orderRow.getFinishTime()) : "未完成") + "\r\n";
|
||||
if (oldValidCode != -100) {
|
||||
orderInfo = "订单状态从 :" + (converter.getCodeDescription(oldValidCode)) + " --变成-- " +
|
||||
(converter.getCodeDescription(orderRow.getValidCode())) + "\r" + orderInfo;
|
||||
}
|
||||
|
||||
|
||||
return orderInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定的日期时间拉取订单
|
||||
*/
|
||||
public UnionOpenOrderRowQueryResponse fetchOrdersForDateTime(LocalDateTime startTime, boolean isRealTime, Integer page) throws Exception {
|
||||
|
||||
LocalDateTime endTime = isRealTime ? startTime.plusMinutes(10) : startTime.plusHours(1);
|
||||
String key = startTime.format(DATE_TIME_FORMATTER);
|
||||
String hourRange = isRealTime ? "minute" : "hour";
|
||||
|
||||
SetOperations<String, String> setOps = redisTemplate.opsForSet();
|
||||
|
||||
// 调用 API 以拉取订单
|
||||
UnionOpenOrderRowQueryResponse unionOpenOrderRowQueryResponse = getUnionOpenOrderRowQueryResponse(startTime, endTime, page);
|
||||
|
||||
// 标记已拉取
|
||||
setOps.add(key, hourRange);
|
||||
// 打印方法调用和开始结束时间
|
||||
|
||||
logger.info("拉取订单:开始时间:{}结束时间:{}", startTime.format(DATE_TIME_FORMATTER), endTime.format(DATE_TIME_FORMATTER));
|
||||
return unionOpenOrderRowQueryResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收京粉指令指令
|
||||
*/
|
||||
public void sendOrderToWxByOrderJD(String order) throws Exception {
|
||||
int[] parm = {-1};
|
||||
List<OrderRow> orderRows = orderRowRepository.findByValidCodeNotInOrderByOrderTimeDesc(parm);
|
||||
/**
|
||||
* 菜单:
|
||||
* 今日统计
|
||||
* 昨日统计
|
||||
* 最近七天统计
|
||||
* 最近一个月统计
|
||||
* 今天订单
|
||||
* 昨天订单
|
||||
* */
|
||||
StringBuilder content = new StringBuilder();
|
||||
switch (order) {
|
||||
case "菜单":
|
||||
content.append("菜单:京+命令 \n 如: 京今日统计\r");
|
||||
content.append("今日统计\r");
|
||||
content.append("昨天统计\r");
|
||||
content.append("七日统计\r");
|
||||
content.append("一个月统计\r");
|
||||
content.append("两个月统计\r");
|
||||
content.append("三个月统计\r");
|
||||
content.append("这个月统计\r");
|
||||
content.append("上个月统计\r");
|
||||
content.append("今日订单\r");
|
||||
content.append("昨日订单\r");
|
||||
content.append("刷新三天\r");
|
||||
content.append("刷新两个月\r\n");
|
||||
|
||||
content.append(":::高级菜单:::\r");
|
||||
content.append("菜单:京+高级+命令 \n 如: 京高级违规30\r");
|
||||
content.append("京高级违规+整数\r");
|
||||
content.append("京高级SKU+sku\\r\"");
|
||||
break;
|
||||
case "今日统计": {
|
||||
List<OrderRow> todayOrders = filterOrdersByDate(orderRows, 0);
|
||||
// 订单总数,已付款,已取消,佣金总计
|
||||
content.append("今日统计:\n");
|
||||
content.append("订单总数:").append(todayOrders.size()).append("\r");
|
||||
content.append("已付款:").append(todayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).count()).append("\r");
|
||||
content.append("已取消:").append(todayOrders.stream().filter(orderRow -> orderRow.getValidCode() != 16 && orderRow.getValidCode() != 17).count()).append("\r");
|
||||
content.append("已完成:").append(todayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).count()).append("\r");
|
||||
content.append("违规:").append(getStreamForWeiGui(todayOrders).count()).append("\r");
|
||||
content.append("已付款佣金:").append(todayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum()).append("\r");
|
||||
content.append("已完成佣金:").append(todayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum());
|
||||
content.append("\r" + "违规佣金:").append(getStreamForWeiGui(todayOrders).mapToDouble(orderRow -> orderRow.getEstimateCosPrice() * orderRow.getCommissionRate() * 0.01).sum());
|
||||
|
||||
break;
|
||||
}
|
||||
case "昨日统计": {
|
||||
List<OrderRow> yesterdayOrders = filterOrdersByDate(orderRows, 1);
|
||||
List<OrderRow> todayOrders = filterOrdersByDate(orderRows, 0);
|
||||
yesterdayOrders.removeAll(todayOrders);
|
||||
content.append("昨日统计:\n");
|
||||
content.append("订单总数:").append(yesterdayOrders.size()).append("\r");
|
||||
content.append("已付款:").append(yesterdayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).count()).append("\r");
|
||||
content.append("已取消:").append(yesterdayOrders.stream().filter(orderRow -> orderRow.getValidCode() != 16 && orderRow.getValidCode() != 17).count()).append("\r");
|
||||
content.append("已完成:").append(yesterdayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).count()).append("\r");
|
||||
content.append("违规:").append(getStreamForWeiGui(yesterdayOrders).count()).append("\r");
|
||||
content.append("已付款佣金:").append(yesterdayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum()).append("\r");
|
||||
content.append("已完成佣金:").append(yesterdayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum());
|
||||
content.append("\r" + "违规佣金:").append(getStreamForWeiGui(yesterdayOrders).mapToDouble(orderRow -> orderRow.getEstimateCosPrice() * orderRow.getCommissionRate() * 0.01).sum());
|
||||
break;
|
||||
}
|
||||
case "三日统计": {
|
||||
List<OrderRow> last3DaysOrders = filterOrdersByDate(orderRows, 3);
|
||||
content.append("三日统计:\n");
|
||||
content.append("订单总数:").append(last3DaysOrders.size()).append("\r");
|
||||
content.append("已付款:").append(last3DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).count()).append("\r");
|
||||
content.append("已取消:").append(last3DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() != 16 && orderRow.getValidCode() != 17).count()).append("\r");
|
||||
content.append("已完成:").append(last3DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).count()).append("\r");
|
||||
content.append("违规:").append(getStreamForWeiGui(last3DaysOrders).count()).append("\r");
|
||||
content.append("已付款佣金:").append(last3DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum()).append("\r");
|
||||
content.append("已完成佣金:").append(last3DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum());
|
||||
content.append("\r" + "违规佣金:").append(getStreamForWeiGui(last3DaysOrders).mapToDouble(orderRow -> orderRow.getEstimateCosPrice() * orderRow.getCommissionRate() * 0.01).sum());
|
||||
break;
|
||||
}
|
||||
case "七日统计": {
|
||||
List<OrderRow> last7DaysOrders = filterOrdersByDate(orderRows, 7);
|
||||
content.append("七日统计:\n");
|
||||
content.append("订单总数:").append(last7DaysOrders.size()).append("\r");
|
||||
content.append("已付款:").append(last7DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).count()).append("\r");
|
||||
content.append("已取消:").append(last7DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() != 16 && orderRow.getValidCode() != 17).count()).append("\r");
|
||||
content.append("已完成:").append(last7DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).count()).append("\r");
|
||||
content.append("违规:").append(getStreamForWeiGui(last7DaysOrders).count()).append("\r");
|
||||
content.append("已付款佣金:").append(last7DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum()).append("\r");
|
||||
content.append("已完成佣金:").append(last7DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum());
|
||||
content.append("\r" + "违规佣金:").append(getStreamForWeiGui(last7DaysOrders).mapToDouble(orderRow -> orderRow.getEstimateCosPrice() * orderRow.getCommissionRate() * 0.01).sum());
|
||||
break;
|
||||
}
|
||||
case "一个月统计": {
|
||||
List<OrderRow> last30DaysOrders = filterOrdersByDate(orderRows, 30);
|
||||
content.append("一个月统计:\n");
|
||||
content.append("订单总数:").append(last30DaysOrders.size()).append("\r");
|
||||
content.append("已付款:").append(last30DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).count()).append("\r");
|
||||
content.append("已取消:").append(last30DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() != 16 && orderRow.getValidCode() != 17).count()).append("\r");
|
||||
content.append("已完成:").append(last30DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).count()).append("\r");
|
||||
content.append("违规:").append(getStreamForWeiGui(last30DaysOrders).count()).append("\r");
|
||||
content.append("已付款佣金:").append(last30DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum()).append("\r");
|
||||
content.append("已完成佣金:").append(last30DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum());
|
||||
content.append("\r" + "违规佣金:").append(getStreamForWeiGui(last30DaysOrders).mapToDouble(orderRow -> orderRow.getEstimateCosPrice() * orderRow.getCommissionRate() * 0.01).sum());
|
||||
break;
|
||||
}
|
||||
case "两个月统计": {
|
||||
List<OrderRow> last60DaysOrders = filterOrdersByDate(orderRows, 60);
|
||||
content.append("两个月统计:\n");
|
||||
content.append("订单总数:").append(last60DaysOrders.size()).append("\r");
|
||||
content.append("已付款:").append(last60DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).count()).append("\r");
|
||||
content.append("已取消:").append(last60DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() != 16 && orderRow.getValidCode() != 17).count()).append("\r");
|
||||
content.append("已完成:").append(last60DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).count()).append("\r");
|
||||
content.append("违规:").append(getStreamForWeiGui(last60DaysOrders).count()).append("\r");
|
||||
content.append("已付款佣金:").append(last60DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum()).append("\r");
|
||||
content.append("已完成佣金:").append(last60DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum());
|
||||
content.append("\r" + "违规佣金:").append(getStreamForWeiGui(last60DaysOrders).mapToDouble(orderRow -> orderRow.getEstimateCosPrice() * orderRow.getCommissionRate() * 0.01).sum());
|
||||
break;
|
||||
}
|
||||
case "三个月统计": {
|
||||
List<OrderRow> last90DaysOrders = filterOrdersByDate(orderRows, 90);
|
||||
content.append("订单总数:").append(last90DaysOrders.size()).append("\r");
|
||||
content.append("已付款:").append(last90DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).count()).append("\r");
|
||||
content.append("已取消:").append(last90DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() != 16 && orderRow.getValidCode() != 17).count()).append("\r");
|
||||
content.append("已完成:").append(last90DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).count()).append("\r");
|
||||
content.append("违规:").append(getStreamForWeiGui(last90DaysOrders).count()).append("\r");
|
||||
content.append("已付款佣金:").append(last90DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum()).append("\r");
|
||||
content.append("已完成佣金:").append(last90DaysOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum());
|
||||
content.append("\r" + "违规佣金:").append(getStreamForWeiGui(last90DaysOrders).mapToDouble(orderRow -> orderRow.getEstimateCosPrice() * orderRow.getCommissionRate() * 0.01).sum());
|
||||
break;
|
||||
}
|
||||
case "这个月统计": {
|
||||
// 计算出距离1号有几天
|
||||
int days = LocalDate.now().getDayOfMonth();
|
||||
List<OrderRow> thisMonthOrders = filterOrdersByDate(orderRows, days);
|
||||
|
||||
content.append("本月统计:\n");
|
||||
content.append("订单总数:").append(thisMonthOrders.size()).append("\r");
|
||||
content.append("已付款:").append(thisMonthOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).count()).append("\r");
|
||||
content.append("已取消:").append(thisMonthOrders.stream().filter(orderRow -> orderRow.getValidCode() != 16 && orderRow.getValidCode() != 17).count()).append("\r");
|
||||
content.append("已完成:").append(thisMonthOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).count()).append("\r");
|
||||
content.append("违规:").append(getStreamForWeiGui(thisMonthOrders).count()).append("\r");
|
||||
content.append("已付款佣金:").append(thisMonthOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum()).append("\r");
|
||||
content.append("已完成佣金:").append(thisMonthOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum());
|
||||
content.append("\r" + "违规佣金:").append(getStreamForWeiGui(thisMonthOrders).mapToDouble(orderRow -> orderRow.getEstimateCosPrice() * orderRow.getCommissionRate() * 0.01).sum());
|
||||
break;
|
||||
}
|
||||
case "上个月统计": {
|
||||
LocalDate lastMonth = LocalDate.now().minusMonths(1);
|
||||
int days = LocalDate.now().getDayOfMonth();
|
||||
|
||||
List<OrderRow> lastMonthOrders = filterOrdersByDate(orderRows, lastMonth.lengthOfMonth() + days);
|
||||
List<OrderRow> thisMonthOrders = filterOrdersByDate(orderRows, days);
|
||||
lastMonthOrders = lastMonthOrders.stream().filter(orderRow -> !thisMonthOrders.contains(orderRow)).collect(Collectors.toList());
|
||||
|
||||
content.append("上个月统计:\n");
|
||||
content.append("订单总数:").append(lastMonthOrders.size()).append("\r");
|
||||
content.append("已付款:").append(lastMonthOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).count()).append("\r");
|
||||
content.append("已取消:").append(lastMonthOrders.stream().filter(orderRow -> orderRow.getValidCode() != 16 && orderRow.getValidCode() != 17).count()).append("\r");
|
||||
content.append("已完成:").append(lastMonthOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).count()).append("\r");
|
||||
content.append("违规:").append(getStreamForWeiGui(lastMonthOrders).count()).append("\r");
|
||||
content.append("已付款佣金:").append(lastMonthOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum()).append("\r");
|
||||
content.append("已完成佣金:").append(lastMonthOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum());
|
||||
content.append("\r" + "违规佣金:").append(getStreamForWeiGui(lastMonthOrders).mapToDouble(orderRow -> orderRow.getEstimateCosPrice() * orderRow.getCommissionRate() * 0.01).sum());
|
||||
break;
|
||||
|
||||
}
|
||||
case "今日订单": {
|
||||
List<OrderRow> todayOrders = filterOrdersByDate(orderRows, 0);
|
||||
// 订单总数,已付款,已取消,佣金总计
|
||||
content.append("今日统计:\n");
|
||||
content.append("订单总数:").append(todayOrders.size()).append("\r");
|
||||
content.append("已付款:").append(todayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).count()).append("\r");
|
||||
content.append("已取消:").append(todayOrders.stream().filter(orderRow -> orderRow.getValidCode() != 16 && orderRow.getValidCode() != 17).count()).append("\r");
|
||||
content.append("已完成:").append(todayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).count()).append("\r");
|
||||
content.append("违规:").append(getStreamForWeiGui(todayOrders).count()).append("\r");
|
||||
content.append("已付款佣金:").append(todayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum()).append("\r");
|
||||
content.append("已完成佣金:").append(todayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum());
|
||||
content.append("\r" + "违规佣金:").append(getStreamForWeiGui(todayOrders).mapToDouble(orderRow -> orderRow.getEstimateCosPrice() * orderRow.getCommissionRate() * 0.01).sum());
|
||||
|
||||
for (OrderRow orderRow : todayOrders) {
|
||||
orderToWx(orderRow, false);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "昨日订单": {
|
||||
List<OrderRow> yesterdayOrders = filterOrdersByDate(orderRows, 1);
|
||||
List<OrderRow> todayOrders = filterOrdersByDate(orderRows, 0);
|
||||
yesterdayOrders.removeAll(todayOrders);
|
||||
content.append("昨日统计:\n");
|
||||
content.append("订单总数:").append(yesterdayOrders.size()).append("\r");
|
||||
content.append("已付款:").append(yesterdayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).count()).append("\r");
|
||||
content.append("已取消:").append(yesterdayOrders.stream().filter(orderRow -> orderRow.getValidCode() != 16 && orderRow.getValidCode() != 17).count()).append("\r");
|
||||
content.append("已完成:").append(yesterdayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).count()).append("\r");
|
||||
content.append("违规:").append(getStreamForWeiGui(yesterdayOrders).count()).append("\r");
|
||||
content.append("已付款佣金:").append(yesterdayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum()).append("\r");
|
||||
content.append("已完成佣金:").append(yesterdayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum());
|
||||
content.append("\r" + "违规佣金:").append(getStreamForWeiGui(yesterdayOrders).mapToDouble(orderRow -> orderRow.getEstimateCosPrice() * orderRow.getCommissionRate() * 0.01).sum());
|
||||
for (OrderRow orderRow : yesterdayOrders) {
|
||||
orderToWx(orderRow, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "刷新三天": {
|
||||
long start = System.currentTimeMillis();
|
||||
int count = 0;
|
||||
LocalDateTime startDate = LocalDateTime.now().minusDays(3).withMinute(0).withSecond(0).withNano(0);
|
||||
LocalDateTime lastHour = LocalDateTime.now().minusHours(1).withMinute(0).withSecond(0).withNano(0);
|
||||
while (!startDate.isEqual(lastHour)) {
|
||||
startDate = startDate.plusHours(1);
|
||||
UnionOpenOrderRowQueryResponse response = fetchOrdersForDateTime(startDate, false,1);
|
||||
if (response != null) {
|
||||
|
||||
int code = response.getQueryResult().getCode();
|
||||
if (code == 200) {
|
||||
if (response.getQueryResult().getCode() == 200) {
|
||||
OrderRowResp[] orderRowResps = response.getQueryResult().getData();
|
||||
if (orderRowResps == null) {
|
||||
continue;
|
||||
}
|
||||
for (OrderRowResp orderRowResp : orderRowResps) {
|
||||
// 固化到数据库
|
||||
OrderRow orderRow = createOrderRow(orderRowResp);
|
||||
// 订单号不存在就保存,存在就更新订单状态
|
||||
orderRowRepository.save(orderRow);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
content.append("刷新三天成功,耗时").append((System.currentTimeMillis() - start) / 1000).append("秒\r").append("刷新订单数:").append(count);
|
||||
break;
|
||||
|
||||
}
|
||||
case "刷新两个月": {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
fetchHistoricalOrders();
|
||||
content.append("刷新两个月,耗时").append((System.currentTimeMillis() - start) / 1000).append("秒\r");
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
default:
|
||||
sendOrderToWxByOrderJDAdvanced(order);
|
||||
}
|
||||
if (content.length() > 0) {
|
||||
wxUtil.sendTextMessage(WXUtil.super_admin_wxid, content.toString(), 1, WXUtil.super_admin_wxid);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收京粉指令指令
|
||||
* 高级菜单
|
||||
*/
|
||||
public void sendOrderToWxByOrderJDAdvanced(String order) {
|
||||
int[] parm = {-1};
|
||||
List<OrderRow> orderRows = orderRowRepository.findByValidCodeNotInOrderByOrderTimeDesc(parm);
|
||||
|
||||
StringBuilder content = new StringBuilder();
|
||||
if (order.startsWith("高级")) {
|
||||
order = order.replace("高级", "");
|
||||
if (order.startsWith("违规")) {
|
||||
String days = order.replace("违规", "");
|
||||
Integer daysInt = 365;
|
||||
if (Util.isNotEmpty(days)) {
|
||||
daysInt = Integer.parseInt(days);
|
||||
}
|
||||
List<OrderRow> filterOrdersByDays = filterOrdersByDate(orderRows, daysInt);
|
||||
|
||||
content.append("违规排行:");
|
||||
content.append(daysInt).append("天").append("\r\n");
|
||||
|
||||
Map<String, Long> skuIdViolationCountMap = filterOrdersByDays.stream().filter(orderRow -> orderRow.getValidCode() == 27
|
||||
|| orderRow.getValidCode() == 28
|
||||
|| orderRow.getValidCode() == 2).filter(orderRow -> orderRow.getSkuName() != null).collect(Collectors.groupingBy(OrderRow::getSkuName, Collectors.counting()));
|
||||
List<Map.Entry<String, Long>> sortedViolationCounts = skuIdViolationCountMap.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).collect(Collectors.toList());
|
||||
Integer num = 0;
|
||||
for (Map.Entry<String, Long> entry : sortedViolationCounts) {
|
||||
num++;
|
||||
String skuName = entry.getKey();
|
||||
Long count = entry.getValue();
|
||||
content.append(num).append(",商品:").append(skuName).append("\r\r").append(" 违规次数:").append(count).append("\r");
|
||||
}
|
||||
|
||||
}
|
||||
// 订单状态查询
|
||||
if (order.startsWith("3") || order.startsWith("2")) {
|
||||
List<OrderRow> orderRowList = orderRowRepository.findByOrderId(Long.parseLong(order));
|
||||
if (orderRowList.size() > 0) {
|
||||
OrderRow orderRow = orderRowList.get(0);
|
||||
content.append(getFormattedOrderInfo(orderRow, orderRow.getValidCode()));
|
||||
} else {
|
||||
content.append("订单不存在");
|
||||
}
|
||||
}
|
||||
if (order.startsWith("SKU")) {
|
||||
order = order.replace("SKU", "");
|
||||
String[] split = order.split("\r\n");
|
||||
content.append("电脑端").append("\r\n");
|
||||
for (String s : split) {
|
||||
content.append("https://item.jd.com/").append(s).append(".html").append("\r\n");
|
||||
}
|
||||
content.append("手机端").append("\r\n");
|
||||
for (String s : split) {
|
||||
content.append("https://item.m.jd.com/product/").append(s).append(".html").append("\r\n");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
sendOrderToWxByOrderJD("菜单");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
if (content.length() > 0) {
|
||||
wxUtil.sendTextMessage(WXUtil.super_admin_wxid, content.toString(), 1, WXUtil.super_admin_wxid);
|
||||
}
|
||||
}
|
||||
//public UnionOpenGoodsBigfieldQueryResponse getUnionOpenGoodsBigfieldQueryResponse(){
|
||||
// JdClient client = new DefaultJdClient(SERVER_URL, ACCESS_TOKEN, APP_KEY, SECRET_KEY);
|
||||
//
|
||||
// UnionOpenGoodsBigfieldQueryRequest request=new UnionOpenGoodsBigfieldQueryRequest();
|
||||
// BigFieldGoodsReq goodsReq=new BigFieldGoodsReq();
|
||||
// goodsReq.setSkuIds();
|
||||
// request.setGoodsReq(goodsReq);
|
||||
// request.setVersion("1.0");
|
||||
// UnionOpenGoodsBigfieldQueryResponse response= null;
|
||||
// try {
|
||||
// response = client.execute(request);
|
||||
// } catch (Exception e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// return response;
|
||||
//}
|
||||
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*
|
||||
* @param start 开始时间
|
||||
* @param end 结束时间
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public UnionOpenOrderRowQueryResponse getUnionOpenOrderRowQueryResponse(LocalDateTime start, LocalDateTime end, Integer pageIndex) throws Exception {
|
||||
String startTime = start.format(DATE_TIME_FORMATTER);
|
||||
String endTime = end.format(DATE_TIME_FORMATTER);
|
||||
// 模拟 API 调用
|
||||
//System.out.println("调用API - 从 " + startTime
|
||||
// + " 到 " + endTime);
|
||||
// 实际的 API 调用逻辑应在这里进行
|
||||
JdClient client = new DefaultJdClient(SERVER_URL, ACCESS_TOKEN, APP_KEY, SECRET_KEY);
|
||||
UnionOpenOrderRowQueryRequest request = new UnionOpenOrderRowQueryRequest();
|
||||
OrderRowReq orderReq = new OrderRowReq();
|
||||
orderReq.setPageIndex(pageIndex);
|
||||
orderReq.setPageSize(200);
|
||||
orderReq.setStartTime(startTime);
|
||||
orderReq.setEndTime(endTime);
|
||||
orderReq.setType(1);
|
||||
|
||||
|
||||
request.setOrderReq(orderReq);
|
||||
request.setVersion("1.0");
|
||||
request.setSignmethod("md5");
|
||||
// 时间戳,格式为yyyy-MM-dd HH:mm:ss,时区为GMT+8。API服务端允许客户端请求最大时间误差为10分钟
|
||||
Date date = new Date();
|
||||
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
request.setTimestamp(simpleDateFormat.format(date));
|
||||
|
||||
|
||||
return client.execute(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转链
|
||||
*/
|
||||
String transfer(String url) throws Exception {
|
||||
JdClient client = new DefaultJdClient(SERVER_URL, ACCESS_TOKEN, APP_KEY, SECRET_KEY);
|
||||
UnionOpenPromotionCommonGetRequest request = new UnionOpenPromotionCommonGetRequest();
|
||||
request.setVersion("1.0");
|
||||
request.setSignmethod("md5");
|
||||
|
||||
PromotionCodeReq promotionCodeReq = new PromotionCodeReq();
|
||||
promotionCodeReq.setMaterialId(url);
|
||||
promotionCodeReq.setSiteId(
|
||||
"4101253066");
|
||||
promotionCodeReq.setSceneId(1);
|
||||
promotionCodeReq.setCommand(1);
|
||||
promotionCodeReq.setProType(5);
|
||||
|
||||
|
||||
request.setPromotionCodeReq(promotionCodeReq);
|
||||
UnionOpenPromotionCommonGetResponse response = client.execute(request);
|
||||
|
||||
String jsonString = JSON.toJSONString(response);
|
||||
System.out.println(jsonString);
|
||||
//
|
||||
//System.out.println(request.getAppJsonParams());
|
||||
//System.out.println(request.getPromotionCodeReq());
|
||||
//
|
||||
//System.out.println("--------");
|
||||
//System.out.println(response.getGetResult().getCode());
|
||||
//System.out.println(response.getGetResult().getMessage());
|
||||
//System.out.println(response.getGetResult().getData().getClickURL());
|
||||
//System.out.println(response.getGetResult().getData().getJCommand());
|
||||
return response.getGetResult().getData().getClickURL();
|
||||
}
|
||||
|
||||
}
|
||||
26
src/main/java/cn/van/business/util/OrderStats.java
Normal file
26
src/main/java/cn/van/business/util/OrderStats.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package cn.van.business.util;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2025/5/30 13:43
|
||||
* @description:
|
||||
*/ // 统计指标DTO
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
class OrderStats {
|
||||
private long totalOrders; // 总订单数
|
||||
private long validOrders; // 有效订单数(不含取消)
|
||||
private long paidOrders; // 已付款订单
|
||||
private double paidCommission; // 已付款佣金
|
||||
private long pendingOrders; // 待付款订单
|
||||
private double pendingCommission; // 待付款佣金
|
||||
private long canceledOrders; // 已取消订单
|
||||
private long completedOrders; // 已完成订单
|
||||
private double completedCommission;// 已完成佣金
|
||||
private long violations; // 违规订单数
|
||||
private double violationCommission;// 违规佣金
|
||||
}
|
||||
75
src/main/java/cn/van/business/util/OrderStatsUtil.java
Normal file
75
src/main/java/cn/van/business/util/OrderStatsUtil.java
Normal file
@@ -0,0 +1,75 @@
|
||||
package cn.van.business.util;
|
||||
|
||||
import cn.van.business.model.jd.OrderRow;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class OrderStatsUtil {
|
||||
|
||||
public static Map<Long, OrderStats> groupByUnionIdAndCalculateStats(List<OrderRow> orderRows) {
|
||||
// 按 unionId 分组并统计
|
||||
return orderRows.stream()
|
||||
.collect(Collectors.groupingBy(OrderRow::getUnionId,
|
||||
Collectors.collectingAndThen(Collectors.toList(), OrderStatsUtil::calculateStats)));
|
||||
}
|
||||
|
||||
// 统计逻辑
|
||||
private static OrderStats calculateStats(List<OrderRow> orders) {
|
||||
long totalOrders = orders.size();
|
||||
long validOrders = (int) orders.stream().filter(o -> o.getValidCode() != -1).count();
|
||||
|
||||
long paidOrders = orders.stream().filter(o -> o.getValidCode() == 16).count();
|
||||
double paidCommission = orders.stream()
|
||||
.filter(o -> o.getValidCode() == 16)
|
||||
.mapToDouble(OrderRow::getEstimateFee)
|
||||
.sum();
|
||||
|
||||
long pendingOrders = orders.stream().filter(o -> o.getValidCode() == 15).count();
|
||||
double pendingCommission = orders.stream()
|
||||
.filter(o -> o.getValidCode() == 15)
|
||||
.mapToDouble(OrderRow::getEstimateFee)
|
||||
.sum();
|
||||
|
||||
long canceledOrders = orders.stream()
|
||||
.filter(o -> o.getValidCode() != 16 && o.getValidCode() != 17)
|
||||
.count();
|
||||
|
||||
long completedOrders = orders.stream().filter(o -> o.getValidCode() == 17).count();
|
||||
double completedCommission = orders.stream()
|
||||
.filter(o -> o.getValidCode() == 17)
|
||||
.mapToDouble(OrderRow::getEstimateFee)
|
||||
.sum();
|
||||
|
||||
long violations = getStreamForWeiGui(orders).count();
|
||||
double violationCommission = getStreamForWeiGui(orders)
|
||||
.mapToDouble(o -> o.getEstimateCosPrice() * o.getCommissionRate() * 0.01)
|
||||
.sum();
|
||||
|
||||
return new OrderStats(
|
||||
totalOrders,
|
||||
validOrders,
|
||||
paidOrders,
|
||||
paidCommission,
|
||||
pendingOrders,
|
||||
pendingCommission,
|
||||
canceledOrders,
|
||||
completedOrders,
|
||||
completedCommission,
|
||||
violations,
|
||||
violationCommission
|
||||
);
|
||||
}
|
||||
|
||||
// 获取违规订单流
|
||||
private static Stream<OrderRow> getStreamForWeiGui(List<OrderRow> orderRows) {
|
||||
return orderRows.stream().filter(orderRow ->
|
||||
orderRow.getValidCode() == 13 ||
|
||||
orderRow.getValidCode() == 25 ||
|
||||
orderRow.getValidCode() == 26 ||
|
||||
orderRow.getValidCode() == 27 ||
|
||||
orderRow.getValidCode() == 28 ||
|
||||
orderRow.getValidCode() == 29);
|
||||
}
|
||||
}
|
||||
391
src/main/java/cn/van/business/util/OrderUtil.java
Normal file
391
src/main/java/cn/van/business/util/OrderUtil.java
Normal file
@@ -0,0 +1,391 @@
|
||||
package cn.van.business.util;
|
||||
|
||||
import cn.van.business.enums.ValidCodeConverter;
|
||||
import cn.van.business.model.jd.OrderRow;
|
||||
import cn.van.business.model.wx.SuperAdmin;
|
||||
import cn.van.business.repository.OrderRowRepository;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static cn.van.business.util.WXUtil.*;
|
||||
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2024/11/29 11:43
|
||||
* @description:
|
||||
*/
|
||||
@Service
|
||||
public class OrderUtil {
|
||||
private static final Logger logger = LoggerFactory.getLogger(OrderUtil.class);
|
||||
|
||||
@Autowired
|
||||
private StringRedisTemplate redisTemplate;
|
||||
@Autowired
|
||||
private OrderRowRepository orderRowRepository;
|
||||
@Autowired
|
||||
private WXUtil wxUtil;
|
||||
|
||||
//标记唯一订单行:订单+sku维度的唯一标识
|
||||
private static final String ORDER_ROW_KEY = "jd:order:row:";
|
||||
private static final String ORDER_ROW_JB_KEY = "jd:order:row:jb:";
|
||||
private static final String SEND_DAILY_STATS_KEY = "sendDailyStats:";
|
||||
|
||||
|
||||
/**
|
||||
* 手动调用 将订单发送到微信
|
||||
*/
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void orderToWx(OrderRow orderRow, Boolean isAutoFlush, Boolean isAutoFlushJB) {
|
||||
try {
|
||||
// 获取订单当前状态
|
||||
Integer newValidCode = orderRow.getValidCode();
|
||||
String oldValidCode = getFromRedis(ORDER_ROW_KEY + orderRow.getId());
|
||||
|
||||
// 检查Redis中是否有旧的状态码,没有的话赋予默认值
|
||||
Integer lastValidCode = oldValidCode != null ? Integer.parseInt(oldValidCode) : -100;
|
||||
|
||||
// 先更新 Redis 状态,防止消息发送失败导致重复触发
|
||||
redisTemplate.opsForValue().set(ORDER_ROW_KEY + orderRow.getId(), String.valueOf(orderRow.getValidCode()));
|
||||
|
||||
// 订单变化:当 isAutoFlush == false 或状态确实有变化时,进行消息发送
|
||||
if (!isAutoFlush || !lastValidCode.equals(newValidCode)) {
|
||||
String content = getFormattedOrderInfo(orderRow);
|
||||
String wxId = getWxidFromJdid(orderRow.getUnionId().toString());
|
||||
// 根据unionId获取接收人列表
|
||||
String unionIdStr = orderRow.getUnionId().toString();
|
||||
String touser = WXUtil.getTouserByUnionId(unionIdStr);
|
||||
logger.info("京粉订单推送 - unionId={}, wxId={}, touser={}", unionIdStr, wxId, touser);
|
||||
|
||||
if (Util.isNotEmpty(wxId)) {
|
||||
wxUtil.sendTextMessage(wxId, content, 1, wxId, true, touser);
|
||||
// 不是已完成,不是违规的才发送
|
||||
if (newValidCode != 17 && newValidCode != 25 && newValidCode != 26 && newValidCode != 27 && newValidCode != 28) {
|
||||
// 发送今日统计信息
|
||||
sendDailyStats(wxId);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理价保逻辑
|
||||
BigDecimal newProPriceAmount = Optional.ofNullable(orderRow.getProPriceAmount()).map(BigDecimal::new).orElse(BigDecimal.ZERO);
|
||||
|
||||
// 获取Redis中的旧价保金额和通知状态
|
||||
String redisKey = ORDER_ROW_JB_KEY + orderRow.getId();
|
||||
String redisValue = getFromRedis(redisKey);
|
||||
|
||||
// 解析Redis值,格式为 "金额:通知状态",例如 "100.00:true"
|
||||
String[] parts = redisValue != null ? redisValue.split(":") : new String[0];
|
||||
BigDecimal oldProPriceAmount = parts.length > 0 ? new BigDecimal(parts[0]) : BigDecimal.ZERO;
|
||||
boolean hasNotified = parts.length > 1 && Boolean.parseBoolean(parts[1]);
|
||||
|
||||
// 判断是否需要发送通知:金额不为零且金额变化了或者未发送过通知
|
||||
boolean shouldNotify = newProPriceAmount.compareTo(BigDecimal.ZERO) > 0 && (newProPriceAmount.compareTo(oldProPriceAmount) != 0 || !hasNotified);
|
||||
|
||||
if (isAutoFlushJB) {
|
||||
shouldNotify = true;
|
||||
}
|
||||
if (shouldNotify) {
|
||||
String wxId = getWxidFromJdid(orderRow.getUnionId().toString());
|
||||
// 根据unionId获取接收人列表
|
||||
String touser = WXUtil.getTouserByUnionId(orderRow.getUnionId().toString());
|
||||
if (Util.isNotEmpty(wxId)) {
|
||||
String content = getFormattedOrderInfoForJB(orderRow);
|
||||
String alertMsg = "[爱心] 价保/赔付 : " + newProPriceAmount + " [爱心] \n" + content;
|
||||
|
||||
try {
|
||||
// 先发送通知
|
||||
wxUtil.sendTextMessage(wxId, alertMsg, 1, wxId, true, touser);
|
||||
|
||||
// 通知成功后更新Redis,格式为 "金额:true"
|
||||
if (!isAutoFlushJB) {
|
||||
String newRedisValue = newProPriceAmount + ":true";
|
||||
redisTemplate.opsForValue().set(redisKey, newRedisValue);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("发送价保通知失败: {}", e.getMessage(), e);
|
||||
// 通知失败,不更新Redis,下次继续尝试
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理订单微信通知失败: {}", e.getMessage(), e);
|
||||
// 可加入重试机制或上报监控
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDailyStats(String wxId) {
|
||||
String key = SEND_DAILY_STATS_KEY + wxId;
|
||||
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SuperAdmin> superAdmins = getSuperAdmins(wxId);
|
||||
if (superAdmins.isEmpty()) return;
|
||||
|
||||
List<Long> unionIds = superAdmins.stream().map(admin -> Long.valueOf(admin.getUnionId())).collect(Collectors.toList());
|
||||
|
||||
List<OrderRow> orderRows = orderRowRepository.findByValidCodeNotInAndUnionIdIn(new int[]{-1}, unionIds);
|
||||
List<OrderRow> todayOrders = filterOrdersByDate(orderRows, 0);
|
||||
|
||||
if (todayOrders.isEmpty()) return;
|
||||
|
||||
// 按照 unionId 分组统计
|
||||
Map<String, List<OrderRow>> grouped = todayOrders.stream().collect(Collectors.groupingBy(o -> o.getUnionId().toString()));
|
||||
|
||||
StringBuilder resultContent = new StringBuilder();
|
||||
for (Map.Entry<String, List<OrderRow>> entry : grouped.entrySet()) {
|
||||
String unionId = entry.getKey();
|
||||
OrderStats stats = calculateStats(entry.getValue());
|
||||
resultContent.append(buildStatsContent("京粉 : " + getRemarkFromJdid(unionId), stats));
|
||||
}
|
||||
|
||||
OrderStats totalStats = calculateStats(todayOrders);
|
||||
String totalMsg = buildStatsContent("今日总统计 : ", totalStats) + " \n详:\n━━━━━━━━━━━━\n" + resultContent;
|
||||
|
||||
wxUtil.sendTextMessage(wxId, totalMsg, 1, wxId, true);
|
||||
//60秒
|
||||
redisTemplate.opsForValue().set(key, "true", 60);
|
||||
}
|
||||
|
||||
private String getFromRedis(String key) {
|
||||
try {
|
||||
return redisTemplate.opsForValue().get(key);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Redis get 失败 key={}", key, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动调用 将订单发送到微信 批量
|
||||
*/
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void orderToWxBatch(List<OrderRow> orderRowList) {
|
||||
if (!orderRowList.isEmpty()) {
|
||||
int i = 1;
|
||||
String wxId = getWxidFromJdid(orderRowList.get(0).getUnionId().toString());
|
||||
// 根据unionId获取接收人列表
|
||||
String touser = WXUtil.getTouserByUnionId(orderRowList.get(0).getUnionId().toString());
|
||||
StringBuilder content = new StringBuilder();
|
||||
content.append("批量订单:\n\r ").append(" 共 ").append(orderRowList.size()).append("单 \r");
|
||||
List<OrderRow> filterList = orderRowList.stream().filter(orderRow -> orderRow.getValidCode() != 2 && orderRow.getValidCode() != 3).toList();
|
||||
content.append("移除 拆单或者取消 的订单, 共 ").append(filterList.size()).append("单: \n\r");
|
||||
for (OrderRow orderRow : filterList) {
|
||||
content.append("\r\n");
|
||||
content.append(i++).append("、");
|
||||
content.append(getFormattedOrderInfoBatch(orderRow));
|
||||
|
||||
}
|
||||
if (Util.isNotEmpty(wxId)) {
|
||||
wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false, touser);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将数据库订单转化成微信所需要文本
|
||||
*/
|
||||
public String getFormattedOrderInfo(OrderRow orderRow) {
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
ValidCodeConverter converter = new ValidCodeConverter();
|
||||
Long unionId = orderRow.getUnionId();
|
||||
String remarkFromJdid = getRemarkFromJdid(String.valueOf(unionId));
|
||||
StringBuilder orderInfo = new StringBuilder().append(" ").append(getEmjoy(orderRow.getValidCode())).append(" ").append(converter.getCodeDescription(orderRow.getValidCode())).append("\r");
|
||||
|
||||
//if (oldValidCode != -100 && !oldValidCode.equals(orderRow.getValidCode())) {
|
||||
// orderInfo.insert(0, "从 :" + getEmjoy(oldValidCode) + " "
|
||||
// + converter.getCodeDescription(oldValidCode) + "\r变成 "
|
||||
// + getEmjoy(orderRow.getValidCode()) + " "
|
||||
// + converter.getCodeDescription(orderRow.getValidCode()) + "\r\n");
|
||||
//}
|
||||
|
||||
orderInfo
|
||||
//+ "订单+sku:" + orderRow.getId() + "\r"
|
||||
.append("京粉:").append(remarkFromJdid).append("\r").append("订单:").append(orderRow.getOrderId()).append(" (").append(orderRow.getPlus() == 1 ? "plus" : "非plus").append(")\r").append("名称:").append(orderRow.getSkuName()).append("\r").append("\r")
|
||||
//+ "商品单价:" + orderRow.getPrice() + "\r"
|
||||
//+ "商品数量:" + orderRow.getSkuNum() + "\r"
|
||||
//+ "商品总价:" + (orderRow.getPrice() * orderRow.getSkuNum()) + "\r"
|
||||
.append("计佣金额:").append(orderRow.getEstimateCosPrice()).append("\r")
|
||||
//+ "金额:" + orderRow.getActualCosPrice() + "\r"
|
||||
.append("比例:").append(orderRow.getCommissionRate()).append("\r").append("[Packet] 佣金:").append(orderRow.getEstimateFee()).append("\r").append("下单:").append(formatter.format(orderRow.getOrderTime())).append("\r").append("完成:").append(orderRow.getFinishTime() != null ? formatter.format(orderRow.getFinishTime()) : "未完成");
|
||||
|
||||
return orderInfo.toString();
|
||||
}
|
||||
|
||||
// 价保
|
||||
public String getFormattedOrderInfoForJB(OrderRow orderRow) {
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
ValidCodeConverter converter = new ValidCodeConverter();
|
||||
Long unionId = orderRow.getUnionId();
|
||||
String remarkFromJdid = getRemarkFromJdid(String.valueOf(unionId));
|
||||
StringBuilder orderInfo = new StringBuilder().append(" ").append(getEmjoy(orderRow.getValidCode())).append(" ").append(converter.getCodeDescription(orderRow.getValidCode())).append("\r");
|
||||
orderInfo
|
||||
//+ "订单+sku:" + orderRow.getId() + "\r"
|
||||
.append("京粉:").append(remarkFromJdid).append("\r").append("订单:").append(orderRow.getOrderId()).append(" (").append(orderRow.getPlus() == 1 ? "plus" : "非plus").append(")\r").append("名称:").append(orderRow.getSkuName()).append("\r").append("\r").append("下单:").append(formatter.format(orderRow.getOrderTime())).append("\r").append("完成:").append(orderRow.getFinishTime() != null ? formatter.format(orderRow.getFinishTime()) : "未完成");
|
||||
|
||||
return orderInfo.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动调用 将订单发送到微信 批量
|
||||
*/
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void orderToWxBatchForJB(List<OrderRow> orderRowList) {
|
||||
if (!orderRowList.isEmpty()) {
|
||||
int i = 1;
|
||||
String wxId = getWxidFromJdid(orderRowList.get(0).getUnionId().toString());
|
||||
// 根据unionId获取接收人列表
|
||||
String touser = WXUtil.getTouserByUnionId(orderRowList.get(0).getUnionId().toString());
|
||||
StringBuilder content = new StringBuilder();
|
||||
content.append("批量订单:\n\r ").append(" 共 ").append(orderRowList.size()).append("单 \r");
|
||||
List<OrderRow> filterList = orderRowList.stream().filter(orderRow -> orderRow.getValidCode() != 2 && orderRow.getValidCode() != 3).toList();
|
||||
content.append("移除 拆单或者取消 的订单, 共 ").append(filterList.size()).append("单: \n\r");
|
||||
for (OrderRow orderRow : filterList) {
|
||||
content.append("\r\n");
|
||||
content.append(i++).append("、");
|
||||
content.append(getFormattedOrderInfoBatchForJB(orderRow));
|
||||
|
||||
}
|
||||
if (Util.isNotEmpty(wxId)) {
|
||||
wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false, touser);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public String getEmjoy(Integer status) {
|
||||
return switch (status) {
|
||||
//[爱心]已付款
|
||||
case 16 -> "[爱心]";
|
||||
//[Wow] 待付款
|
||||
case 15 -> "[Wow]";
|
||||
//[心碎]已取消
|
||||
case 2, 3 -> "[心碎]";
|
||||
//[亲亲] 已完成
|
||||
case 17 -> "[亲亲]";
|
||||
//[Broken] 违规
|
||||
case 27, 28 -> "[Broken]";
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将数据库订单转化成微信所需要文本 简洁版
|
||||
*/
|
||||
public String getFormattedOrderInfoBatch(OrderRow orderRow) {
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
ValidCodeConverter converter = new ValidCodeConverter();
|
||||
Long unionId = orderRow.getUnionId();
|
||||
String remarkFromJdid = getRemarkFromJdid(String.valueOf(unionId));
|
||||
|
||||
|
||||
return "订单:" + orderRow.getOrderId() + " (" + (orderRow.getPlus() == 1 ? "plus" : "非plus") + ")\r" + "走的京粉:" + remarkFromJdid + "\r"
|
||||
|
||||
+ "状态:" + (converter.getCodeDescription(orderRow.getValidCode())) + "\r"
|
||||
|
||||
+ "标题:" + orderRow.getSkuName() + "\r\n" + "\r\n"
|
||||
//+ "商品单价:" + orderRow.getPrice() + "\r"
|
||||
//+ "商品数量:" + orderRow.getSkuNum() + "\r"
|
||||
//+ "商品总价:" + (orderRow.getPrice() * orderRow.getSkuNum()) + "\r"
|
||||
+ "计佣金额:" + orderRow.getEstimateCosPrice() + "\r"
|
||||
|
||||
//+ "金额:" + orderRow.getActualCosPrice() + "\r"
|
||||
//+ "比例:" + orderRow.getCommissionRate() + "\r"
|
||||
+ "佣金:" + orderRow.getEstimateFee() + "\r\n"
|
||||
|
||||
+ "下单:" + formatter.format(orderRow.getOrderTime()) + "\r" + "完成:" + (orderRow.getFinishTime() != null ? formatter.format(orderRow.getFinishTime()) : "未完成") + "\r";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据库订单转化成微信所需要文本 简洁版
|
||||
*/
|
||||
public String getFormattedOrderInfoBatchForJB(OrderRow orderRow) {
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
StringBuilder orderInfo = new StringBuilder().append("\n").append("价保了:").append(orderRow.getProPriceAmount()).append("\n");
|
||||
orderInfo.append("订单:").append(orderRow.getOrderId()).append(" (").append(orderRow.getPlus() == 1 ? "plus" : "非plus").append(")\r").append("名称:").append(orderRow.getSkuName()).append("\r").append("\r").append("下单:").append(formatter.format(orderRow.getOrderTime())).append("\r").append("完成:").append(orderRow.getFinishTime() != null ? formatter.format(orderRow.getFinishTime()) : "未完成");
|
||||
return orderInfo.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* JDUtil拷贝的方法,避免循环注入
|
||||
*/
|
||||
|
||||
private List<OrderRow> filterOrdersByDate(List<OrderRow> orderRows, int daysBack) {
|
||||
LocalDate now = LocalDate.now();
|
||||
|
||||
return orderRows.stream().filter(order -> {
|
||||
// 将 Date 转换为 LocalDate
|
||||
LocalDate orderDate = order.getOrderTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||
|
||||
// 计算是否在给定的天数内
|
||||
return !orderDate.isBefore(now.minusDays(daysBack)) && !orderDate.isAfter(now);
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private OrderStats calculateStats(List<OrderRow> orders) {
|
||||
long paid = orders.stream().filter(o -> o.getValidCode() == 16).count();
|
||||
long pending = orders.stream().filter(o -> o.getValidCode() == 15).count();
|
||||
long canceled = orders.stream().filter(o -> o.getValidCode() != 16 && o.getValidCode() != 17).count();
|
||||
long completed = orders.stream().filter(o -> o.getValidCode() == 17).count();
|
||||
|
||||
return new OrderStats(orders.size(), orders.size() - canceled, paid, orders.stream().filter(o -> o.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum(), pending, orders.stream().filter(o -> o.getValidCode() == 15).mapToDouble(OrderRow::getEstimateFee).sum(), canceled, completed, orders.stream().filter(o -> o.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum(), getStreamForWeiGui(orders).count(), getStreamForWeiGui(orders).mapToDouble(o -> o.getEstimateCosPrice() * o.getCommissionRate() * 0.01).sum());
|
||||
}
|
||||
|
||||
private String buildStatsContent(String title, OrderStats stats) {
|
||||
StringBuilder content = new StringBuilder();
|
||||
content//[爱心][Wow][Packet][Party][Broken][心碎][亲亲][色]
|
||||
.append(title).append(" \n").append("[爱心] 订单总数:").append(stats.getTotalOrders()).append("\n") // [文件]
|
||||
.append("[Party] 有效订单:").append(stats.getValidOrders()).append("\n") // [OK]
|
||||
.append("[心碎]已取消:").append(stats.getCanceledOrders()).append("\n") // [禁止]
|
||||
.append("[爱心]已付款:").append(stats.getPaidOrders()).append("\n") // [钱袋]
|
||||
.append("[Packet] 已付款佣金:").append(String.format("%.2f", stats.getPaidCommission())).append("\n") // [钞票]
|
||||
.append("[Wow] 待付款:").append(stats.getPendingOrders()).append("\n") // [时钟]
|
||||
.append("[Packet] 待付款佣金:").append(String.format("%.2f", stats.getPendingCommission())).append("\n").append("━━━━━━━━━━━━\n");
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
private Stream<OrderRow> getStreamForWeiGui(List<OrderRow> todayOrders) {
|
||||
return todayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 13 || orderRow.getValidCode() == 25 || orderRow.getValidCode() == 26 || orderRow.getValidCode() == 27 || orderRow.getValidCode() == 28 || orderRow.getValidCode() == 29);
|
||||
}
|
||||
|
||||
// 统计指标DTO
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
static class OrderStats {
|
||||
private long totalOrders; // 总订单数
|
||||
private long validOrders; // 有效订单数(不含取消)
|
||||
private long paidOrders; // 已付款订单
|
||||
private double paidCommission; // 已付款佣金
|
||||
private long pendingOrders; // 待付款订单
|
||||
private double pendingCommission; // 待付款佣金
|
||||
private long canceledOrders; // 已取消订单
|
||||
private long completedOrders; // 已完成订单
|
||||
private double completedCommission;// 已完成佣金
|
||||
private long violations; // 违规订单数
|
||||
private double violationCommission;// 违规佣金
|
||||
}
|
||||
}
|
||||
80
src/main/java/cn/van/business/util/OtherUtil.java
Normal file
80
src/main/java/cn/van/business/util/OtherUtil.java
Normal file
@@ -0,0 +1,80 @@
|
||||
package cn.van.business.util;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2025/7/8 21:13
|
||||
* @description:
|
||||
*/
|
||||
@Component
|
||||
public class OtherUtil {
|
||||
final WXUtil wxUtil;
|
||||
|
||||
public OtherUtil(WXUtil wxUtil) {
|
||||
this.wxUtil = wxUtil;
|
||||
}
|
||||
|
||||
public void tmyk(String msg, String fromWxid) {
|
||||
// 正则表达式:匹配pagepath标签中的tid参数值
|
||||
// 解释:
|
||||
// <pagepath><!\[CDATA\[ 匹配pagepath的CDATA开始标签
|
||||
// [^\]]*?tid= 非贪婪匹配任意字符,直到遇到tid=
|
||||
// (\d+) 捕获一个或多个数字(tid的值)
|
||||
// \]\]> 匹配CDATA结束标签
|
||||
String regex = "<pagepath><!\\[CDATA\\[[^\\]]*?tid=(\\d+)\\]\\]></pagepath>";
|
||||
|
||||
// 编译正则表达式,忽略大小写(防止标签大小写不一致)
|
||||
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = pattern.matcher(msg);
|
||||
|
||||
// 查找匹配项
|
||||
if (matcher.find()) {
|
||||
// 返回第一个捕获组(即tid的值)
|
||||
String tid = matcher.group(1);
|
||||
// 构建post请求 url:https://tm.wx.hackp.net/App/zm/getlist,参数:tid=tid放在body
|
||||
String url = "https://tm.wx.hackp.net/App/zm/getlist";
|
||||
String response = HttpRequest.post(url)
|
||||
.body("tid=" + tid)
|
||||
.execute().body();
|
||||
if (Util.isNotEmpty(response)) {
|
||||
JSONObject jsonObject = JSON.parseObject(response);
|
||||
/**
|
||||
* {
|
||||
* "code": "1",
|
||||
* "msg": {
|
||||
* "id": "1987",
|
||||
* "type": "3",
|
||||
* "tid": "52",
|
||||
* "hot": "2",
|
||||
* "title": "头条号项目玩法,从账号注册,素材获取到视频制作发布和裂变全方位教学 ",
|
||||
* "picname": "https://tm.wx.hackp.net/data/attachment/temp/202111/07/130031qb5lw95b259ehlnn.jpg_thumb.jpg",
|
||||
* "content": "<span style=\"color:#555555;font-family:"font-size:large;background-color:#FFFFFF;\">1.项目玩法介绍mp4</span><br />\r\n<br />\r\n<span style=\"color:#555555;font-family:"font-size:large;background-color:#FFFFFF;\">2.账号注册.mp4</span><br />\r\n<br />\r\n<span style=\"color:#555555;font-family:"font-size:large;background-color:#FFFFFF;\">3.素材获取mp4</span><br />\r\n<br />\r\n<span style=\"color:#555555;font-family:"font-size:large;background-color:#FFFFFF;\">4.视频批星制作.mp4</span><br />\r\n<br />\r\n<span style=\"color:#555555;font-family:"font-size:large;background-color:#FFFFFF;\">5.视频发布.mp4</span><br />\r\n<br />\r\n<span style=\"color:#555555;font-family:"font-size:large;background-color:#FFFFFF;\">6.视频裂变.mp4</span>",
|
||||
* "count": "10122",
|
||||
* "dizhi": "下载地址:链接: https://pan.baidu.com/s/104YEJVgTx4ZSNVJjw7n9oQ提取码: 92kr",
|
||||
* "price": "0",
|
||||
* "addtime": "1636261305",
|
||||
* "status": "1",
|
||||
* "hackp_id": "36657",
|
||||
* "buygoods": null
|
||||
* }
|
||||
* }
|
||||
* */
|
||||
JSONObject msgResponse = jsonObject.getJSONObject("msg");
|
||||
// title和dizhi
|
||||
wxUtil.sendTextMessage(fromWxid, msgResponse.getString("title") + "\n" + msgResponse.getString("dizhi"), 0, null, true);
|
||||
} else {
|
||||
wxUtil.sendTextMessage(fromWxid, "请求失败", 0, null, true);
|
||||
}
|
||||
} else {
|
||||
wxUtil.sendTextMessage(fromWxid, "没有匹配到tid", 0, null, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/main/java/cn/van/business/util/StringsUtil.java
Normal file
118
src/main/java/cn/van/business/util/StringsUtil.java
Normal file
@@ -0,0 +1,118 @@
|
||||
package cn.van.business.util;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 字符串常量集
|
||||
*
|
||||
*/
|
||||
public class StringsUtil {
|
||||
/**
|
||||
* 空字符串
|
||||
*/
|
||||
public static final String EMPTY = "";
|
||||
/**
|
||||
* 逗号
|
||||
*/
|
||||
public static final String COMMA = ",";
|
||||
/**
|
||||
* 句点
|
||||
*/
|
||||
public static final String DOT = ".";
|
||||
/**
|
||||
* 下划线
|
||||
*/
|
||||
public static final String UNDERLINE = "_";
|
||||
/**
|
||||
* 空格
|
||||
*/
|
||||
public static final String SPACE = " ";
|
||||
/**
|
||||
* 等于
|
||||
*/
|
||||
public static final String EQUAL = "=";
|
||||
/**
|
||||
* 星号
|
||||
*/
|
||||
public static final String ASTERISK = "*";
|
||||
/**
|
||||
* 双引号
|
||||
*/
|
||||
public static final String DOUBLE_QUOTES = "\"";
|
||||
/**
|
||||
* 单引号
|
||||
*/
|
||||
public static final String SINGLE_QUOTES = "'";
|
||||
/**
|
||||
* 回车符
|
||||
*/
|
||||
public static final String ENTER = "\n";
|
||||
/**
|
||||
* 左括弧
|
||||
*/
|
||||
public static final String LEFT_BRACKET = "(";
|
||||
/**
|
||||
* 右括弧
|
||||
*/
|
||||
public static final String RIGHT_BRACKET = ")";
|
||||
/**
|
||||
* 冒号
|
||||
*/
|
||||
public static final String COLON = ":";
|
||||
/**
|
||||
* 分号
|
||||
*/
|
||||
public static final String SEMICOLON = ";";
|
||||
/**
|
||||
* 斜杠
|
||||
*/
|
||||
public static final String SLASH = "/";
|
||||
/**
|
||||
* 反斜杠
|
||||
*/
|
||||
public static final String BACKSLASH = "\\";
|
||||
/**
|
||||
* 百分号
|
||||
*/
|
||||
public static final String PERCENT = "%";
|
||||
/**
|
||||
* 减号
|
||||
*/
|
||||
public static final String MINUS = "-";
|
||||
/**
|
||||
* 加号
|
||||
*/
|
||||
public static final String PLUS = "+";
|
||||
/**
|
||||
* 与号
|
||||
*/
|
||||
public static final String AND = "&";
|
||||
|
||||
/**
|
||||
* @
|
||||
*/
|
||||
public static final String AT = "@";
|
||||
|
||||
/**
|
||||
* 井号
|
||||
*/
|
||||
public static final String WELL = "#";
|
||||
|
||||
/**
|
||||
* 字符编码:UTF-8
|
||||
*/
|
||||
public static final String ENCODING_UTF8 = "UTF-8";
|
||||
/**
|
||||
* 字符编码:GBK
|
||||
*/
|
||||
public static final String ENCODING_GBK = "GBK";
|
||||
/**
|
||||
* 默认字符编码
|
||||
*/
|
||||
public static final String DEFAULT_ENCODING = StringsUtil.ENCODING_UTF8;
|
||||
|
||||
public static String randomUUID() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -23,6 +25,36 @@ Util {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Util.class);
|
||||
|
||||
/**
|
||||
* 将字符串转换为 MD5 摘要
|
||||
*
|
||||
* @param input 原始字符串
|
||||
* @return MD5 加密后的十六进制字符串
|
||||
*/
|
||||
public static String md5(String input) {
|
||||
try {
|
||||
// 创建 MessageDigest 实例,指定 MD5 算法
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
|
||||
// 将输入字符串转换为字节数组并进行哈希计算
|
||||
byte[] messageDigest = md.digest(input.getBytes());
|
||||
|
||||
// 将字节数组转换为十六进制字符串
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : messageDigest) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1)
|
||||
hexString.append('0'); // 补零
|
||||
hexString.append(hex);
|
||||
}
|
||||
|
||||
return hexString.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("MD5加密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* byte数组倒序
|
||||
*
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package cn.van.business.util;
|
||||
|
||||
import cn.van.business.model.wx.SuperAdmin;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.van.business.enums.WXReqType;
|
||||
import cn.van.business.mq.MessageProducerService;
|
||||
import cn.van.business.repository.SuperAdminRepository;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -12,12 +14,14 @@ import lombok.NoArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
@@ -28,8 +32,34 @@ import java.util.List;
|
||||
@Component
|
||||
public class WXUtil {
|
||||
|
||||
public static final String default_super_admin_wxid = "wxid_ytpc72mdoskt22";
|
||||
private static final Logger logger = LoggerFactory.getLogger(WXUtil.class);
|
||||
public static final String super_admin_wxid = "wxid_ytpc72mdoskt22";
|
||||
//大号
|
||||
//public static String default_bot_wxid = "wxid_kr145nk7l0an31";
|
||||
|
||||
//小号
|
||||
public static String default_bot_wxid = "wxid_cfmrk2upjtf322";
|
||||
|
||||
public static Map<String, SuperAdmin> super_admins = new HashMap<>();
|
||||
public static Map<String, String> jdidToWxidMap = new HashMap<>();
|
||||
public static Map<String, String> jdidToRemarkMap = new HashMap<>();
|
||||
public static List<String> notify_wx = new ArrayList<>();
|
||||
|
||||
// 群聊管理白名单
|
||||
public static List<String> chatRoom_admin = new ArrayList<>();
|
||||
public static List<String> chatRoom_admin_inner = new ArrayList<>();
|
||||
public static List<String> chatRoom_admin_pl = new ArrayList<>();
|
||||
|
||||
// 线报来源群
|
||||
public static Map<String, String> chatRoom_xb = new HashMap<>();
|
||||
|
||||
// 747|23:38:48|wxid_kr145nk7l0an31|收到群聊|群(50322578882@chatroom)wxid_ytpc72mdoskt22:1
|
||||
public static String chatRoom_BY = "50322578882@chatroom";
|
||||
|
||||
//群(50006079425@chatroom)wxid_cfmrk2upjtf322:1
|
||||
public static List<String> chatRoom_JD_Order = new ArrayList<>();
|
||||
|
||||
|
||||
/**
|
||||
* url http://127.0.0.1:7777/DaenWxHook/httpapi/
|
||||
* 获取微信列表 (X0000)
|
||||
@@ -61,13 +91,146 @@ public class WXUtil {
|
||||
* 发送名片(Q0025)
|
||||
*/
|
||||
public static String WX_BASE_URL;
|
||||
private Environment env;
|
||||
private final WxtsUtil wxTsUtil;
|
||||
private final MessageProducerService messageProducerService;
|
||||
private final SuperAdminRepository superAdminRepository;
|
||||
public int sendTimes = 0;
|
||||
private static boolean restartNoticeSent = false;
|
||||
|
||||
@Autowired
|
||||
public WXUtil(Environment env) {
|
||||
this.env = env;
|
||||
public WXUtil(Environment env, WxtsUtil wxTsUtil, @Lazy MessageProducerService messageProducerService, SuperAdminRepository superAdminRepository) {
|
||||
this.messageProducerService = messageProducerService;
|
||||
this.wxTsUtil = wxTsUtil;
|
||||
WX_BASE_URL = env.getProperty("config.WX_BASE_URL");
|
||||
this.superAdminRepository = superAdminRepository;
|
||||
System.out.println("WX_BASE_URL:" + WX_BASE_URL);
|
||||
initSuperAdmins();
|
||||
}
|
||||
|
||||
public static String getWxidFromJdid(String jdid) {
|
||||
return jdidToWxidMap.get(jdid);
|
||||
}
|
||||
|
||||
public static String getRemarkFromJdid(String jdid) {
|
||||
return jdidToRemarkMap.get(jdid);
|
||||
}
|
||||
public static String getJdidFromRemark(String remark) {
|
||||
for (Map.Entry<String, String> entry : jdidToRemarkMap.entrySet()) {
|
||||
if (entry.getValue().equals(remark)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<SuperAdmin> getSuperAdmins(String wxid) {
|
||||
List<SuperAdmin> result = new ArrayList<>();
|
||||
for (SuperAdmin admin : super_admins.values()) {
|
||||
if (admin.getWxid().equals(wxid)) {
|
||||
result.add(admin);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据unionId获取SuperAdmin的接收人列表
|
||||
* @param unionId 联盟ID
|
||||
* @return 接收人列表(企业微信用户ID,多个用逗号分隔),如果未配置则返回null
|
||||
*/
|
||||
public static String getTouserByUnionId(String unionId) {
|
||||
if (unionId == null || unionId.trim().isEmpty()) {
|
||||
logger.debug("getTouserByUnionId: unionId为空");
|
||||
return null;
|
||||
}
|
||||
logger.debug("getTouserByUnionId: 查找unionId={}, super_admins数量={}", unionId, super_admins.size());
|
||||
for (SuperAdmin admin : super_admins.values()) {
|
||||
if (unionId.equals(admin.getUnionId())) {
|
||||
String touser = admin.getTouser();
|
||||
logger.debug("getTouserByUnionId: 找到匹配的SuperAdmin, unionId={}, name={}, touser={}",
|
||||
admin.getUnionId(), admin.getName(), touser);
|
||||
if (touser != null && !touser.trim().isEmpty()) {
|
||||
return touser.trim();
|
||||
} else {
|
||||
logger.debug("getTouserByUnionId: SuperAdmin的touser字段为空或未配置");
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug("getTouserByUnionId: 未找到匹配的SuperAdmin, unionId={}", unionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static List<String> splitStringByLength(String input, int length) {
|
||||
List<String> result = new ArrayList<>();
|
||||
// 循环增加长度直到超过字符串长度
|
||||
for (int start = 0; start < input.length(); start += length) {
|
||||
// 截取字符串,但需要检查边界
|
||||
int end = Math.min(start + length, input.length());
|
||||
result.add(input.substring(start, end));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 初始化超级管理员
|
||||
public void initSuperAdmins() {
|
||||
if (restartNoticeSent) {
|
||||
return;
|
||||
}
|
||||
logger.info("初始化超级管理员");
|
||||
List<SuperAdmin> superAdminList = superAdminRepository.findAll();
|
||||
for (SuperAdmin superAdmin : superAdminList) {
|
||||
super_admins.put(superAdmin.getWxid() + superAdmin.getUnionId(), superAdmin);
|
||||
if (Util.isNotEmpty(superAdmin.getUnionId())){
|
||||
jdidToWxidMap.put(superAdmin.getUnionId(), superAdmin.getWxid());
|
||||
jdidToRemarkMap.put(superAdmin.getUnionId(), superAdmin.getName());
|
||||
}
|
||||
logger.info("超级管理员:{} {}, unionId={}, touser={}",
|
||||
superAdmin.getName(), superAdmin.getWxid(), superAdmin.getUnionId(), superAdmin.getTouser());
|
||||
}
|
||||
|
||||
/* 内部管理群 */
|
||||
|
||||
// 方案
|
||||
//chatRoom_admin.add("50400969285@chatroom");
|
||||
// 闲鱼
|
||||
chatRoom_admin.add("50203565991@chatroom");
|
||||
chatRoom_admin_inner.add("50203565991@chatroom");
|
||||
// 什么都发
|
||||
chatRoom_admin.add("49533691813@chatroom");
|
||||
chatRoom_admin_inner.add("49533691813@chatroom");
|
||||
// 方案交互群
|
||||
chatRoom_admin.add("44960628585@chatroom");
|
||||
chatRoom_admin_inner.add("44960628585@chatroom");
|
||||
|
||||
// 评价生成群 大群
|
||||
chatRoom_admin_pl.add("47484514467@chatroom");
|
||||
// 评价生成群 小群 群(43745034055@chatroom)wxid_gca9mnidqhkq11:加入群聊
|
||||
chatRoom_admin_pl.add("43745034055@chatroom");
|
||||
//群(43835433515@chatroom)wxid_ytpc72mdoskt22:1
|
||||
chatRoom_admin_pl.add("43835433515@chatroom");
|
||||
//群(47981003490@chatroom):"Cheonhee"已成为新群主
|
||||
chatRoom_admin_pl.add("47981003490@chatroom");
|
||||
|
||||
/* 线报采集来源群 */
|
||||
// 玩了买
|
||||
chatRoom_xb.put("23143922156@chatroom", "玩乐买");
|
||||
|
||||
chatRoom_xb.put("44980131813@chatroom", "舵手");
|
||||
//786|14:05:38|wxid_kr145nk7l0an31|收到群聊|群(46156118222@chatroom):"130大号"修改群名为“\uD83E\uDD16 转链 礼金通知”
|
||||
chatRoom_xb.put("46156118222@chatroom", "测试群");
|
||||
|
||||
/*录单群*/
|
||||
chatRoom_JD_Order.add("50006079425@chatroom");
|
||||
// 109|17:07:20|wxid_kr145nk7l0an31|收到群聊|群(48146712436@chatroom)wxid_ytpc72mdoskt22:1
|
||||
chatRoom_JD_Order.add("48146712436@chatroom");
|
||||
|
||||
String messageContent = "Jarvis 更新完成 [亲亲][亲亲][亲亲] ";
|
||||
String fromWxid = default_bot_wxid; // 来源为机器人自身
|
||||
sendTextMessage(default_super_admin_wxid, messageContent, 1, fromWxid, false);
|
||||
|
||||
restartNoticeSent = true;
|
||||
|
||||
}
|
||||
|
||||
// 获取微信列表
|
||||
@@ -82,40 +245,38 @@ public class WXUtil {
|
||||
|
||||
}
|
||||
|
||||
public static List<String> splitStringByLength(String input, int length) {
|
||||
List<String> result = new ArrayList<>();
|
||||
// 循环增加长度直到超过字符串长度
|
||||
for (int start = 0; start < input.length(); start += length) {
|
||||
// 截取字符串,但需要检查边界
|
||||
int end = Math.min(start + length, input.length());
|
||||
result.add(input.substring(start, end));
|
||||
}
|
||||
return result;
|
||||
public void sendTextMessage(String wxid, String content, Integer msgType, String fromwxid, Boolean hiddenTime) {
|
||||
sendTextMessage(wxid, content, msgType, fromwxid, hiddenTime, null);
|
||||
}
|
||||
|
||||
// 发送文本消息 msgType 1:私聊 2:群发
|
||||
public void sendTextMessage(String wxid, String content, Integer msgType, String fromwxid) {
|
||||
public void sendTextMessage(String wxid, String content, Integer msgType, String fromwxid, Boolean hiddenTime, String touser) {
|
||||
// 全部打印
|
||||
logger.info("发送文本消息 msgType: {} wxid: {} fromwxid: {} content: {}", msgType, wxid, fromwxid, content);
|
||||
List<String> strings = splitStringByLength(content, 3072);
|
||||
//logger.info("发送文本消息 msgType: {} wxid: {} fromwxid: {} content: {}", msgType, wxid, fromwxid, content);
|
||||
// 先在content顶部插入时间戳
|
||||
// 因为引入了消息队列,所以在每条消息都加上时间戳 格式化成 yyyy-MM-dd HH:mm:ss
|
||||
if (!hiddenTime) {
|
||||
content = "[ " + DateUtil.format(new Date(), "HH:mm:ss yyyy-MM-dd") + " ] \r\n" + content;
|
||||
}
|
||||
|
||||
// 如果是自己的微信,所有信息都加上少爷
|
||||
//if (wxid.equals(super_admin_wxid) || fromwxid.equals(super_admin_wxid)) {
|
||||
// content = "超管: 凡神 !\r\n" + content;
|
||||
//}
|
||||
List<String> strings = splitStringByLength(content, 4096);
|
||||
int count = 1;
|
||||
for (String string : strings) {
|
||||
if (strings.size()>1) {
|
||||
string = "---长消息---第:" + count + "条 "+ "\r" + string ;
|
||||
|
||||
if (strings.size() > 1) {
|
||||
string = "---长消息---第:" + count + "条 " + "\r" + string;
|
||||
}
|
||||
count++;
|
||||
// 如果是自己的微信,所有信息都加上少爷
|
||||
if (wxid.equals(super_admin_wxid) || fromwxid.equals(super_admin_wxid)) {
|
||||
string = "超管: 凡神 !\r\n" + string;
|
||||
}
|
||||
|
||||
//JSONObject wxList = getWxList();
|
||||
//JSONObject wxBotInfo = (JSONObject) wxList.getJSONArray("result").get(0);
|
||||
//botWxid = wxBotInfo.getString("wxid");
|
||||
//
|
||||
////
|
||||
//WxReqDate wxReqDate = createWxReqData(WXReqType.SEND_TEXT_MESSAGE);
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("type", WXReqType.SEND_TEXT_MESSAGE.getType());
|
||||
WxReqDate wxReqDate = createWxReqData(WXReqType.SEND_TEXT_MESSAGE);
|
||||
JSONObject data = new JSONObject();
|
||||
//if ((msgType.equals(1))) {
|
||||
// jsonObject.put("wxid", wxid);
|
||||
// content = content;
|
||||
@@ -130,54 +291,45 @@ public class WXUtil {
|
||||
"wxid": "filehelper",
|
||||
"msg": "666大佬~"
|
||||
}*/
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("msg", string);
|
||||
data.put("wxid", wxid);
|
||||
jsonObject.put("data", data);
|
||||
data.put("msgType", msgType);
|
||||
data.put("fromWxid", fromwxid);
|
||||
data.put("hiddenTime", hiddenTime);
|
||||
// 如果提供了接收人列表,添加到数据中
|
||||
if (touser != null && !touser.trim().isEmpty()) {
|
||||
data.put("touser", touser.trim());
|
||||
}
|
||||
wxReqDate.setData(data);
|
||||
// wxReqDate 转成 JSONObject
|
||||
JSONObject message = JSON.parseObject(JSON.toJSONString(wxReqDate));
|
||||
|
||||
|
||||
//System.out.println(JSON.toJSONString(jsonObject));
|
||||
//wxReqDate.setData(jsonObject);
|
||||
if (Util.isNotEmpty(wxid)) {
|
||||
String responseStr = HttpRequest.post(WX_BASE_URL)
|
||||
.body(JSON.toJSONString(jsonObject)).execute().body();
|
||||
if (ObjectUtil.isNotEmpty(responseStr)) {
|
||||
JSONObject response = JSON.parseObject(responseStr);
|
||||
logger.info("消息响应:{}", response.toString());
|
||||
//return response;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
// 把消息发送到RocketMQ,使用'wx-message'作为topic,jsonObject作为消息内容。
|
||||
messageProducerService.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//private JSONObject sendWxReq(WxReqDate wxReqDate) {
|
||||
// if (wxReqDate == null) {
|
||||
// return null;
|
||||
// } else {
|
||||
// logger.info("wxReqDate: {}", wxReqDate);
|
||||
//
|
||||
// String responseStr = HttpRequest.post(WX_BASE_URL).body(JSON.toJSONString(wxReqDate)).execute().body();
|
||||
// if (ObjectUtil.isNotEmpty(responseStr)) {
|
||||
// JSONObject jsonObject = JSON.parseObject(responseStr);
|
||||
// //WxResponse wxResponse = JSON.parseObject(responseStr, WxResponse.class);
|
||||
// //System.out.println(wxResponse);
|
||||
// //if (Objects.equals(String.valueOf(wxResponse.getCode()), "200")) {
|
||||
// // return wxResponse.getData();
|
||||
// //}
|
||||
// //JSONObject jsonObject = HttpUtil.sendPost(url, wxReqDate.getData());
|
||||
// return jsonObject;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return null;
|
||||
// }
|
||||
//}
|
||||
public void sendImageMessage(String wxid, String imagePath) {
|
||||
WxReqDate wxReqDate = createWxReqData(WXReqType.SEND_IMAGE);
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("wxid", wxid);
|
||||
data.put("path", imagePath);
|
||||
String[] split = imagePath.split("/");
|
||||
data.put("fileName", split[split.length - 1]);
|
||||
wxReqDate.setData(data);
|
||||
JSONObject message = JSON.parseObject(JSON.toJSONString(wxReqDate));
|
||||
|
||||
if (Util.isNotEmpty(wxid)) {
|
||||
// 把消息发送到RocketMQ,使用'wx-message'作为topic,jsonObject作为消息内容。
|
||||
messageProducerService.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {
|
||||
@@ -209,6 +361,29 @@ public class WXUtil {
|
||||
|
||||
}
|
||||
|
||||
//private JSONObject sendWxReq(WxReqDate wxReqDate) {
|
||||
// if (wxReqDate == null) {
|
||||
// return null;
|
||||
// } else {
|
||||
// logger.info("wxReqDate: {}", wxReqDate);
|
||||
//
|
||||
// String responseStr = HttpRequest.post(WX_BASE_URL).body(JSON.toJSONString(wxReqDate)).execute().body();
|
||||
// if (ObjectUtil.isNotEmpty(responseStr)) {
|
||||
// JSONObject jsonObject = JSON.parseObject(responseStr);
|
||||
// //WxResponse wxResponse = JSON.parseObject(responseStr, WxResponse.class);
|
||||
// //System.out.println(wxResponse);
|
||||
// //if (Objects.equals(String.valueOf(wxResponse.getCode()), "200")) {
|
||||
// // return wxResponse.getData();
|
||||
// //}
|
||||
// //JSONObject jsonObject = HttpUtil.sendPost(url, wxReqDate.getData());
|
||||
// return jsonObject;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return null;
|
||||
// }
|
||||
//}
|
||||
|
||||
public WxReqDate createWxReqData(WXReqType wxReqType) {
|
||||
|
||||
WxReqDate wxReqDate = new WxReqDate(wxReqType.getType(), null);
|
||||
@@ -216,6 +391,67 @@ public class WXUtil {
|
||||
return wxReqDate;
|
||||
}
|
||||
|
||||
//@Scheduled(cron = "0 * * * * ?")
|
||||
public void checkWxStatus() {
|
||||
WxReqDate wxReqDate = createWxReqData(WXReqType.GET_WX_STATUS);
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("wxid", default_bot_wxid);
|
||||
wxReqDate.setData(data);
|
||||
String responseStr = HttpRequest.post(WX_BASE_URL).body(JSON.toJSONString(wxReqDate)).execute().body();
|
||||
if (ObjectUtil.isNotEmpty(responseStr)) {
|
||||
JSONObject jsonObject = JSON.parseObject(responseStr);
|
||||
/**
|
||||
* {
|
||||
* "code": 200,
|
||||
* "msg": "正常",
|
||||
* "result": {
|
||||
* "startTimeStamp": "1716467892",
|
||||
* "startTime": "2024年5月23日20时38分12秒",
|
||||
* "runTime": "3分10秒",
|
||||
* "recv": 0,
|
||||
* "send": 0,
|
||||
* "wxNum": "DaenPro",
|
||||
* "nick": "小鹿\\uD83D\\uDE00\\uD83D\\uDE00摸",
|
||||
* "wxid": "wxid_nq6r0w9v12612"
|
||||
* },
|
||||
* "wxid": "wxid_nq6r0w9v12612",
|
||||
* "port": 7799,
|
||||
* "pid": 18892,
|
||||
* "flag": "7888",
|
||||
* "timestamp": "1716468082967"
|
||||
* }
|
||||
* */
|
||||
Integer code = jsonObject.getInteger("code");
|
||||
if (code == 500) {
|
||||
if (sendTimes > 3) {
|
||||
return;
|
||||
}
|
||||
wxTsUtil.sendCriticalAlert("微信状态异常", jsonObject.getString("msg"));
|
||||
sendTimes++;
|
||||
} else if (code == 200) {
|
||||
sendTimes = 0;
|
||||
}
|
||||
} else {
|
||||
// 新建格式化日期
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒");
|
||||
wxTsUtil.sendCriticalAlert("千寻框架状态异常", dateFormat.format(new Date()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
private static class WxReqDate {
|
||||
//{
|
||||
// "type": "X0000",
|
||||
// "data": {}
|
||||
//}
|
||||
private String type;
|
||||
private JSONObject data;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@@ -242,18 +478,4 @@ public class WXUtil {
|
||||
private String timestamp;
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
private static class WxReqDate {
|
||||
//{
|
||||
// "type": "X0000",
|
||||
// "data": {}
|
||||
//}
|
||||
private String type;
|
||||
private JSONObject data;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@ package cn.van.business.util;
|
||||
|
||||
|
||||
import cn.van.business.enums.FromType;
|
||||
import cn.van.business.model.wx.SuperAdmin;
|
||||
import cn.van.business.model.wx.WxMessage;
|
||||
import cn.van.business.repository.SettingRepository;
|
||||
import cn.van.business.repository.WxMessageDataForChatRepository;
|
||||
import cn.van.business.repository.WxUserRepository;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -14,13 +12,7 @@ import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static cn.van.business.util.WXUtil.super_admin_wxid;
|
||||
import static cn.van.business.util.WXUtil.*;
|
||||
|
||||
|
||||
/**
|
||||
@@ -31,210 +23,29 @@ import static cn.van.business.util.WXUtil.super_admin_wxid;
|
||||
*/
|
||||
@Component
|
||||
public class WxMessageConsumer {
|
||||
|
||||
private static final String meituanCookie = "meituanCookie";
|
||||
/**
|
||||
* key开头的为 setting 的 key
|
||||
*/
|
||||
private static final String key_caiDan_user = "用户菜单";
|
||||
private static final String key_caiDan_admin = "管理员菜单";
|
||||
/**
|
||||
* order 开头的是接受的指令
|
||||
*/
|
||||
private static final String order_caiDan = "菜单";
|
||||
private static final String order_admin = "管理员";
|
||||
/**/
|
||||
private static final Logger logger = LoggerFactory.getLogger(WxMessageConsumer.class);
|
||||
|
||||
/**
|
||||
* 临时参数
|
||||
* 每次扣费
|
||||
*/
|
||||
private static final BigDecimal priceOfMT20 = new BigDecimal("0.2");
|
||||
private static final Integer fromGR = 10008;
|
||||
private static final String SERVER_URL = "https://api.jd.com/routerjson";
|
||||
private static final String accessToken = "";
|
||||
private static final String appKey = "98e21c89ae5610240ec3f5f575f86a59";
|
||||
private static final String appSecret = "3dcb6b23a1104639ac433fd07adb6dfb";
|
||||
private final WXUtil wxUtil;
|
||||
private final QLUtil qlUtil;
|
||||
private final WxMessageDataForChatRepository wxMessageDataForChatRepository;
|
||||
private final WxUserRepository wxUserRepository;
|
||||
private final SettingRepository settingRepository;
|
||||
private final JDUtils jdUtils;
|
||||
private final JDUtil jdUtils;
|
||||
|
||||
private final OtherUtil otherUtil;
|
||||
|
||||
@Autowired
|
||||
public WxMessageConsumer(WXUtil wxUtil, QLUtil qlUtil,
|
||||
@Lazy WxMessageDataForChatRepository wxMessageDataForChatService,
|
||||
@Lazy WxUserRepository wxUserRepository,
|
||||
@Lazy SettingRepository settingRepository,
|
||||
@Lazy JDUtils jdUtils) {
|
||||
this.wxUtil = wxUtil;
|
||||
this.qlUtil = qlUtil;
|
||||
this.wxMessageDataForChatRepository = wxMessageDataForChatService;
|
||||
this.wxUserRepository = wxUserRepository;
|
||||
this.settingRepository = settingRepository;
|
||||
public WxMessageConsumer(@Lazy JDUtil jdUtils, OtherUtil otherUtil) {
|
||||
this.jdUtils = jdUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从京东商品 URL 中提取产品 ID
|
||||
*
|
||||
* @param url 需要解析的 URL
|
||||
* @return 提取出的产品 ID
|
||||
*/
|
||||
public static String extractProductId(String url) {
|
||||
// 使用正则表达式匹配 pattern 包含 /product/ 后跟一系列数字,结束于 .html
|
||||
Pattern pattern = Pattern.compile("/product/(\\d+)\\.html");
|
||||
Matcher matcher = pattern.matcher(url);
|
||||
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1); // 返回第一个捕获组(产品 ID)
|
||||
}
|
||||
return null; // 如果没有找到匹配项,返回 null
|
||||
}
|
||||
|
||||
//private Boolean heiMingDan(String wxid) {
|
||||
// // 0 正常 1 黑名单
|
||||
// //
|
||||
// boolean flag = false;
|
||||
// WxUser wxUser = wxUserRepository.getOne(Wrappers.query(new WxUser()).eq("wxid", wxid));
|
||||
// if (Util.isNotEmpty(wxUser)) {
|
||||
// if (wxUser.getStatus().equals(1)) {
|
||||
// flag = true;
|
||||
// }
|
||||
// }
|
||||
// return flag;
|
||||
//}
|
||||
|
||||
|
||||
//private Boolean isNew(String wxid) {
|
||||
// // 0 是 1 不是
|
||||
// boolean flag = false;
|
||||
// WxUser wxUser = wxUserRepository.getOne(Wrappers.query(new WxUser()).eq("wxid", wxid));
|
||||
// if (Util.isNotEmpty(wxUser)) {
|
||||
// if (wxUser.getIsnew().equals(0)) {
|
||||
// flag = true;
|
||||
// }
|
||||
// }
|
||||
// return flag;
|
||||
//}
|
||||
|
||||
//private Boolean isNew(Integer isNew) {
|
||||
// // 0 是 1 不是
|
||||
// boolean flag = false;
|
||||
// if (isNew.equals(0)) {
|
||||
// flag = true;
|
||||
// }
|
||||
// return flag;
|
||||
//}
|
||||
|
||||
/**
|
||||
* @param wxMessage
|
||||
* @return
|
||||
* @throws
|
||||
* @description
|
||||
*/
|
||||
//private void handleTransferEvent(WxMessage wxMessage) {
|
||||
// Integer msgType = 1;
|
||||
//
|
||||
// /**
|
||||
// * {
|
||||
// * "fromWxid": "wxid_ytpc72mdoskt22", 对方wxid
|
||||
// * "msgSource": 1, 1|收到转账 2|对方接收转账 3|发出转账 4|自己接收转账 5|对方退还 6|自己退还
|
||||
// * "transType": 1, 1|即时到账 2|延时到账
|
||||
// * "money": "2.00", 金额,单位元
|
||||
// * "memo": "", 转账备注
|
||||
// * "transferid": "1000050001202312250424037787039", 转账ID
|
||||
// * "invalidtime": "1703577220" 10位时间戳
|
||||
// * }*/
|
||||
// JSONObject data = wxMessage.getData().getJSONObject("data");
|
||||
// if (data == null) {
|
||||
// return;
|
||||
// }
|
||||
// WxMessageDataForTransfer wxMessageDataForTransfer = data.to(WxMessageDataForTransfer.class);
|
||||
//
|
||||
// String result = null;
|
||||
// String wxid = wxMessageDataForTransfer.getFromwxid();
|
||||
//
|
||||
//
|
||||
//
|
||||
// if (heiMingDan(wxid)) {
|
||||
// result = "您已被拉黑,请联系客服!";
|
||||
// } else {
|
||||
// if (wxMessageDataForTransfer.getTranstype().equals(2)) {
|
||||
// result = "请勿使用延时到账功能。累计三次将永久拉黑!";
|
||||
// } else {
|
||||
//
|
||||
// JSONObject shouKuanResult = wxUtil.queRenShouKuan(wxid, wxMessageDataForTransfer.getTransferid());
|
||||
// if (shouKuanResult == null) {
|
||||
// result = "查询转账失败,请稍后再试。";
|
||||
// }
|
||||
// if (shouKuanResult != null && shouKuanResult.getInteger("code") == 200) {
|
||||
// BigDecimal money = wxMessageDataForTransfer.getMoney();
|
||||
// if (money.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// WxUser wxUser = wxUserService.getOne(Wrappers.query(new WxUser()).eq("wxid", wxid));
|
||||
// wxUser.setMoneyLeiji(wxUser.getMoneyLeiji().add(money));
|
||||
// wxUser.setMoneyShengyu(wxUser.getMoneyShengyu().add(money));
|
||||
// wxUser.setCountChongzhi(wxUser.getCountChongzhi().add(BigDecimal.ONE));
|
||||
// wxUserService.updateById(wxUser);
|
||||
// result = "收到转账" + money + "元,已成功存入账户。感谢您的使用。";
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// wxUtil.sendTextMessage(wxid, result, msgType, null);
|
||||
//
|
||||
//}
|
||||
private static String getUrlStr(String msg) {
|
||||
//String urlPattern = "https?://[\\w-\\.]+(\\.[a-z]{2,})?(/[\\w-./?%&=]*)?"
|
||||
String urlPattern = "https?://[^\\s]+?\\.(html|htm)(\\?[^\\s]*?)?";
|
||||
Pattern pattern = Pattern.compile(urlPattern);
|
||||
Matcher matcher = pattern.matcher(msg);
|
||||
|
||||
// 检查是否存在URL,如果存在则打印出来
|
||||
String finallyUrl = null;
|
||||
if (matcher.find()) {
|
||||
finallyUrl = matcher.group();
|
||||
System.out.println("Extracted URL: " + finallyUrl);
|
||||
} else {
|
||||
System.out.println("No URL found in the given text.");
|
||||
}
|
||||
if (finallyUrl != null && finallyUrl.endsWith("?")) {
|
||||
// 移除最后一个字符(即问号)
|
||||
finallyUrl = finallyUrl.substring(0, finallyUrl.length() - 1);
|
||||
}
|
||||
if (finallyUrl.contains("item.m.jd.com/product")) {
|
||||
finallyUrl = finallyUrl.replace("item.m.jd.com/product", "item.jd.com");
|
||||
}
|
||||
return finallyUrl;
|
||||
this.otherUtil = otherUtil;
|
||||
}
|
||||
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void consume(WxMessage wxMessage) throws Exception {
|
||||
//logger.info("接收到消息 : {}", wxMessage);
|
||||
if (wxMessage.getEvent() == null) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 需要处理 私聊 和 转账消息
|
||||
* 其他消息暂时不处理
|
||||
* 私聊需要解析是否美团领券
|
||||
* 转账需要对接会员系统
|
||||
*
|
||||
* */
|
||||
//WxMessage.DataSection data = wxMessage.getData();
|
||||
|
||||
if (FromType.PRIVATE.getKey().equals(wxMessage.getEvent())) {
|
||||
handlePrivateMessage(wxMessage);
|
||||
} else if (FromType.GROUP.getKey().equals(wxMessage.getEvent())) {
|
||||
//handleGroupMessage(wxMessage);
|
||||
handleGroupMessage(wxMessage);
|
||||
}
|
||||
//if (event.equals(EventType.TRANSFER_EVENT.getKey())) {
|
||||
// handleTransferEvent(wxMessage);
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,561 +53,98 @@ public class WxMessageConsumer {
|
||||
*
|
||||
* @param wxMessage
|
||||
*/
|
||||
private void handlePrivateMessage(WxMessage wxMessage) throws Exception {
|
||||
Integer msgType = 1;
|
||||
// 做业务处理
|
||||
//logger.info("处理消息: {}", JSON.toJSONString(wxMessage));
|
||||
private void handlePrivateMessage(WxMessage wxMessage) {
|
||||
logger.info("处理私聊消息: {}", JSON.toJSONString(wxMessage));
|
||||
|
||||
/**
|
||||
* {
|
||||
* "event": 10009,
|
||||
* "wxid": "wxid_nq6r0w9v12612",
|
||||
* "data": {
|
||||
* "type": "recvMsg",
|
||||
* "des": "收到消息",
|
||||
* "data": {
|
||||
* "timeStamp": "1716620300237",
|
||||
* "fromType": 1,
|
||||
* "msgType": 1,
|
||||
* "msgSource": 0,
|
||||
* "fromWxid": "wxid_3sq4tklb6c3121",
|
||||
* "finalFromWxid": "",
|
||||
* "atWxidList": [],
|
||||
* "silence": 0,
|
||||
* "membercount": 0,
|
||||
* "signature": "V1_uKhKVjB1|v1_uKhKVjB1",
|
||||
* "msg": " 你在干嘛呢",
|
||||
* "msgId": "4937897417714063715",
|
||||
* "msgBase64": "IOS9oOWcqOW5suWYm+WRog=="
|
||||
* },
|
||||
* "timestamp": "1716620300238",
|
||||
* "wxid": "wxid_nq6r0w9v12612",
|
||||
* "port": 8888,
|
||||
* "pid": 3944,
|
||||
* "flag": "7888"
|
||||
* }
|
||||
* }
|
||||
* */
|
||||
WxMessage.DataSection data = wxMessage.getData();
|
||||
WxMessage.DataSection.InnerData innerData = data.getData();
|
||||
Integer event = wxMessage.getEvent();
|
||||
|
||||
if (Util.isAnyEmpty(innerData.getMsg(), innerData.getFromWxid())) {
|
||||
logger.info("消息内容为空,不处理");
|
||||
return;
|
||||
} else {
|
||||
logger.info("消息内容:{}", innerData.getMsg());
|
||||
}
|
||||
// 只处理超管的消息
|
||||
if (!Objects.equals(innerData.getFromWxid(), super_admin_wxid)) {
|
||||
String fromWxid = innerData.getFromWxid();
|
||||
SuperAdmin superAdmin = getSuperAdmins(fromWxid).get(0);
|
||||
if (Util.isEmpty(superAdmin)) {
|
||||
logger.info("不是超管消息,不处理");
|
||||
return;
|
||||
}
|
||||
|
||||
String msg = innerData.getMsg();
|
||||
//美团 20-7 + https://i.meituan.com/mttouch/page/account?userId=3822095266&token=AgHdIkm2tAGHc9SQSiG7M8xCx1LbTue9D2HPOAun2eYl3ou7BeEw1uGrGZH-DxmEiUgsbA1v9SM4DQAAAAC6HAAAz0rTXmkB_CIHin08hCu68mFv5k6nUc2q6_CfZqEdBcngRK_xD8Sx5fE4rfdq-yAJ, msgbase64=576O5ZuiIDIwLTcgKyBodHRwczovL2kubWVpdHVhbi5jb20vbXR0b3VjaC9wYWdlL2FjY291bnQ/dXNlcklkPTM4MjIwOTUyNjYmdG9rZW49QWdIZElrbTJ0QUdIYzlTUVNpRzdNOHhDeDFMYlR1ZTlEMkhQT0F1bjJlWWwzb3U3QmVFdzF1R3JHWkgtRHhtRWlVZ3NiQTF2OVNNNERRQUFBQUM2SEFBQXowclRYbWtCX0NJSGluMDhoQ3U2OG1GdjVrNm5VYzJxNl9DZlpxRWRCY25nUktfeEQ4U3g1ZkU0cmZkcS15QUo=
|
||||
if (msg.startsWith("转链")) {
|
||||
String wxid;
|
||||
if (Objects.equals(event, fromGR)) {
|
||||
wxid = innerData.getFromWxid();
|
||||
} else {
|
||||
wxid = innerData.getFinalFromWxid();
|
||||
}
|
||||
|
||||
// 使用正则表达式匹配URL
|
||||
//从 转链https://item.m.jd.com/product/100065976064.html?utm_user=plusmember&gx=RnAomTM2bGbfy59DrNFzDHu0uUde7Oc&gxd=RnAoxWMLamXdwpscqIV-D94totD10SY&ad_od=share&utm_source=androidapp&utm_medium=appshare&utm_campaign=t_335139774&utm_term=CopyURL_shareid64b2a4939719b1d3173112851071496926_shangxiang_none
|
||||
// 获取 100065976064
|
||||
logger.info("处理转链消息");
|
||||
|
||||
//String finallyUrl = getUrlStr(msg);
|
||||
//String finallyUrl = extractProductId(msg);
|
||||
String finallyUrl = msg.substring(2);
|
||||
if (Util.isNotEmpty(finallyUrl)) {
|
||||
String transferResultUrl = jdUtils.transfer(finallyUrl);
|
||||
wxUtil.sendTextMessage(wxid, transferResultUrl, msgType, null);
|
||||
}
|
||||
} else if (msg.startsWith("京")) {
|
||||
jdUtils.sendOrderToWxByOrderJD(msg.replace("京", ""));
|
||||
if (msg.startsWith("京")) {
|
||||
logger.info("消息以京开头,处理京东指令消息");
|
||||
jdUtils.sendOrderToWxByOrderJD(msg.replace("京", ""), fromWxid);
|
||||
return;
|
||||
}
|
||||
//else if (msg.startsWith("美团 ")) {
|
||||
// logger.info("处理美团的消息");
|
||||
// msg = msg.substring(msg.indexOf("https://i.meituan.com/mttouch/page/account"));
|
||||
// String[] all = msg.split("\\?");
|
||||
//
|
||||
// if (all.length == 2) {
|
||||
// String wxid = null;
|
||||
// if (wxMessage.getFromtype().equals(fromGR)) {
|
||||
// wxid = wxMessage.getFromid();
|
||||
// } else {
|
||||
// wxid = wxMessage.getFromgid();
|
||||
// }
|
||||
// String httpData = all[1];
|
||||
// String[] httpDataArr = httpData.split("&");
|
||||
// if (httpDataArr.length == 2) {
|
||||
// // 调用美团
|
||||
// //String result = mt20(wxid, httpDataArr[0].split("=")[1], httpDataArr[1].split("=")[1]);
|
||||
//
|
||||
// //wxUtil.sendTextMessage(wxid, result, msgType, null);
|
||||
// } else {
|
||||
//
|
||||
// wxUtil.sendTextMessage(wxid, "请检查提交的数据格式是否正确。", msgType, null);
|
||||
// }
|
||||
// }
|
||||
//} else if ("余额".equals(msg)) {
|
||||
// String wxid = null;
|
||||
// if (wxMessageDataForChat.getFromtype() == 1) {
|
||||
// wxid = wxMessageDataForChat.getFromwxid();
|
||||
// } else if (wxMessageDataForChat.getFromtype() == 2) {
|
||||
// wxid = wxMessageDataForChat.getFinalfromwxid();
|
||||
// }
|
||||
// WxUser wxUser = wxUserService.getOne(Wrappers.query(new WxUser()).eq("wxid", wxid));
|
||||
// String result = "";
|
||||
// if (Util.isNotEmpty(wxUser)) {
|
||||
// result = "您的余额为:" + wxUser.getMoneyLeiji() + "元\r";
|
||||
// result = result + " 您的消费次数为:" + wxUser.getCountXiaofei() + "次\r";
|
||||
// result = result + " 您的充值次数为:" + wxUser.getCountChongzhi() + "次\r";
|
||||
// result = result + " 您的累计充值为:" + wxUser.getMoneyLeiji() + "元";
|
||||
// } else {
|
||||
// result = "暂未查询到充值记录。\r";
|
||||
// }
|
||||
//
|
||||
//
|
||||
// wxUtil.sendTextMessage(wxid, result, msgType, null);
|
||||
//} else if ("体验".equals(msg)) {
|
||||
// String wxid = null;
|
||||
// if (wxMessageDataForChat.getFromtype() == 1) {
|
||||
// wxid = wxMessageDataForChat.getFromwxid();
|
||||
// } else if (wxMessageDataForChat.getFromtype() == 2) {
|
||||
// wxid = wxMessageDataForChat.getFinalfromwxid();
|
||||
// }
|
||||
// String result = "";
|
||||
// if (heiMingDan(wxid)) {
|
||||
// result = "黑名单!";
|
||||
// } else {
|
||||
// WxUser wxUser = wxUserService.getOne(Wrappers.query(new WxUser()).eq("wxid", wxid));
|
||||
// if (isNew(wxUser.getIsnew())) {
|
||||
// wxUser.setMoneyLeiji(wxUser.getMoneyLeiji().add(new BigDecimal(1)));
|
||||
// wxUser.setMoneyShengyu(wxUser.getMoneyShengyu().add(new BigDecimal(1)));
|
||||
// wxUser.setCountChongzhi(wxUser.getCountChongzhi().add(BigDecimal.ONE));
|
||||
// wxUser.setIsnew(1);
|
||||
// wxUserService.updateById(wxUser);
|
||||
// result = "体验成功,您已成功充值" + 1.00 + "元,已成功存入账户。感谢您的使用。";
|
||||
// } else {
|
||||
// result = "您已体验过,请勿重复体验。";
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// wxUtil.sendTextMessage(wxid, result, msgType, null);
|
||||
//
|
||||
//}// 用户返回用户菜单
|
||||
//else if (order_caiDan.equals(msg)) {
|
||||
// String wxid = null;
|
||||
// if (wxMessageDataForChat.getFromtype() == 1) {
|
||||
// wxid = wxMessageDataForChat.getFromwxid();
|
||||
// } else if (wxMessageDataForChat.getFromtype() == 2) {
|
||||
// wxid = wxMessageDataForChat.getFinalfromwxid();
|
||||
// }
|
||||
// String result = "";
|
||||
// String puTong = getSetting(key_caiDan_user);
|
||||
// String chaoJi = getSetting(key_caiDan_admin);
|
||||
// if (isSuperAdminUser(wxid)) {
|
||||
// result = "用户菜单:" + puTong + " 管理员菜单:" + chaoJi;
|
||||
// } else {
|
||||
// result = "用户菜单:" + puTong;
|
||||
// }
|
||||
//
|
||||
// wxUtil.sendTextMessage(wxid, result, msgType, wxMessageDataForChat.getFromwxid());
|
||||
//}
|
||||
// wxMessageDataForChatService.save(wxMessageDataForChat);
|
||||
if (msg.startsWith("单")) {
|
||||
logger.info("消息以单开头,处理单指令消息");
|
||||
jdUtils.sendOrderToWxByOrderD(msg.replace("单", ""), fromWxid);
|
||||
return;
|
||||
}
|
||||
if (msg.contains("<sourcedisplayname>唐门云课</sourcedisplayname>")){
|
||||
logger.info("消息来自唐门云课,处理指令消息" );
|
||||
otherUtil.tmyk(msg,fromWxid);
|
||||
}
|
||||
logger.info("未命中前置指令,开始命中 Default 流程");
|
||||
jdUtils.sendOrderToWxByOrderDefault(msg, fromWxid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param wxMessage
|
||||
* @return
|
||||
* @throws
|
||||
* @description 处理群聊消息
|
||||
*/
|
||||
// private void handleGroupMessage (WxMessage wxMessage){
|
||||
// Integer msgType = 2;
|
||||
// /**
|
||||
// * 接收到消息 : WxMessage(event=10009, wxid=wxid_kr145nk7l0an31, data={"type":"D0003","des":"收到消息","data":{"timeStamp":"1703128368100","fromType":1,"msgT两次ype":1,"msgSource":0,"fromWxid":"wxid_ytpc72mdoskt22","finalFromWxid":"","atWxidList":[],"silence":0,"membercount":0,"signature":"v1_vXrWK/iB","msg":"嗨鲁个迷紫123","msgBase64":"5Zeo6bKB5Liq6L+357SrMTIz"},"timestamp":"1703128368112","wxid":"wxid_kr145nk7l0an31","port":16888,"pid":10468,"flag":"7777"})
|
||||
// * 需要get 两次 data 字段*/
|
||||
// JSONObject data = wxMessage.getData().getJSONObject("data");
|
||||
// if (data == null) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /**{"type":"D0003","des":"收到消息","data":{"timeStamp":"1702957325031","fromType":1,"msgType":1,"msgSource":0,"fromWxid":"wxid_ytpc72mdoskt22","finalFromWxid":"","atWxidList":[],"silence":0,"membercount":0,"signature":"v1_OJXJYpvM","msg":"在不","msgBase64":"5Zyo5LiN"},"timestamp":"1702957325041","wxid":"wxid_kr145nk7l0an31","port":16888,"pid":10468,"flag":"7777"}
|
||||
// * */
|
||||
// WxMessageDataForChat wxMessageDataForChat = data.to(WxMessageDataForChat.class);
|
||||
//
|
||||
// // 做业务处理
|
||||
// logger.info("处理消息: {}", wxMessageDataForChat.toString());
|
||||
//
|
||||
// /**
|
||||
// * timeStamp 收到这条消息的13位现行时间戳
|
||||
// * fromType 来源类型:1|私聊 2|群聊 3|公众号
|
||||
// * msgType 消息类型:1|文本 3|图片 34|语音 42|名片 43|视频 47|动态表情 48|地理位置 49|分享链接或附件 2001|红包 2002|小程序 2003|群邀请 10000|系统消息
|
||||
// * msgSource 消息来源:0|别人发送 1|自己手机发送
|
||||
// * fromWxid fromType=1时为好友wxid,fromType=2时为群wxid,fromType=3时公众号wxid
|
||||
// * finalFromWxid 仅fromType=2时有效,为群内发言人wxid
|
||||
// * atWxidList 仅fromType=2,且msgSource=0时有效,为消息中艾特人wxid列表
|
||||
// * silence 仅fromType=2时有效,0
|
||||
// * membercount 仅fromType=2时有效,群成员数量
|
||||
// * signature 消息签名
|
||||
// * msg 消息内容
|
||||
// * msgBase64 消息内容的Base64
|
||||
// * */
|
||||
// if (Util.isAnyEmpty(wxMessageDataForChat.getMsg(), wxMessageDataForChat.getFromwxid(), wxMessageDataForChat.getFromtype())) {
|
||||
// logger.info("消息内容为空,不处理");
|
||||
// return;
|
||||
// }
|
||||
// String atwxidlist = wxMessageDataForChat.getAtwxidlist();
|
||||
// if (Util.isNotEmpty((atwxidlist))) {
|
||||
// JSONObject wxList = wxUtil.getWxList();
|
||||
// JSONObject wxBotInfo = (JSONObject) wxList.getJSONArray("result").get(0);
|
||||
// String botWxid = wxBotInfo.getString("wxid");
|
||||
//
|
||||
// if (atwxidlist.contains(botWxid)) {
|
||||
// String[] split = wxMessageDataForChat.getMsg().split(" ");
|
||||
// String msg;
|
||||
// if (split.length == 2) {
|
||||
// msg = split[1];
|
||||
// } else {
|
||||
// String[] newArray = new String[split.length - 1];
|
||||
// System.arraycopy(split, 1, newArray, 0, newArray.length);
|
||||
// StringBuilder stringBuilder = new StringBuilder();
|
||||
// Iterator<String> iterator = Arrays.stream(newArray).iterator();
|
||||
// while (iterator.hasNext()) {
|
||||
// String s = iterator.next();
|
||||
// stringBuilder.append(s).append(" ");
|
||||
// }
|
||||
// msg = stringBuilder.toString();
|
||||
// }
|
||||
//
|
||||
////美团 20-7 + https://i.meituan.com/mttouch/page/account?userId=3822095266&token=AgHdIkm2tAGHc9SQSiG7M8xCx1LbTue9D2HPOAun2eYl3ou7BeEw1uGrGZH-DxmEiUgsbA1v9SM4DQAAAAC6HAAAz0rTXmkB_CIHin08hCu68mFv5k6nUc2q6_CfZqEdBcngRK_xD8Sx5fE4rfdq-yAJ, msgbase64=576O5ZuiIDIwLTcgKyBodHRwczovL2kubWVpdHVhbi5jb20vbXR0b3VjaC9wYWdlL2FjY291bnQ/dXNlcklkPTM4MjIwOTUyNjYmdG9rZW49QWdIZElrbTJ0QUdIYzlTUVNpRzdNOHhDeDFMYlR1ZTlEMkhQT0F1bjJlWWwzb3U3QmVFdzF1R3JHWkgtRHhtRWlVZ3NiQTF2OVNNNERRQUFBQUM2SEFBQXowclRYbWtCX0NJSGluMDhoQ3U2OG1GdjVrNm5VYzJxNl9DZlpxRWRCY25nUktfeEQ4U3g1ZkU0cmZkcS15QUo=
|
||||
// if (msg.startsWith("美团 20-7 ")) {
|
||||
// logger.info("处理美团的消息");
|
||||
// msg = msg.substring(msg.indexOf("https://i.meituan.com/mttouch/page/account"));
|
||||
// String[] all = msg.split("\\?");
|
||||
//
|
||||
// if (all.length == 2) {
|
||||
// String wxid = null;
|
||||
// if (wxMessageDataForChat.getFromtype() == 1) {
|
||||
// wxid = wxMessageDataForChat.getFromwxid();
|
||||
// } else if (wxMessageDataForChat.getFromtype() == 2) {
|
||||
// wxid = wxMessageDataForChat.getFinalfromwxid();
|
||||
// }
|
||||
// String httpData = all[1];
|
||||
// String[] httpDataArr = httpData.split("&");
|
||||
// if (httpDataArr.length == 2) {
|
||||
// String result = mt20(wxMessageDataForChat.getFinalfromwxid(), httpDataArr[0].split("=")[1], httpDataArr[1].split("=")[1]);
|
||||
//
|
||||
// wxUtil.sendTextMessage(wxMessageDataForChat.getFinalfromwxid(), result, msgType, wxMessageDataForChat.getFromwxid());
|
||||
// } else {
|
||||
//
|
||||
// wxUtil.sendTextMessage(wxMessageDataForChat.getFinalfromwxid(), "请检查提交的数据格式是否正确。", msgType, wxMessageDataForChat.getFromwxid());
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// } else if ("[转账待你接收,可在手机上查看]".equals(msg)) {
|
||||
//
|
||||
// wxUtil.sendTextMessage(wxMessageDataForChat.getFinalfromwxid(), "暂不支持群内转账功能,请私聊进行转账充值。", msgType, wxMessageDataForChat.getFromwxid());
|
||||
// } else if ("余额".equals(msg)) {
|
||||
// String wxid = null;
|
||||
// if (wxMessageDataForChat.getFromtype() == 1) {
|
||||
// wxid = wxMessageDataForChat.getFromwxid();
|
||||
// } else if (wxMessageDataForChat.getFromtype() == 2) {
|
||||
// wxid = wxMessageDataForChat.getFinalfromwxid();
|
||||
// }
|
||||
// WxUser wxUser = wxUserService.getOne(Wrappers.query(new WxUser()).eq("wxid", wxid));
|
||||
// String result = "";
|
||||
// if (Util.isNotEmpty(wxUser)) {
|
||||
// result = "您的余额为:" + wxUser.getMoneyLeiji() + "元\r";
|
||||
// result = result + " 您的消费次数为:" + wxUser.getCountXiaofei() + "次\r";
|
||||
// result = result + " 您的充值次数为:" + wxUser.getCountChongzhi() + "次\r";
|
||||
// result = result + " 您的累计充值为:" + wxUser.getMoneyLeiji() + "元";
|
||||
// } else {
|
||||
// result = "暂未查询到充值记录。\r";
|
||||
// }
|
||||
//
|
||||
//
|
||||
// wxUtil.sendTextMessage(wxid, result, msgType, wxMessageDataForChat.getFromwxid());
|
||||
// } else if ("体验".equals(msg)) {
|
||||
// String wxid = null;
|
||||
// if (wxMessageDataForChat.getFromtype() == 1) {
|
||||
// wxid = wxMessageDataForChat.getFromwxid();
|
||||
// } else if (wxMessageDataForChat.getFromtype() == 2) {
|
||||
// wxid = wxMessageDataForChat.getFinalfromwxid();
|
||||
// }
|
||||
// String result = "";
|
||||
// if (heiMingDan(wxid)) {
|
||||
// result = "黑名单!";
|
||||
// } else {
|
||||
// WxUser wxUser = wxUserService.getOne(Wrappers.query(new WxUser()).eq("wxid", wxid));
|
||||
// if (isNew(wxUser.getIsnew())) {
|
||||
// wxUser.setMoneyLeiji(wxUser.getMoneyLeiji().add(new BigDecimal(1)));
|
||||
// wxUser.setMoneyShengyu(wxUser.getMoneyShengyu().add(new BigDecimal(1)));
|
||||
// wxUser.setCountChongzhi(wxUser.getCountChongzhi().add(BigDecimal.ONE));
|
||||
// wxUser.setIsnew(1);
|
||||
// wxUserService.updateById(wxUser);
|
||||
// result = "体验成功,您已成功充值" + 1.00 + "元,已成功存入账户。感谢您的使用。";
|
||||
// } else {
|
||||
// result = "您已体验过,请勿重复体验。";
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// wxUtil.sendTextMessage(wxid, result, msgType, wxMessageDataForChat.getFromwxid());
|
||||
//
|
||||
// } else if (msg.startsWith("S")) {
|
||||
// logger.info("处理超级管理员的消息");
|
||||
// String wxid = null;
|
||||
// if (wxMessageDataForChat.getFromtype() == 1) {
|
||||
// wxid = wxMessageDataForChat.getFromwxid();
|
||||
// } else if (wxMessageDataForChat.getFromtype() == 2) {
|
||||
// wxid = wxMessageDataForChat.getFinalfromwxid();
|
||||
// }
|
||||
// if (!isSuperAdminUser(wxid)) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// String result = "";
|
||||
// if (heiMingDan(wxid)) {
|
||||
// result = "黑名单!";
|
||||
// } else {
|
||||
// String[] split1 = msg.split("\\+");
|
||||
// String superAdminOrder = split1[1];
|
||||
// if ("设置普通菜单".equals(superAdminOrder)) {
|
||||
// if (split1.length == 3) {
|
||||
// String value = split1[2];
|
||||
// Setting setting = settingService.getOne(new QueryWrapper<Setting>().eq("setting_key", key_caiDan_admin));
|
||||
// setting.setSettingValue(value);
|
||||
// settingService.saveOrUpdate(setting);
|
||||
// result = "设置成功!";
|
||||
// } else {
|
||||
// result = "设置失败!";
|
||||
// }
|
||||
// } else if ("设置超级菜单".equals(superAdminOrder)) {
|
||||
// if (split1.length == 3) {
|
||||
// String value = split1[2];
|
||||
// Setting setting = settingService.getOne(new QueryWrapper<Setting>().eq("setting_key", key_caiDan_admin));
|
||||
// setting.setSettingValue(value);
|
||||
// settingService.saveOrUpdate(setting);
|
||||
// result = "设置成功!";
|
||||
// } else {
|
||||
// result = "设置失败!";
|
||||
// }
|
||||
// } else if ("查询管理员".equals(superAdminOrder)) {
|
||||
// result = getSetting(order_admin);
|
||||
// } else if ("添加管理员".equals(superAdminOrder)) {
|
||||
// if (split1.length == 3) {
|
||||
// String value = split1[2];
|
||||
// Setting setting = settingService.getOne(new QueryWrapper<Setting>().eq("setting_key", order_admin));
|
||||
// setting.setSettingValue(value.concat(",").concat(setting.getSettingValue()));
|
||||
// settingService.saveOrUpdate(setting);
|
||||
// result = "设置成功!";
|
||||
// } else {
|
||||
// result = "设置失败!";
|
||||
// }
|
||||
// } else if ("删除管理员".equals(superAdminOrder)) {
|
||||
// if (split1.length == 3) {
|
||||
// String value = split1[2];
|
||||
// Setting setting = settingService.getOne(new QueryWrapper<Setting>().eq("setting_key", order_admin));
|
||||
// setting.setSettingValue(setting.getSettingValue().replace(value, ""));
|
||||
// settingService.saveOrUpdate(setting);
|
||||
// result = "设置成功!";
|
||||
// } else {
|
||||
// result = "设置失败!";
|
||||
// }
|
||||
// }
|
||||
// //
|
||||
// //if ()
|
||||
//
|
||||
// wxUtil.sendTextMessage(wxid, result, msgType, wxMessageDataForChat.getFromwxid());
|
||||
// }
|
||||
// }
|
||||
// // 管理员返回两种菜单
|
||||
// else if (order_caiDan.equals(msg)) {
|
||||
// String wxid = null;
|
||||
// if (wxMessageDataForChat.getFromtype() == 1) {
|
||||
// wxid = wxMessageDataForChat.getFromwxid();
|
||||
// } else if (wxMessageDataForChat.getFromtype() == 2) {
|
||||
// wxid = wxMessageDataForChat.getFinalfromwxid();
|
||||
// }
|
||||
// String result = "";
|
||||
// String puTong = getSetting(key_caiDan_user);
|
||||
// String chaoJi = getSetting(key_caiDan_admin);
|
||||
// if (isSuperAdminUser(wxid)) {
|
||||
// result = "用户菜单:" + puTong + " 管理员菜单:" + chaoJi;
|
||||
// } else {
|
||||
// result = "用户菜单:" + puTong;
|
||||
// }
|
||||
//
|
||||
// wxUtil.sendTextMessage(wxid, result, msgType, wxMessageDataForChat.getFromwxid());
|
||||
// }
|
||||
// }
|
||||
// wxMessageDataForChatService.save(wxMessageDataForChat);
|
||||
//
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// private String getSetting(String key) {
|
||||
// if (Util.isNotEmpty(key)) {
|
||||
// Setting value = settingRepository.getOne(new QueryWrapper<Setting>().eq("setting_key", key));
|
||||
// return value.getSettingValue();
|
||||
// } else {
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
//private boolean isSuperAdminUser(String wxid) {
|
||||
// boolean flag = false;
|
||||
// Setting setting = settingRepository.getOne(new QueryWrapper<Setting>().eq("setting_key", "管理员"));
|
||||
// if (Util.isNotEmpty(setting)) {
|
||||
// if (setting.getSettingValue().contains(wxid)) {
|
||||
// flag = true;
|
||||
// }
|
||||
// }
|
||||
// return flag;
|
||||
//}
|
||||
|
||||
/**
|
||||
* @param userId
|
||||
* @param token
|
||||
* @return
|
||||
* @throws
|
||||
* @description
|
||||
*/
|
||||
//private String mt20(String wxid, String userId, String token) {
|
||||
// /**
|
||||
// * 1 查询用户余额
|
||||
// * 2 调用青龙的添加环境变量
|
||||
// * 3 执行美团领券
|
||||
// * 4 删除环境变量
|
||||
// * 5 改写返回的消息内容返回给用户
|
||||
// * */
|
||||
// logger.info("查询用户余额");
|
||||
// HashMap<String, Object> checkYuE = checkYuE(wxid);
|
||||
// Boolean isRun = (Boolean) checkYuE.get("isRun");
|
||||
// String info = (String) checkYuE.get("info");
|
||||
// BigDecimal yuE = (BigDecimal) checkYuE.get("yuE");
|
||||
// //isRun = true;
|
||||
//
|
||||
// // 余额可以支持一次扣费
|
||||
// if (isRun) {
|
||||
// // 调用青龙 成功
|
||||
// return runQL(token, wxid, 1);
|
||||
//
|
||||
// } else {
|
||||
// // 调用青龙 失败
|
||||
// logger.info("余额不支持一次扣费");
|
||||
// return info;
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
/**
|
||||
* @param wxid
|
||||
* @return
|
||||
* @throws
|
||||
* @description 根据 wxid 查询余额
|
||||
*/
|
||||
//private HashMap<String, Object> checkYuE(String wxid) {
|
||||
//
|
||||
// HashMap<String, Object> result = new HashMap<>();
|
||||
// BigDecimal yuE = BigDecimal.ZERO;
|
||||
// String info = "";
|
||||
// Boolean isRun = false;
|
||||
// WxUser wxUser = wxUserRepository.getOne(Wrappers.query(new WxUser()).eq("wxid", wxid));
|
||||
// if (Util.isEmpty(wxUser)) {
|
||||
// info = "未进行过充值,请先充值后使用。";
|
||||
// } else {
|
||||
// // 如果余额小于等于零
|
||||
// if (wxUser.getMoneyShengyu().compareTo(BigDecimal.ZERO) <= 0) {
|
||||
// info = "账户余额不足,请先充值后使用。";
|
||||
// }
|
||||
//
|
||||
// if (wxUser.getMoneyShengyu().compareTo(priceOfMT20) < 0) {
|
||||
// info = "剩余余额不足以支持本次扣费,请先充值后使用。";
|
||||
// } else {
|
||||
// isRun = true;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 返回结果
|
||||
// result.put("yuE", yuE);
|
||||
// result.put("info", info);
|
||||
// result.put("isRun", isRun);
|
||||
// return result;
|
||||
//}
|
||||
|
||||
/**
|
||||
* @param wxid
|
||||
* @param time 调用次数,后期可以改成包月还是一次 ,目前都是 1
|
||||
* @param token
|
||||
* @return
|
||||
* @throws
|
||||
* @description
|
||||
*/
|
||||
private String runQL(String token, String wxid, Integer time) {
|
||||
|
||||
/**
|
||||
* 1. 在系统设置 -> 应用设置 -> 添加应用,权限目前支持5个模块,可以选择多个模块。选择一个模块之后,可读写此模块的所有接口。
|
||||
* 2. 使用生成的 client_id 和 client_secret 请求获取token接口 http://localhost:5700/open/auth/token?client_id=xxxxxx&client_secret=xxxxxxxx
|
||||
* 3. 上面接口返回的token有效期为30天,可用于请求青龙的接口 curl 'http://localhost:5700/open/envs?searchValue=&t=1630032278171' -H 'Authorization: Bearer
|
||||
* 接口返回的token'
|
||||
* 4. openapi的接口与系统正常接口的区别就是青龙里的是/api/envs,openapi是/open/envs,即就是青龙接口中的api换成open
|
||||
* */
|
||||
|
||||
//String responseStr = HttpRequest.post(QL_BASE_URL + getToken())
|
||||
// .body(JSON.toJSONString(jsonMap))//头信息,多个头信息多次调用此方法即可
|
||||
// .execute().body();
|
||||
|
||||
/*
|
||||
* 1.查询有没有这个环境变量
|
||||
* 2.没有就创建
|
||||
* 3.有就修改 环境变量名 需要 wxid + 手机号
|
||||
* 4.执行crons
|
||||
* 5.如果是一次性的,time = 1 ,就删除环境变量(考虑改成禁用)
|
||||
* 6.拿到青龙的结果
|
||||
* */
|
||||
//new QLUtil().getEnv(remark)
|
||||
|
||||
|
||||
HashMap<String, String> loginedUserInfo = qlUtil.getLoginedUserInfo(token);
|
||||
if (Util.isNotEmpty(loginedUserInfo)) {
|
||||
String mobile = loginedUserInfo.get("mobile");
|
||||
String nickName = loginedUserInfo.get("nickName");
|
||||
String remark = wxid + "+" + mobile + "+" + nickName;
|
||||
JSONArray env = qlUtil.getEnv(token);
|
||||
logger.info("1 查询环境变量 env = " + env);
|
||||
// 第一次用 token 查询
|
||||
if (env.size() == 0) {
|
||||
env = qlUtil.getEnv(wxid + "+" + mobile);
|
||||
}
|
||||
// 第二次用 wxid + mobile 查询,如果不存在就直接创建
|
||||
if (env.size() == 0) {
|
||||
Boolean addEnv = qlUtil.addEnv(token, meituanCookie, remark);
|
||||
logger.info("2 使用token查询不存在环境变量,向青龙添加变量 addEnv = " + addEnv);
|
||||
} else {
|
||||
// 如果存在则说明 需要更新token
|
||||
logger.info("3 环境变量已存在{}", env);
|
||||
}
|
||||
// 这时候已经有了环境变量,可以执行crons
|
||||
qlUtil.getCron("美团");
|
||||
//logger.info("查询crons cron = " + cron);
|
||||
|
||||
|
||||
return "runQL 调用成功";
|
||||
private void handleGroupMessage(WxMessage wxMessage) {
|
||||
logger.info("处理群聊消息: {}", JSON.toJSONString(wxMessage));
|
||||
|
||||
WxMessage.DataSection data = wxMessage.getData();
|
||||
WxMessage.DataSection.InnerData innerData = data.getData();
|
||||
if (Util.isAnyEmpty(innerData.getMsg(), innerData.getFromWxid())) {
|
||||
logger.info("消息内容为空,不处理");
|
||||
return;
|
||||
} else {
|
||||
logger.info("消息内容:{}", innerData.getMsg());
|
||||
}
|
||||
|
||||
return "获取用户信息失败";
|
||||
String fromWxid = innerData.getFromWxid();
|
||||
String msg = innerData.getMsg();
|
||||
|
||||
if (chatRoom_xb.containsKey(fromWxid)) {
|
||||
logger.info("线报群消息");
|
||||
jdUtils.xb(msg, fromWxid);
|
||||
return;
|
||||
}
|
||||
// 录单群
|
||||
if ((chatRoom_JD_Order.contains(fromWxid))){
|
||||
if (msg.startsWith("单") || msg.startsWith("慢单") || msg.startsWith("录单") || msg.startsWith("TF") || msg.startsWith("H") || msg.startsWith("慢搜") || msg.startsWith("慢查")) {
|
||||
//logger.info("录单");
|
||||
jdUtils.manman(msg, fromWxid);
|
||||
return;
|
||||
}
|
||||
if (msg.startsWith("生")){
|
||||
logger.info("消息以生开头,处理单指令消息");
|
||||
jdUtils.sendOrderToWxByOrderD(msg.replace("生", ""), fromWxid);
|
||||
}
|
||||
}
|
||||
// 超级管理员内部群
|
||||
//if (chatRoom_admin_inner.contains(fromWxid)) {
|
||||
// if (msg.startsWith("表")) {
|
||||
// logger.info("消息以表开头,处理单指令消息");
|
||||
// jdUtils.sendOrderToWxByOrderD(msg.replace("表", ""), fromWxid);
|
||||
// return;
|
||||
// }
|
||||
//}
|
||||
// 可以对外的群聊
|
||||
if (!chatRoom_admin.contains(fromWxid) && !chatRoom_admin_pl.contains(fromWxid)) {
|
||||
logger.info("不是白名单群聊,不处理");
|
||||
return;
|
||||
}
|
||||
|
||||
if (chatRoom_admin_pl.contains(fromWxid)) {
|
||||
logger.info("处理评价指令消息 {}" ,fromWxid);
|
||||
jdUtils.sendOrderToWxByOrderP(msg.trim(), fromWxid);
|
||||
}else {
|
||||
logger.info("未命中前置指令,开始命中 Default 流程");
|
||||
jdUtils.sendOrderToWxByOrderDefault(msg, fromWxid);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
133
src/main/java/cn/van/business/util/WxtsUtil.java
Normal file
133
src/main/java/cn/van/business/util/WxtsUtil.java
Normal file
@@ -0,0 +1,133 @@
|
||||
package cn.van.business.util;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2025/1/22 10:20
|
||||
* @description: 企业微信推送工具类
|
||||
*/
|
||||
@Component
|
||||
public class WxtsUtil {
|
||||
private static final Logger logger = LoggerFactory.getLogger(WxtsUtil.class);
|
||||
public static final String TOKEN = "super_token_b62190c26";
|
||||
private static final String SERVER_URL = "https://wxts.van333.cn";
|
||||
|
||||
|
||||
public void sendNotify(String content) {
|
||||
try {
|
||||
String url = SERVER_URL + "/wx/send/jd";
|
||||
HashMap<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("title", "JD机器人微信推送");
|
||||
content = content.replaceAll("\\n", "<br>");
|
||||
String common = "192.168.8.88 (微信机器人), 信息 : ";
|
||||
content = common + content + "<br><br>";
|
||||
paramMap.put("text", content);
|
||||
HttpRequest.post(url).header("vanToken", TOKEN).header("source", "XZJ_UBUNTU").body(JSON.toJSONString(paramMap)).execute();
|
||||
//logger.info("企业微信推送结果:{}", execute);
|
||||
} catch (Exception e) {
|
||||
logger.error("企业微信推送失败:{}", e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 添加分级告警方法
|
||||
public void sendCriticalAlert(String title, String content) {
|
||||
String formattedMsg = String.format("[CRITICAL] %s\n%s", title, content);
|
||||
// 这里调用实际的通知渠道,例如:
|
||||
// - 发送邮件
|
||||
// - 调用企业微信机器人
|
||||
// - 触发短信通知
|
||||
sendNotify(formattedMsg); // 复用原有通知方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送微信文本消息到wxts接口
|
||||
* @param wxid 接收者微信ID
|
||||
* @param content 消息内容
|
||||
* @param msgType 消息类型
|
||||
* @param fromWxid 发送者微信ID
|
||||
* @param hiddenTime 是否隐藏时间戳
|
||||
*/
|
||||
public void sendWxTextMessage(String wxid, String content, Integer msgType, String fromWxid, Boolean hiddenTime) {
|
||||
sendWxTextMessage(wxid, content, msgType, fromWxid, hiddenTime, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送微信文本消息到wxts接口(带接收人参数)
|
||||
* @param wxid 接收者微信ID
|
||||
* @param content 消息内容
|
||||
* @param msgType 消息类型
|
||||
* @param fromWxid 发送者微信ID
|
||||
* @param hiddenTime 是否隐藏时间戳
|
||||
* @param touser 接收人列表(企业微信用户ID,多个用逗号分隔)
|
||||
*/
|
||||
public void sendWxTextMessage(String wxid, String content, Integer msgType, String fromWxid, Boolean hiddenTime, String touser) {
|
||||
try {
|
||||
String url = SERVER_URL + "/wx/send/jd";
|
||||
HashMap<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("text", content);
|
||||
|
||||
// 如果提供了接收人列表,添加到参数中
|
||||
if (touser != null && !touser.trim().isEmpty()) {
|
||||
paramMap.put("touser", touser.trim());
|
||||
logger.info("企业微信推送设置接收人 - 接收人: {}", touser);
|
||||
}
|
||||
|
||||
HttpResponse execute = HttpRequest.post(url)
|
||||
.header("vanToken", TOKEN)
|
||||
.header("source", "XZJ_UBUNTU")
|
||||
.body(JSON.toJSONString(paramMap))
|
||||
.execute();
|
||||
|
||||
if (execute.getStatus() == 200) {
|
||||
logger.info("微信文本消息发送成功:wxid={}, content={}, touser={}", wxid, content, touser);
|
||||
} else {
|
||||
logger.error("微信文本消息发送失败:status={}, response={}", execute.getStatus(), execute.body());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("微信文本消息发送异常:{}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送微信图片消息到wxts接口
|
||||
* @param wxid 接收者微信ID
|
||||
* @param imagePath 图片路径
|
||||
*/
|
||||
public void sendWxImageMessage(String wxid, String imagePath) {
|
||||
try {
|
||||
String url = SERVER_URL + "/send/jd";
|
||||
HashMap<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("wxid", wxid);
|
||||
paramMap.put("imagePath", imagePath);
|
||||
|
||||
// 提取文件名
|
||||
String[] split = imagePath.split("/");
|
||||
paramMap.put("fileName", split[split.length - 1]);
|
||||
|
||||
HttpResponse execute = HttpRequest.post(url)
|
||||
.header("vanToken", TOKEN)
|
||||
.header("source", "XZJ_UBUNTU")
|
||||
.body(JSON.toJSONString(paramMap))
|
||||
.execute();
|
||||
|
||||
if (execute.getStatus() == 200) {
|
||||
logger.info("微信图片消息发送成功:wxid={}, imagePath={}", wxid, imagePath);
|
||||
} else {
|
||||
logger.error("微信图片消息发送失败:status={}, response={}", execute.getStatus(), execute.body());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("微信图片消息发送异常:{}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package cn.van.business.util.ds;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.Method;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class DeepSeekClientUtil {
|
||||
// logger
|
||||
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(DeepSeekClientUtil.class);
|
||||
|
||||
private static final String DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"; // 确认 API 地址
|
||||
private static final String DEEPSEEK_API_KEY = "sk-d99b8cc6b7414cc88a5d950a3ff7585e"; // 替换为你的密钥
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper(); // Jackson JSON 解析器
|
||||
|
||||
|
||||
/**
|
||||
* 调用 DeepSeek API 并返回第一次回复的文本内容
|
||||
*
|
||||
* @param inputText 用户输入的文本
|
||||
* @return API 返回的第一个回复内容
|
||||
* @throws IOException 如果网络请求或 JSON 解析失败
|
||||
* @throws IllegalArgumentException 如果输入为空或过长
|
||||
*/
|
||||
public String getDeepSeekResponse(String inputText) throws IOException {
|
||||
// 1. 输入校验
|
||||
if (inputText == null || inputText.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("输入文本不能为空");
|
||||
}
|
||||
if (inputText.length() > 10000) {
|
||||
throw new IllegalArgumentException("输入文本过长");
|
||||
}
|
||||
|
||||
// 2. 构建请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", "deepseek-chat");
|
||||
requestBody.put("messages", new Map[]{
|
||||
Map.of("role", "user", "content", inputText)
|
||||
});
|
||||
requestBody.put("temperature", 0.7);
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
// 3. 使用 Hutool HTTP 发送请求
|
||||
HttpRequest request = HttpRequest.of(DEEPSEEK_API_URL)
|
||||
.method(Method.POST)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + DEEPSEEK_API_KEY) // 确保格式正确
|
||||
.header("Accept", "application/json")
|
||||
.body(jsonBody);
|
||||
|
||||
logger.info("请求 DeepSeek API: URL={}, Body={}", DEEPSEEK_API_URL, jsonBody); // 调试日志
|
||||
|
||||
HttpResponse response = request.execute();
|
||||
|
||||
// 4. 检查 HTTP 状态码
|
||||
if (response.getStatus() != 200) {
|
||||
logger.error("DeepSeek API 调用失败!状态码={}, 响应={}", response.getStatus(), response.body());
|
||||
throw new IOException("API 调用失败,HTTP 状态码: " + response.getStatus());
|
||||
}
|
||||
|
||||
// 5. 解析 JSON 响应
|
||||
JsonNode rootNode = objectMapper.readTree(response.body());
|
||||
JsonNode choices = rootNode.path("choices");
|
||||
if (choices.isEmpty()) {
|
||||
throw new IOException("API 返回数据格式异常,未找到回复内容");
|
||||
}
|
||||
|
||||
return choices.get(0)
|
||||
.path("message")
|
||||
.path("content")
|
||||
.asText();
|
||||
}
|
||||
}
|
||||
94
src/main/java/cn/van/business/util/ds/GPTClientUtil.java
Normal file
94
src/main/java/cn/van/business/util/ds/GPTClientUtil.java
Normal file
@@ -0,0 +1,94 @@
|
||||
package cn.van.business.util.ds;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2025/5/8 22:19
|
||||
* @description:
|
||||
*/
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.Method;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class GPTClientUtil {
|
||||
// logger
|
||||
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(GPTClientUtil.class);
|
||||
|
||||
private static final String GPT_API_URL = "https://api.openai.com/v1/chat/completions"; // GPT API 地址
|
||||
private static final String GPT_API_KEY = "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"; // 替换为你的 GPT API 密钥
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper(); // Jackson JSON 解析器
|
||||
private static final String PROXY_HOST = "192.168.8.9"; // 本地代理地址
|
||||
private static final int PROXY_PORT = 1070; // 本地代理端口
|
||||
|
||||
/**
|
||||
* 调用 GPT API 并返回第一次回复的文本内容
|
||||
*
|
||||
* @param inputText 用户输入的文本
|
||||
* @return API 返回的第一个回复内容
|
||||
* @throws IOException 如果网络请求或 JSON 解析失败
|
||||
* @throws IllegalArgumentException 如果输入为空或过长
|
||||
*/
|
||||
public String getGPTResponse(String inputText) throws IOException {
|
||||
// 1. 输入校验
|
||||
if (inputText == null || inputText.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("输入文本不能为空");
|
||||
}
|
||||
if (inputText.length() > 10000) {
|
||||
throw new IllegalArgumentException("输入文本过长");
|
||||
}
|
||||
|
||||
// 2. 构建请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", "gpt-4o");
|
||||
requestBody.put("messages", new Map[]{
|
||||
Map.of("role", "user", "content", inputText)
|
||||
});
|
||||
requestBody.put("temperature", 0.7);
|
||||
|
||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||
|
||||
// 3. 使用 Hutool HTTP 发送请求,设置代理
|
||||
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(PROXY_HOST, PROXY_PORT));
|
||||
HttpRequest request = HttpRequest.of(GPT_API_URL)
|
||||
.method(Method.POST)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + GPT_API_KEY)
|
||||
.header("Accept", "application/json")
|
||||
.body(jsonBody)
|
||||
.setProxy(proxy)
|
||||
.timeout(30000); // 设置超时时间为30秒
|
||||
|
||||
logger.info("请求 GPT API: URL={}, Body={}", GPT_API_URL, jsonBody);
|
||||
|
||||
HttpResponse response = request.execute();
|
||||
|
||||
// 4. 检查 HTTP 状态码
|
||||
if (response.getStatus() != 200) {
|
||||
logger.error("GPT API 调用失败!状态码={}, 响应={}", response.getStatus(), response.body());
|
||||
throw new IOException("API 调用失败,HTTP 状态码: " + response.getStatus());
|
||||
}
|
||||
|
||||
// 5. 解析 JSON 响应
|
||||
JsonNode rootNode = objectMapper.readTree(response.body());
|
||||
JsonNode choices = rootNode.path("choices");
|
||||
if (choices.isEmpty()) {
|
||||
throw new IOException("API 返回数据格式异常,未找到回复内容");
|
||||
}
|
||||
|
||||
return choices.get(0)
|
||||
.path("message")
|
||||
.path("content")
|
||||
.asText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.van.business.util.jdReq;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
@Component
|
||||
public class Days0007Strategy implements OrderFetchStrategy {
|
||||
@Override
|
||||
public TimeRange calculateRange(LocalDateTime baseTime) {
|
||||
LocalDateTime end = baseTime.truncatedTo(ChronoUnit.MINUTES);
|
||||
LocalDateTime start = end.minusDays(7).truncatedTo(ChronoUnit.MINUTES);
|
||||
if (start.isAfter(end)) { // 防御性校验
|
||||
throw new IllegalArgumentException(strategyName()+"时间范围错误");
|
||||
}
|
||||
return new TimeRange(start, end);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String strategyName() {
|
||||
return "00-07天历史订单抓取策略";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean isRealTime() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.van.business.util.jdReq;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
@Component
|
||||
public class Days0714Strategy implements OrderFetchStrategy {
|
||||
@Override
|
||||
public TimeRange calculateRange(LocalDateTime baseTime) {
|
||||
LocalDateTime end = baseTime.truncatedTo(ChronoUnit.HOURS).minusDays(7);
|
||||
LocalDateTime start = end.minusDays(14).truncatedTo(ChronoUnit.HOURS);
|
||||
if (start.isAfter(end)) { // 防御性校验
|
||||
throw new IllegalArgumentException(strategyName()+"时间范围错误");
|
||||
}
|
||||
return new TimeRange(start, end);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String strategyName() {
|
||||
return "07-14天历史订单抓取策略";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean isRealTime() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.van.business.util.jdReq;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
// 在jdReq包中补充策略类
|
||||
public class Days1430Strategy implements OrderFetchStrategy {
|
||||
@Override
|
||||
public TimeRange calculateRange(LocalDateTime baseTime) {
|
||||
LocalDateTime end = baseTime.minusDays(14).truncatedTo(ChronoUnit.HOURS);
|
||||
LocalDateTime start = baseTime.minusDays(30).truncatedTo(ChronoUnit.HOURS);
|
||||
if (start.isAfter(end)) { // 防御性校验
|
||||
throw new IllegalArgumentException(strategyName()+"时间范围错误");
|
||||
}
|
||||
return new TimeRange(start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String strategyName() {
|
||||
return "14-30天历史订单抓取策略";
|
||||
}
|
||||
@Override
|
||||
public Boolean isRealTime() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.van.business.util.jdReq;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
@Component
|
||||
public class Days3090Strategy implements OrderFetchStrategy {
|
||||
@Override
|
||||
public TimeRange calculateRange(LocalDateTime baseTime) {
|
||||
LocalDateTime end = baseTime.minusMonths(1).truncatedTo(ChronoUnit.HOURS);
|
||||
LocalDateTime start = end.minusMonths(2).truncatedTo(ChronoUnit.HOURS);
|
||||
if (start.isAfter(end)) { // 防御性校验
|
||||
throw new IllegalArgumentException(strategyName()+"时间范围错误");
|
||||
}
|
||||
return new TimeRange(start, end);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String strategyName() {
|
||||
return "30-90天历史订单抓取策略";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean isRealTime() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//package cn.van.business.util.jdReq;
|
||||
//
|
||||
//import cn.van.business.repository.OrderRowRepository;
|
||||
//import com.jd.open.api.sdk.response.kplunion.UnionOpenOrderRowQueryResponse;
|
||||
//import org.springframework.beans.factory.annotation.Autowired;
|
||||
//
|
||||
//import java.time.LocalDateTime;
|
||||
//
|
||||
//public abstract class HistoricalOrderFetcher {
|
||||
// @Autowired
|
||||
// protected OrderRowRepository orderRowRepository;
|
||||
//
|
||||
// protected int fetchOrders(OrderFetchStrategy strategy) {
|
||||
// int count = 0;
|
||||
// LocalDateTime start = strategy.getStartTime();
|
||||
// LocalDateTime end = strategy.getEndTime();
|
||||
//
|
||||
// while (!start.isEqual(end)) {
|
||||
// Integer pageIndex = 1;
|
||||
// boolean hasMore;
|
||||
//
|
||||
// do {
|
||||
// UnionOpenOrderRowQueryResponse response = fetchPage(strategy, start, pageIndex);
|
||||
// hasMore = processResponse(response, strategy);
|
||||
// pageIndex++;
|
||||
// } while (hasMore);
|
||||
//
|
||||
// start = start.plusHours(1);
|
||||
// }
|
||||
// return count;
|
||||
// }
|
||||
//
|
||||
// protected abstract UnionOpenOrderRowQueryResponse fetchPage(OrderFetchStrategy strategy,
|
||||
// LocalDateTime startTime,
|
||||
// Integer pageIndex);
|
||||
//
|
||||
// private boolean processResponse(UnionOpenOrderRowQueryResponse response, OrderFetchStrategy strategy) {
|
||||
// // 统一响应处理逻辑...
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.van.business.util.jdReq;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public interface OrderFetchStrategy {
|
||||
/**
|
||||
* 计算要抓取的时间范围
|
||||
* @param baseTime 基准时间(通常用当前时间)
|
||||
* @return 包含开始时间和结束时间的值对象
|
||||
*/
|
||||
TimeRange calculateRange(LocalDateTime baseTime);
|
||||
|
||||
/**
|
||||
* 策略标识
|
||||
*/
|
||||
String strategyName();
|
||||
|
||||
Boolean isRealTime() ;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package cn.van.business.util.jdReq;
|
||||
|
||||
public class StrategyFactory {
|
||||
public static OrderFetchStrategy getStrategy(String type) {
|
||||
switch (type) {
|
||||
case "30-90": return new Days3090Strategy();
|
||||
//case "14-30": return new Days1430Strategy();
|
||||
default: throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/main/java/cn/van/business/util/jdReq/TimeRange.java
Normal file
14
src/main/java/cn/van/business/util/jdReq/TimeRange.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package cn.van.business.util.jdReq;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
// 时间范围值对象
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class TimeRange {
|
||||
private LocalDateTime start;
|
||||
private LocalDateTime end;
|
||||
}
|
||||
17
src/main/java/cn/van/business/util/test.java
Normal file
17
src/main/java/cn/van/business/util/test.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package cn.van.business.util;
|
||||
|
||||
/**
|
||||
* @author Leo
|
||||
* @version 1.0
|
||||
* @create 2025/5/17 00:05
|
||||
* @description:
|
||||
*/
|
||||
public class test {
|
||||
public static class Main {
|
||||
public static void main(String[] args) {
|
||||
String str = "wxid_cfmrk2upjtf322";
|
||||
int length = str.length();
|
||||
System.out.println("字符串长度是: " + length); // 输出:15
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,28 +1,24 @@
|
||||
server:
|
||||
port: 6666
|
||||
spring:
|
||||
main:
|
||||
allow-circular-references: true
|
||||
application:
|
||||
name: wxSend
|
||||
name: jd
|
||||
#数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://192.168.8.88:3306/jd?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT%2B8
|
||||
url: jdbc:mysql://134.175.126.60:33306/jd?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: root
|
||||
password: mysql_7sjTXH
|
||||
#redis配置
|
||||
redis:
|
||||
host: 192.168.8.88
|
||||
port: 6379
|
||||
database: 7
|
||||
timeout: 1800000
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
#最大阻塞等待时间(负数表示没限制)
|
||||
max-wait: -1
|
||||
max-idle: 5
|
||||
min-idle: 0
|
||||
password: redis_6PZ52S # 文件上传
|
||||
hikari:
|
||||
maximum-pool-size: 2000 # 最大连接数
|
||||
minimum-idle: 48 # 最小空闲连接数
|
||||
idle-timeout: 30000 # 空闲连接超时时间(毫秒)
|
||||
max-lifetime: 2000000 # 最大生命周期(毫秒)
|
||||
connection-timeout: 30000 # 连接超时时间(毫秒)
|
||||
pool-name: SpringBootHikariCP # 连接池名字
|
||||
|
||||
servlet:
|
||||
multipart:
|
||||
# 单个文件大小
|
||||
@@ -40,6 +36,13 @@ spring:
|
||||
messages:
|
||||
# 国际化资源文件路径
|
||||
basename: i18n/messages
|
||||
data:
|
||||
redis:
|
||||
host: 134.175.126.60
|
||||
password: redis_6PZ52S
|
||||
timeout: 1800000
|
||||
port: 36379
|
||||
database: 7
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
@@ -47,5 +50,18 @@ logging:
|
||||
cn.van: debug
|
||||
org.springframework: warn
|
||||
config:
|
||||
WX_BASE_URL: http://192.168.8.208:7777/qianxun/httpapi?wxid=wxid_kr145nk7l0an31
|
||||
WX_BASE_URL: http://192.168.8.6:7777/qianxun/httpapi?wxid=wxid_kr145nk7l0an31
|
||||
QL_BASE_URL: http://134.175.126.60:35700
|
||||
rocketmq:
|
||||
name-server: 192.168.8.88:39876 # RocketMQ Name Server 地址
|
||||
producer:
|
||||
group: wx_producer # 生产者组名
|
||||
send-msg-timeout: 1000 # 发送消息超时时间
|
||||
consumer:
|
||||
group: wx_consumer # 消费者组名
|
||||
consume-thread-min: 20 # 消费线程池最小线程数
|
||||
consume-thread-max: 64 # 消费线程池最大线程数
|
||||
consume-message-batch-max-size: 64 # 批量消费最大消息数
|
||||
isRunning:
|
||||
wx: false
|
||||
jd: false
|
||||
|
||||
67
src/main/resources/application-prod.yml
Normal file
67
src/main/resources/application-prod.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
server:
|
||||
port: 6666
|
||||
spring:
|
||||
main:
|
||||
allow-circular-references: true
|
||||
application:
|
||||
name: jd
|
||||
#数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://192.168.8.88:3306/jd?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: root
|
||||
password: mysql_7sjTXH
|
||||
hikari:
|
||||
maximum-pool-size: 2000 # 最大连接数
|
||||
minimum-idle: 48 # 最小空闲连接数
|
||||
idle-timeout: 30000 # 空闲连接超时时间(毫秒)
|
||||
max-lifetime: 2000000 # 最大生命周期(毫秒)
|
||||
connection-timeout: 30000 # 连接超时时间(毫秒)
|
||||
pool-name: SpringBootHikariCP # 连接池名字
|
||||
|
||||
servlet:
|
||||
multipart:
|
||||
# 单个文件大小
|
||||
max-file-size: 20MB
|
||||
# 设置总上传的文件大小
|
||||
max-request-size: 20MB
|
||||
#MyWebMvcConfig中开启@EnableWebMvc则失效
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
# # 对象字段为null不显示
|
||||
# default-property-inclusion: non_null
|
||||
|
||||
# 资源信息
|
||||
messages:
|
||||
# 国际化资源文件路径
|
||||
basename: i18n/messages
|
||||
data:
|
||||
redis:
|
||||
host: 192.168.8.88
|
||||
password: redis_6PZ52S
|
||||
timeout: 1800000
|
||||
port: 6379
|
||||
database: 7
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
cn.van: debug
|
||||
org.springframework: warn
|
||||
config:
|
||||
WX_BASE_URL: http://192.168.8.6:7777/qianxun/httpapi?wxid=wxid_cfmrk2upjtf322 #wxid_kr145nk7l0an31大号
|
||||
QL_BASE_URL: http://134.175.126.60:35700
|
||||
rocketmq:
|
||||
name-server: 192.168.8.88:9876 # RocketMQ Name Server 地址
|
||||
producer:
|
||||
group: wx_producer # 生产者组名
|
||||
send-msg-timeout: 1000 # 发送消息超时时间
|
||||
consumer:
|
||||
group: wx_consumer # 消费者组名
|
||||
consume-thread-min: 20 # 消费线程池最小线程数
|
||||
consume-thread-max: 64 # 消费线程池最大线程数
|
||||
consume-message-batch-max-size: 64 # 批量消费最大消息数
|
||||
isRunning:
|
||||
wx: true
|
||||
jd: true
|
||||
@@ -1,37 +1,12 @@
|
||||
server:
|
||||
port: 6666
|
||||
spring:
|
||||
main:
|
||||
allow-circular-references: true
|
||||
application:
|
||||
name: wxSend
|
||||
name: jd
|
||||
profiles:
|
||||
active: dev
|
||||
#数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://134.175.126.60:33306/jd?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: root
|
||||
password: mysql_7sjTXH
|
||||
hikari:
|
||||
maximum-pool-size: 200 # 最大连接数
|
||||
minimum-idle: 5 # 最小空闲连接数
|
||||
idle-timeout: 30000 # 空闲连接超时时间(毫秒)
|
||||
max-lifetime: 2000000 # 最大生命周期(毫秒)
|
||||
connection-timeout: 30000 # 连接超时时间(毫秒)
|
||||
pool-name: SpringBootHikariCP # 连接池名字
|
||||
#redis配置
|
||||
redis:
|
||||
host: 134.175.126.60
|
||||
port: 36379
|
||||
database: 7
|
||||
timeout: 1800000
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 200
|
||||
#最大阻塞等待时间(负数表示没限制)
|
||||
max-wait: -1
|
||||
max-idle: 5
|
||||
min-idle: 0
|
||||
password: redis_6PZ52S # 文件上传
|
||||
servlet:
|
||||
multipart:
|
||||
# 单个文件大小
|
||||
@@ -49,6 +24,12 @@ spring:
|
||||
messages:
|
||||
# 国际化资源文件路径
|
||||
basename: i18n/messages
|
||||
task:
|
||||
execution:
|
||||
pool:
|
||||
core-size: 32
|
||||
jpa:
|
||||
show-sql: false
|
||||
|
||||
# token配置
|
||||
token:
|
||||
@@ -72,3 +53,49 @@ logging:
|
||||
level:
|
||||
cn.van333: debug
|
||||
org.springframework: warn
|
||||
org.hibernate: ERROR
|
||||
org.springframework.web: WARN
|
||||
org.apache.http: WARN
|
||||
com.zaxxer.hikari: ERROR
|
||||
rocketmq:
|
||||
name-server: 192.168.8.88:9876 # RocketMQ Name Server 地址
|
||||
producer:
|
||||
group: wx_producer # 生产者组名
|
||||
send-msg-timeout: 1000 # 发送消息超时时间
|
||||
consumer:
|
||||
group: wx_consumer # 消费者组名
|
||||
consume-thread-min: 20 # 消费线程池最小线程数
|
||||
consume-thread-max: 64 # 消费线程池最大线程数
|
||||
consume-message-batch-max-size: 64 # 批量消费最大消息数
|
||||
client:
|
||||
charset: UTF-8
|
||||
encoding: UTF-8
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,metrics,resilience4j
|
||||
prometheus:
|
||||
metrics:
|
||||
export:
|
||||
enabled: true
|
||||
|
||||
resilience4j.ratelimiter:
|
||||
instances:
|
||||
wxMsgLimiter:
|
||||
limitForPeriod: 10 # 根据业务吞吐量调整
|
||||
limitRefreshPeriod: 1s # 固定1秒周期
|
||||
timeoutDuration: 0 # 立即失败模式
|
||||
registerHealthIndicator: true
|
||||
|
||||
# 图片转换配置
|
||||
image:
|
||||
convert:
|
||||
# 图片存储路径(转换后的jpg图片存储目录)
|
||||
storage-path: ${user.home}/comment-images
|
||||
# 图片访问基础URL(如果配置,转换后的图片将通过此URL访问)
|
||||
# 例如: http://your-domain.com/images 或 http://localhost:6666/images
|
||||
# 如果为空,则返回本地文件路径
|
||||
# 建议配置为:http://your-domain:6666/images (使用ImageController提供HTTP访问)
|
||||
base-url:
|
||||
|
||||
|
||||
29
src/main/resources/logback-spring.xml
Normal file
29
src/main/resources/logback-spring.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 文件输出,按日期滚动 -->
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>logs/app.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 按天分割日志文件 -->
|
||||
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<!-- 保留最近7天的日志 -->
|
||||
<maxHistory>15</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 设置根日志级别为 DEBUG,并同时输出到控制台和文件 -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<appender-ref ref="FILE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user