Compare commits

...

731 Commits

Author SHA1 Message Date
Leo
f89ed66bcc 1 2025-12-08 15:27:52 +08:00
Leo
a893f3cd61 1 2025-12-05 23:02:02 +08:00
Leo
c2be15e3f5 1 2025-12-05 23:01:41 +08:00
Leo
8445b500ae 1 2025-12-05 22:35:51 +08:00
Leo
ee67d1ae8f Merge branch 'master' of https://git.van333.cn/CC/Jarvis_java 2025-12-04 15:47:42 +08:00
Leo
a20e92d7bf 1 2025-12-04 15:47:39 +08:00
69d1d91f5e 1 2025-12-04 14:48:18 +08:00
Leo
570fcb0b93 1 2025-11-29 23:39:37 +08:00
Leo
7fda3da9ed 1 2025-11-29 22:47:41 +08:00
Leo
e7687c8909 1 2025-11-29 22:35:06 +08:00
Leo
8e12076225 1 2025-11-10 21:49:45 +08:00
Leo
5b48727fb2 1 2025-11-10 18:43:27 +08:00
Leo
bb6c907cda 1 2025-11-10 18:43:18 +08:00
Leo
bdd33581f1 1 2025-11-09 00:00:41 +08:00
Leo
ef358cc6b3 1 2025-11-08 15:25:43 +08:00
Leo
e76c6d4451 羽绒服 2025-11-08 02:18:35 +08:00
31ecfa6a2f 1 2025-11-03 19:49:12 +08:00
127a5b71c6 1 2025-11-03 19:44:22 +08:00
1872908dae 1 2025-11-03 16:03:34 +08:00
efdb727e48 1 2025-11-03 15:46:21 +08:00
4af64b58d6 1 2025-11-03 15:38:07 +08:00
424cf37260 1 2025-11-03 15:30:33 +08:00
47fd91b948 1 2025-11-03 15:29:21 +08:00
89b37907e7 1 2025-11-03 15:26:52 +08:00
6b36f0ee52 1 2025-11-03 11:54:11 +08:00
5f75603532 1 2025-10-31 22:25:09 +08:00
a2d011fb01 1 2025-10-31 22:04:01 +08:00
1a6ddce3f0 1 2025-10-31 16:58:23 +08:00
a82ff0d39f 1 2025-10-28 00:33:26 +08:00
1c9c9cfa06 1 2025-10-28 00:18:31 +08:00
8905ce179c 1 2025-10-22 19:58:26 +08:00
2114f5d0f6 1 2025-10-08 17:16:25 +08:00
4010910846 1 2025-10-08 17:12:25 +08:00
bd9b0f9384 1 2025-10-03 11:53:31 +08:00
雷欧(林平凡)
b9de9ed7f4 1 2025-09-25 16:06:37 +08:00
雷欧(林平凡)
fd940bbd66 1 2025-09-25 16:03:14 +08:00
雷欧(林平凡)
473e305bb7 1 2025-09-09 11:39:09 +08:00
雷欧(林平凡)
c1fbe3bb4b 1 2025-09-09 11:16:52 +08:00
雷欧(林平凡)
41083d4519 1 2025-09-09 10:45:04 +08:00
595642677f 1 2025-09-07 17:35:35 +08:00
d74af8a07f 1 2025-09-07 17:25:16 +08:00
6e68591991 1 2025-09-06 16:17:37 +08:00
雷欧(林平凡)
5543b5bcde 1 2025-09-04 18:00:43 +08:00
c3a23bf6fa 1 2025-09-02 19:17:21 +08:00
b7528dc077 1 2025-08-31 15:25:36 +08:00
3883cd76b4 1 2025-08-31 15:08:06 +08:00
49320e35ed 1 2025-08-28 01:15:34 +08:00
a95956a73e 1 2025-08-28 01:09:36 +08:00
b983219502 1 2025-08-28 01:07:09 +08:00
cbf1600497 1 2025-08-28 01:03:53 +08:00
雷欧(林平凡)
cce7ffad00 1 2025-08-22 11:47:45 +08:00
雷欧(林平凡)
841c4a6a5a Merge remote-tracking branch '群晖/master' 2025-08-22 11:47:17 +08:00
雷欧(林平凡)
e736ad9a96 1 2025-08-22 11:45:59 +08:00
d582014f24 1 2025-08-21 19:55:14 +08:00
1cd75c8384 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/cn/van/business/util/JDProductService.java
2025-08-21 19:48:50 +08:00
c80d3b05b8 1 2025-08-21 19:45:37 +08:00
bdba500fa2 1 2025-08-21 19:40:41 +08:00
雷欧(林平凡)
7eb31a1d4a 1 2025-08-21 16:49:44 +08:00
雷欧(林平凡)
4e024460b4 1 2025-08-20 22:50:03 +08:00
雷欧(林平凡)
81b39804e1 1 2025-08-20 19:59:42 +08:00
雷欧(林平凡)
b28af91409 1 2025-08-20 19:47:49 +08:00
雷欧(林平凡)
b035649467 1 2025-08-18 16:09:28 +08:00
雷欧(林平凡)
a005158d97 1 2025-08-18 11:47:20 +08:00
81cc5bec82 1 2025-08-17 22:46:04 +08:00
54df8a4bc8 1 2025-08-17 22:27:46 +08:00
e3f0160876 1 2025-08-17 22:18:16 +08:00
3b561b0fc7 1 2025-08-17 21:52:18 +08:00
雷欧(林平凡)
488eb5f64c 1 2025-08-15 17:39:31 +08:00
雷欧(林平凡)
26c2b51d72 1 2025-08-15 16:48:43 +08:00
雷欧(林平凡)
37fc0697b3 1 2025-08-15 14:53:32 +08:00
雷欧(林平凡)
f97a5b0130 1 2025-08-15 10:52:28 +08:00
雷欧(林平凡)
7408206f77 1 2025-08-14 18:03:37 +08:00
雷欧(林平凡)
11da6257d6 1 2025-08-14 18:02:31 +08:00
雷欧(林平凡)
1a762eb50a 1 2025-08-11 18:01:32 +08:00
雷欧(林平凡)
cdb871a202 1 2025-08-08 17:18:00 +08:00
雷欧(林平凡)
09741e3b48 1 2025-08-08 16:56:29 +08:00
雷欧(林平凡)
0ba97c39a2 1 2025-08-08 14:46:59 +08:00
雷欧(林平凡)
2f0157e03e 1 2025-08-08 14:42:23 +08:00
雷欧(林平凡)
b4c27a26d7 1 2025-08-08 14:36:29 +08:00
雷欧(林平凡)
aabbb3cc17 1 2025-08-08 14:26:54 +08:00
雷欧(林平凡)
8def7a1cce 1 2025-08-08 14:08:05 +08:00
雷欧(林平凡)
6cf6e95751 1 2025-08-08 13:54:21 +08:00
雷欧(林平凡)
90a8871d36 1 2025-08-08 13:40:26 +08:00
雷欧(林平凡)
34583fce59 1 2025-08-08 11:37:21 +08:00
雷欧(林平凡)
60f8c51f44 1 2025-08-08 11:30:40 +08:00
雷欧(林平凡)
4708323384 1 2025-08-08 10:48:16 +08:00
雷欧(林平凡)
d7c08ce505 1 2025-08-07 16:01:15 +08:00
雷欧(林平凡)
af56d7626c 1 2025-08-07 15:54:16 +08:00
雷欧(林平凡)
0f6ffe06ae 1 2025-08-07 15:46:19 +08:00
雷欧(林平凡)
e1414f5e20 1 2025-08-07 15:41:17 +08:00
雷欧(林平凡)
b99c167db2 1 2025-08-07 15:37:02 +08:00
雷欧(林平凡)
d93575aacb 1 2025-08-07 15:32:12 +08:00
雷欧(林平凡)
0ae4bd345f 1 2025-08-07 14:13:12 +08:00
雷欧(林平凡)
eb070d37e9 Merge remote-tracking branch '群晖/master' 2025-08-06 11:06:42 +08:00
雷欧(林平凡)
8cc8ff5c6d 1 2025-08-06 11:06:12 +08:00
Van0313
6d2db74edc 1 2025-08-05 20:23:30 +08:00
雷欧(林平凡)
fc711b57fc 1 2025-07-30 16:54:03 +08:00
雷欧(林平凡)
d9858ded89 1 2025-07-30 16:43:33 +08:00
雷欧(林平凡)
b5975e101c 1 2025-07-30 15:38:27 +08:00
雷欧(林平凡)
30b9a936f2 1 2025-07-29 17:21:00 +08:00
雷欧(林平凡)
0a3f91493b 1 2025-07-29 17:17:55 +08:00
雷欧(林平凡)
1fc912fa2c Merge remote-tracking branch '群晖/master' 2025-07-29 17:14:23 +08:00
雷欧(林平凡)
79e7e8db2a 1 2025-07-29 17:14:09 +08:00
雷欧(林平凡)
55b7490fa0 Merge remote-tracking branch '群晖/master' 2025-07-29 17:11:34 +08:00
雷欧(林平凡)
77be6551e4 1 2025-07-29 17:10:58 +08:00
雷欧(林平凡)
4db21ffd8d 1 2025-07-29 17:06:31 +08:00
雷欧(林平凡)
60777dda32 1 2025-07-29 17:03:26 +08:00
雷欧(林平凡)
a85024cfad 1 2025-07-29 15:44:54 +08:00
Van0313
a573d71a4f 未完成版本 2025-07-29 15:27:54 +08:00
雷欧(林平凡)
91fcbabfd1 Merge remote-tracking branch '群晖/master' 2025-07-25 11:03:43 +08:00
雷欧(林平凡)
7c42536413 1 2025-07-25 11:03:30 +08:00
Van0313
d0cef50b7c 1 2025-07-24 04:17:19 +08:00
雷欧(林平凡)
12ec14179c 不是已完成,不是违规的才发送 2025-07-21 16:47:48 +08:00
雷欧(林平凡)
d876892cb0 Merge remote-tracking branch '群晖/master'
# Conflicts:
#	src/main/java/cn/van/business/util/JDUtil.java
2025-07-21 16:43:12 +08:00
Van0313
6e385259c0 移除鉴权 2025-07-13 19:13:19 +08:00
Van0313
fda52b2aeb 评论 2025-07-13 13:18:20 +08:00
Van0313
3de6193702 评论 2025-07-13 13:18:09 +08:00
Van0313
55596ae9e5 评论 2025-07-13 13:14:05 +08:00
Van0313
f13e6119d3 评论 2025-07-13 13:13:31 +08:00
Van0313
259607949c 评论 2025-07-13 13:11:08 +08:00
Van0313
d6969e6f6f 评论 2025-07-13 13:07:43 +08:00
Van0313
c68a01e47b 评论 2025-07-13 13:03:52 +08:00
Van0313
af08e1b5fb 评论 2025-07-13 13:01:10 +08:00
Van0313
9b457d25eb 评论 2025-07-09 00:25:53 +08:00
Van0313
58a1b30de9 评论 2025-07-08 21:23:11 +08:00
Van0313
2fb1a45e5e 评论 2025-07-06 11:40:24 +08:00
Van0313
ec3ebdd5d4 评论 2025-07-06 11:39:42 +08:00
Van0313
bf585d7cd1 评论 2025-07-06 11:35:20 +08:00
Van0313
47a77eff47 评论 2025-07-06 11:25:56 +08:00
Van0313
cdaff20462 录单 2025-07-05 22:48:48 +08:00
Van0313
b86b7e44dc 录单 2025-07-05 22:45:29 +08:00
Van0313
dbf4b34072 录单 2025-07-05 22:21:11 +08:00
Van0313
c4799ea7c6 录单 2025-07-05 21:29:52 +08:00
Van0313
543f85d9a5 录单 2025-07-05 21:15:53 +08:00
雷欧(林平凡)
c60ea50c2e Merge remote-tracking branch '群晖/master' 2025-06-25 14:16:12 +08:00
Van0313
61fcedf5d7 录单 2025-06-25 00:49:31 +08:00
Van0313
c0a86f6a6f 录单 2025-06-25 00:46:02 +08:00
Van0313
96e053dd48 录单 2025-06-25 00:36:45 +08:00
雷欧(林平凡)
bb33fe065f 录单 2025-06-23 11:00:06 +08:00
雷欧(林平凡)
be0ae71154 录单 2025-06-20 17:34:35 +08:00
雷欧(林平凡)
13c9d0350c 录单 2025-06-19 15:45:54 +08:00
Van0313
245f90901c 录单 2025-06-19 01:40:29 +08:00
Van0313
d4c8054c94 录单 2025-06-19 01:30:17 +08:00
Van0313
1e035a7b41 录单 2025-06-19 01:21:45 +08:00
Van0313
3744177e16 录单 2025-06-19 01:12:34 +08:00
Van0313
960b68113d 录单 2025-06-19 01:09:59 +08:00
Van0313
3f2f88422e 录单 2025-06-19 01:06:09 +08:00
Van0313
7e6dd11969 录单 2025-06-19 00:55:36 +08:00
Van0313
417edf1f72 录单 2025-06-19 00:41:31 +08:00
Van0313
712c6f84ee 录单 2025-06-19 00:37:36 +08:00
Van0313
19369ba2c2 录单 2025-06-19 00:29:02 +08:00
Van0313
90eafb4385 录单 2025-06-19 00:20:40 +08:00
Van0313
7b3c24bfac 录单 2025-06-19 00:19:25 +08:00
Van0313
ede5ed8d28 录单 2025-06-19 00:15:42 +08:00
Van0313
5c512a01c3 录单 2025-06-19 00:13:31 +08:00
Van0313
6105e963fa 录单 2025-06-19 00:11:59 +08:00
Van0313
97e62eda54 录单 2025-06-18 19:10:00 +08:00
雷欧(林平凡)
d33549529e Merge remote-tracking branch '群晖/master' 2025-06-18 17:08:03 +08:00
雷欧(林平凡)
4fd197451f 录单 2025-06-18 17:07:41 +08:00
Van0313
b03f1d33fe 录单 2025-06-17 23:31:53 +08:00
Van0313
2ebcc4915d 录单 2025-06-17 22:04:31 +08:00
雷欧(林平凡)
e0f9952773 鉴权 2025-06-17 18:50:02 +08:00
雷欧(林平凡)
faab92e9b5 鉴权 2025-06-16 17:25:54 +08:00
雷欧(林平凡)
d733f47e92 鉴权 2025-06-16 17:20:05 +08:00
雷欧(林平凡)
9da48ac245 鉴权 2025-06-16 17:09:17 +08:00
雷欧(林平凡)
959abb6845 鉴权 2025-06-16 16:52:57 +08:00
雷欧(林平凡)
b8ba94612b 鉴权 2025-06-16 16:48:51 +08:00
雷欧(林平凡)
2e4128ab40 鉴权 2025-06-16 16:36:25 +08:00
雷欧(林平凡)
98204bb9f9 接入 2025-06-16 09:41:43 +08:00
雷欧(林平凡)
8e8726804b 礼金过期 2025-06-13 15:27:09 +08:00
雷欧(林平凡)
8635c05565 礼金过期 2025-06-13 15:17:47 +08:00
雷欧(林平凡)
0a503e04fc 录单 2025-06-12 14:12:05 +08:00
雷欧(林平凡)
1a3da997a1 录单 2025-06-12 14:11:36 +08:00
雷欧(林平凡)
b77713adc9 录单 2025-06-12 09:47:03 +08:00
雷欧(林平凡)
09294be57e 录单 2025-06-11 18:06:38 +08:00
雷欧(林平凡)
71c64420ef 录单 2025-06-11 17:50:40 +08:00
雷欧(林平凡)
78f3c72e78 录单 2025-06-11 17:45:59 +08:00
雷欧(林平凡)
efb9b41a57 录单 2025-06-11 17:40:56 +08:00
雷欧(林平凡)
4f97e84f18 录单 2025-06-11 15:12:14 +08:00
雷欧(林平凡)
1579e570eb 录单 2025-06-11 15:04:44 +08:00
雷欧(林平凡)
e2e139ae7e 录单 2025-06-11 11:31:26 +08:00
雷欧(林平凡)
e47f24d3ba 录单 2025-06-11 11:26:32 +08:00
雷欧(林平凡)
f674eaa9de 录单 2025-06-11 11:25:33 +08:00
雷欧(林平凡)
08ee1606c2 录单 2025-06-11 10:14:50 +08:00
雷欧(林平凡)
b1229340bb 录单 2025-06-11 10:02:25 +08:00
Van0313
031789acfb 录单 2025-06-10 23:59:15 +08:00
Van0313
ab62f8714b 录单 2025-06-10 23:54:46 +08:00
Van0313
7a91d5aba1 录单 2025-06-10 23:08:06 +08:00
Van0313
2371274929 录单 2025-06-08 21:32:04 +08:00
Van0313
d101ea3c6a 录单 2025-06-08 20:04:13 +08:00
Van0313
122e2390b9 录单 2025-06-08 20:01:57 +08:00
Van0313
913f0168ec 录单 2025-06-08 19:52:45 +08:00
Van0313
1847e25d28 录单 2025-06-08 19:52:27 +08:00
Van0313
eabbf1b591 录单 2025-06-08 19:47:18 +08:00
Van0313
5c8998fb3a 录单 2025-06-08 19:44:02 +08:00
Van0313
eadfdb02a8 录单 2025-06-08 19:40:15 +08:00
Van0313
e91a336d35 录单 2025-06-08 19:32:31 +08:00
Van0313
80c396cd81 录单 2025-06-08 19:26:17 +08:00
Van0313
9b97816127 录单 2025-06-08 19:23:11 +08:00
Van0313
65f726e33d 录单 2025-06-08 19:21:19 +08:00
Van0313
650c030b13 录单 2025-06-08 18:42:09 +08:00
Van0313
786b636e3b 录单 2025-06-08 17:31:53 +08:00
Van0313
c331511fef 录单 2025-06-08 17:12:27 +08:00
Van0313
110236df55 录单 2025-06-08 15:31:00 +08:00
Van0313
592b7d34f9 录单 2025-06-08 15:30:07 +08:00
Van0313
51c4c93b03 录单 2025-06-06 00:26:22 +08:00
Van0313
c46d050356 录单 2025-06-06 00:12:56 +08:00
Van0313
f15d480d37 录单 2025-06-06 00:04:58 +08:00
Van0313
fb1b3d40de Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/cn/van/business/util/JDUtil.java
2025-06-06 00:01:25 +08:00
Van0313
53e23dc29c 录单 2025-06-05 23:57:18 +08:00
雷欧(林平凡)
3d3cb5d208 精简统计 2025-06-04 15:40:35 +08:00
雷欧(林平凡)
b2fce8c93d 精简统计 2025-06-04 15:10:44 +08:00
雷欧(林平凡)
ef0b10ee78 精简统计 2025-06-04 14:37:32 +08:00
雷欧(林平凡)
b04acda600 精简统计 2025-06-04 14:36:50 +08:00
雷欧(林平凡)
95c466dbc3 精简统计 2025-06-04 14:33:46 +08:00
雷欧(林平凡)
6d22c906ec 精简统计 2025-06-04 14:28:38 +08:00
雷欧(林平凡)
5b4e861f74 日志 2025-06-04 14:03:29 +08:00
雷欧(林平凡)
e3c368fd3a 日志 2025-06-04 13:59:44 +08:00
雷欧(林平凡)
c4ef932dba Merge remote-tracking branch '群晖/master' 2025-06-04 13:37:09 +08:00
雷欧(林平凡)
b44b719cff 日志 2025-06-04 13:37:03 +08:00
Van0313
a1ad742d96 Merge remote-tracking branch 'origin/master' 2025-06-03 22:16:31 +08:00
Van0313
383af37178 录单 2025-06-03 22:16:04 +08:00
雷欧(林平凡)
9b8617ed79 日志 2025-06-03 14:44:01 +08:00
雷欧(林平凡)
0e37c73fb2 录单 2025-06-03 11:46:56 +08:00
雷欧(林平凡)
ebe9abd880 录单 2025-06-03 11:33:31 +08:00
Van0313
99d46acaa2 录单 2025-06-02 23:29:47 +08:00
Van0313
4931842ee6 录单 2025-06-02 22:09:49 +08:00
Van0313
893402279e 录单 2025-06-02 21:50:27 +08:00
Van0313
f340c94bd6 录单 2025-06-02 00:16:26 +08:00
Van0313
794f797a20 录单 2025-06-02 00:08:33 +08:00
Van0313
24c196c8a3 自动抓评论 2025-06-01 17:11:14 +08:00
Van0313
f4d72b4335 录单 2025-06-01 01:09:29 +08:00
Van0313
f33d04ca9a 1 2025-05-31 18:16:15 +08:00
Van0313
a46dd6b864 1 2025-05-31 00:41:09 +08:00
雷欧(林平凡)
6bc5635f35 录单 2025-05-30 16:16:37 +08:00
雷欧(林平凡)
91f17c017d 录单 2025-05-30 13:57:16 +08:00
雷欧(林平凡)
9ffc04d496 录单 2025-05-30 11:38:14 +08:00
雷欧(林平凡)
949bc163af 价保 2025-05-30 10:21:52 +08:00
雷欧(林平凡)
8bf257ffa3 Merge remote-tracking branch '群晖/master' 2025-05-30 10:06:46 +08:00
雷欧(林平凡)
3fdb909d09 价保 2025-05-30 10:06:40 +08:00
Van0313
6401776841 1 2025-05-29 20:01:08 +08:00
Van0313
a54a9d1a5d 1 2025-05-29 19:41:33 +08:00
雷欧(林平凡)
84d0dd328a 价保 2025-05-29 18:06:53 +08:00
雷欧(林平凡)
e216f2f686 价保 2025-05-29 17:13:59 +08:00
雷欧(林平凡)
5a02bdaedd 录单+价保 2025-05-29 17:05:47 +08:00
雷欧(林平凡)
74e42a3d6f 录单+价保 2025-05-29 16:54:45 +08:00
雷欧(林平凡)
cdc81cd91c 录单+价保 2025-05-29 16:50:47 +08:00
雷欧(林平凡)
632ebef136 录单 2025-05-29 16:21:14 +08:00
雷欧(林平凡)
9521e6472a 录单 2025-05-29 16:06:37 +08:00
雷欧(林平凡)
af302a5c85 录单 2025-05-29 15:37:51 +08:00
雷欧(林平凡)
0be7367afe 京粉 2025-05-29 15:35:37 +08:00
雷欧(林平凡)
7459a58f91 京粉 2025-05-29 15:34:46 +08:00
雷欧(林平凡)
7c5cc24b32 京粉 2025-05-29 14:59:20 +08:00
Van0313
fc98a45f93 1 2025-05-28 22:56:06 +08:00
Van0313
26f745fad1 1 2025-05-28 21:30:57 +08:00
雷欧(林平凡)
6c5ef2d462 京粉 2025-05-28 16:02:31 +08:00
雷欧(林平凡)
76a1715fde 京粉 2025-05-28 16:01:13 +08:00
Leo
ba6135c2ac 排序 2025-05-25 03:54:51 +08:00
雷欧(林平凡)
5ee53a5d00 京粉 2025-05-23 15:59:00 +08:00
Van0313
20c87f5f51 录单 2025-05-22 23:43:31 +08:00
Van0313
a3f9695f68 价保 2025-05-19 17:26:06 +08:00
Van0313
43f098479c 价保 2025-05-18 01:52:34 +08:00
Van0313
86523b37b7 价保 2025-05-17 20:43:30 +08:00
Van0313
e4fccb5820 价保 2025-05-17 20:42:59 +08:00
Van0313
2347a46453 价保 2025-05-17 16:10:07 +08:00
Van0313
38bab8e1b0 价保 2025-05-17 16:06:33 +08:00
Van0313
93c11ebbdb 价保 2025-05-17 00:33:47 +08:00
Van0313
fc666e9c0f 价保 2025-05-17 00:24:50 +08:00
Van0313
ea210ff341 价保 2025-05-17 00:19:49 +08:00
Van0313
622b8b1a9a 价保 2025-05-17 00:07:42 +08:00
Van0313
d54bf5affb 价保 2025-05-16 23:43:57 +08:00
Van0313
b6562ba7ec 价保 2025-05-16 23:28:01 +08:00
Van0313
e1a2d59748 价保 2025-05-16 23:23:48 +08:00
Van0313
bcb8404450 重构评论 2025-05-16 17:02:47 +08:00
雷欧(林平凡)
ab87a57b62 京粉 2025-05-15 16:39:35 +08:00
Van0313
69db3d68d5 重构评论 2025-05-14 12:56:24 +08:00
Van0313
a25e78715f 重构评论 2025-05-14 12:06:46 +08:00
Van0313
624a38317c 重构评论 2025-05-13 13:02:54 +08:00
雷欧(林平凡)
6b2a0bab5d 京粉备注 2025-05-12 16:26:53 +08:00
雷欧(林平凡)
50f825644e 京粉备注 2025-05-12 16:26:26 +08:00
雷欧(林平凡)
c93da08c82 京粉备注 2025-05-12 16:02:58 +08:00
雷欧(林平凡)
781fc6cb5d 京粉备注 2025-05-12 15:51:30 +08:00
雷欧(林平凡)
0a98360989 京粉备注 2025-05-12 15:45:43 +08:00
雷欧(林平凡)
364241f419 京粉备注 2025-05-12 15:42:09 +08:00
Van0313
5c110d9ce3 重构评论 2025-05-11 18:25:07 +08:00
Van0313
9250ed919d 重构评论 2025-05-10 17:58:31 +08:00
Van0313
100462f3bb 重构评论 2025-05-10 17:45:51 +08:00
雷欧(林平凡)
29ef2d2968 京粉备注 2025-05-09 09:47:45 +08:00
Van0313
358258ab66 重构评论 2025-05-09 00:32:19 +08:00
Van0313
ab91c3e68d 重构评论 2025-05-08 23:53:08 +08:00
Van0313
33f71e742b 重构评论 2025-05-08 22:24:28 +08:00
雷欧(林平凡)
531a9df7f1 京粉备注 2025-05-08 10:03:02 +08:00
雷欧(林平凡)
a40cb9a8f0 京粉备注 2025-05-08 09:54:28 +08:00
Van0313
e62e6aa3fc 重构评论 2025-05-06 14:25:31 +08:00
Van0313
978b19d110 重构评论 2025-05-06 14:05:57 +08:00
Van0313
82a4474d63 重构评论 2025-05-06 12:05:06 +08:00
Van0313
fedd739018 重构评论 2025-05-06 12:04:30 +08:00
Van0313
f84910a678 重构评论 2025-05-06 11:56:27 +08:00
Van0313
0d10b3583e 重构评论 2025-05-06 11:50:40 +08:00
Van0313
e74eeba527 重构评论 2025-05-04 21:52:34 +08:00
Van0313
d24ed29d47 重构评论 2025-05-04 09:51:03 +08:00
Van0313
3850715413 重构评论 2025-05-04 09:45:07 +08:00
Van0313
e51e6da212 重构评论 2025-05-03 16:27:03 +08:00
Van0313
b243a1d5c8 重构评论 2025-05-03 16:08:42 +08:00
Van0313
ba46d413f6 重构评论 2025-05-03 14:52:58 +08:00
Van0313
a394689e9f 重构评论 2025-05-03 14:26:32 +08:00
Van0313
8c807c816d 重构评论 2025-05-02 21:58:56 +08:00
Van0313
628b58f9b5 重构评论 2025-05-02 21:10:57 +08:00
Van0313
41d2f8b84b 重构评论 2025-05-02 21:03:55 +08:00
Van0313
58c0d5884c 重构评论 2025-05-02 21:03:25 +08:00
Van0313
dba9b08561 重构评论 2025-05-02 20:56:35 +08:00
Van0313
6f9057ce56 重构评论 2025-05-02 20:49:00 +08:00
Van0313
a9f247bf62 重构评论 2025-05-02 20:39:46 +08:00
Van0313
5d89d84aa5 超时退出 2025-05-02 10:08:06 +08:00
Van0313
6907836bad 超时退出 2025-05-02 09:32:30 +08:00
Van0313
f3a198f977 超时退出 2025-05-02 09:24:42 +08:00
Van0313
3995ba4ee0 超时退出 2025-05-01 20:35:56 +08:00
Van0313
b5207fee2a 超时退出 2025-04-30 11:05:53 +08:00
Van0313
334e59ab52 超时退出 2025-04-30 10:40:38 +08:00
Van0313
cfb3991c26 超时退出 2025-04-29 22:21:55 +08:00
Van0313
5159e5fd91 超时退出 2025-04-29 10:47:37 +08:00
Van0313
55588b4d30 超时退出 2025-04-29 10:44:44 +08:00
Van0313
b43a7109d1 超时退出 2025-04-29 10:30:22 +08:00
Van0313
abfbfec790 超时退出 2025-04-29 10:21:55 +08:00
Van0313
f379fd65a8 超时退出 2025-04-29 01:12:10 +08:00
Van0313
b804172a0d 评论+ds 2025-04-29 01:05:09 +08:00
Van0313
00408f7634 评论+ds 2025-04-29 00:54:11 +08:00
Van0313
55ec60cdce 评论+ds 2025-04-28 18:40:40 +08:00
Van0313
7240f4f6a4 评论+ds 2025-04-28 17:56:54 +08:00
Van0313
ff07eda25b 评论+ds 2025-04-28 17:50:03 +08:00
Van0313
8b7f5bf676 评论+ds 2025-04-28 17:46:51 +08:00
Van0313
7885196aa6 评论+ds 2025-04-28 17:39:46 +08:00
Van0313
aa98138760 评论+ds 2025-04-28 17:24:11 +08:00
Van0313
80504627a8 评论+ds 2025-04-28 17:14:16 +08:00
Van0313
e9eb1d18ac 评论+ds 2025-04-28 17:09:46 +08:00
Van0313
c2dd8dd3c0 评论+ds 2025-04-28 16:58:54 +08:00
Van0313
deb797952f 评论+ds 2025-04-28 16:53:21 +08:00
Van0313
d02d4ed915 评论+ds 2025-04-28 16:48:00 +08:00
Van0313
76d65c0cff 评论+ds 2025-04-28 16:35:08 +08:00
Van0313
5dcae40907 评论+ds 2025-04-28 16:28:31 +08:00
Van0313
42f65bafd1 bug fix 2025-04-28 15:49:21 +08:00
Van0313
e673f39958 bug fix 2025-04-27 21:44:25 +08:00
Van0313
e599e00bb7 bug fix 2025-04-27 21:35:47 +08:00
Van0313
4151ba2f3c bug fix 2025-04-27 17:05:25 +08:00
Van0313
d55eacd0b1 bug fix 2025-04-27 16:58:00 +08:00
Van0313
c6f64218db bug fix 2025-04-27 16:51:45 +08:00
Van0313
f82c22e3ee bug fix 2025-04-27 16:47:06 +08:00
Van0313
d7ac62222f bug fix 2025-04-27 16:32:27 +08:00
Van0313
55ac5efbfb bug fix 2025-04-27 16:04:34 +08:00
Van0313
4091299931 bug fix 2025-04-27 16:00:40 +08:00
Van0313
3cbd0a8025 bug fix 2025-04-27 15:58:05 +08:00
Van0313
040f99d285 bug fix 2025-04-27 12:05:38 +08:00
Van0313
2fdf300849 bug fix 2025-04-26 12:33:08 +08:00
Van0313
c8e886a0f8 bug fix 2025-04-26 12:25:23 +08:00
Van0313
433da799ef bug fix 2025-04-26 12:22:16 +08:00
Van0313
7f25fc1619 bug fix 2025-04-26 12:21:44 +08:00
Van0313
a57c7780be bug fix 2025-04-26 12:18:10 +08:00
Van0313
e7f312ec50 bug fix 2025-04-26 12:11:55 +08:00
Van0313
2b15a40f75 bug fix 2025-04-26 12:11:08 +08:00
Van0313
d6110a2d4b bug fix 2025-04-25 16:13:20 +08:00
Van0313
39ea8e5859 bug fix 2025-04-23 22:13:03 +08:00
Van0313
17e592e8ed bug fix 2025-04-23 21:42:51 +08:00
Van0313
999fd713f3 bug fix 2025-04-23 21:26:06 +08:00
Van0313
35a8f79b54 bug fix 2025-04-23 21:02:27 +08:00
雷欧(林平凡)
9c5416719e 文案首行 2025-04-23 17:20:41 +08:00
雷欧(林平凡)
36ff1696ea 文案首行 2025-04-23 17:16:17 +08:00
雷欧(林平凡)
152e5f7b91 文案首行 2025-04-23 17:09:24 +08:00
Van0313
2a78a0a96c bug fix 2025-04-23 14:52:36 +08:00
Van0313
8894a62ab7 bug fix 2025-04-23 11:49:06 +08:00
Van0313
fab02dc96f 多线报来源 2025-04-23 10:50:42 +08:00
Van0313
e4fdd75e9f 多线报来源 2025-04-23 10:45:34 +08:00
雷欧(林平凡)
3143681cd7 文案首行 2025-04-22 17:47:30 +08:00
雷欧(林平凡)
f40f7c35ad 文案首行 2025-04-22 17:44:59 +08:00
雷欧(林平凡)
c084faeee4 文案首行 2025-04-22 17:26:01 +08:00
雷欧(林平凡)
8eb8eaa87a 去除限流 2025-04-22 17:16:28 +08:00
雷欧(林平凡)
ff7a70f3fc 去除限流 2025-04-22 17:13:01 +08:00
雷欧(林平凡)
afafb15d64 去除限流 2025-04-22 16:44:34 +08:00
雷欧(林平凡)
f62dbbaaa7 大号文案 2025-04-22 16:42:48 +08:00
雷欧(林平凡)
2692f9af79 大号文案 2025-04-22 16:39:49 +08:00
雷欧(林平凡)
27fc61138e 只保留开通礼金的流程 2025-04-22 11:29:09 +08:00
Van0313
e5d00e82b2 合并 订单附带京粉每日统计 2025-04-22 10:03:23 +08:00
Van0313
1d33162c1f 合并 订单附带京粉每日统计 2025-04-21 23:02:19 +08:00
Van0313
d2502a6cb6 合并 订单附带京粉每日统计 2025-04-21 19:32:35 +08:00
Van0313
9f7595e4b0 合并 订单附带京粉每日统计 2025-04-21 19:31:36 +08:00
Van0313
80050bc231 合并 订单附带京粉每日统计 2025-04-19 21:05:09 +08:00
Van0313
64ee48ab14 合并 订单附带京粉每日统计 2025-04-19 21:02:01 +08:00
Van0313
e7ef6a8e52 合并 订单附带京粉每日统计 2025-04-19 20:50:35 +08:00
Van0313
0291cee0a8 合并 订单附带京粉每日统计 2025-04-19 20:39:32 +08:00
Van0313
6733ecad88 合并 订单附带京粉每日统计 2025-04-19 20:25:36 +08:00
Van0313
1f91a349da 订单附带京粉每日统计 2025-04-17 14:36:46 +08:00
Van0313
609451a9d6 精简推送文案 2025-04-16 17:19:55 +08:00
Van0313
28de9359fa 去除采集的图片 2025-04-16 16:45:53 +08:00
Van0313
b03fc6f781 去除采集的图片 2025-04-16 16:22:00 +08:00
Van0313
7bb58e5eb1 Merge remote-tracking branch 'origin/master' 2025-04-16 16:15:11 +08:00
Van0313
ba8c1f14a5 去除采集的图片 2025-04-16 16:14:48 +08:00
Leo
614a8abb91 多链接礼金第一版 2025-04-15 22:23:32 +08:00
Leo
88b11a0d31 多链接礼金第一版 2025-04-15 22:16:52 +08:00
Leo
c465ad9faa 多链接礼金第一版 2025-04-14 21:42:31 +08:00
Leo
47d1efd4d6 多链接礼金第一版 2025-04-14 21:30:08 +08:00
Leo
f8c817fa8c 多链接礼金第一版 2025-04-14 20:24:33 +08:00
Leo
c8efd8512e 尝试精简 2025-04-12 14:28:12 +08:00
Leo
b63d50ab5f 1 2025-04-12 14:18:00 +08:00
Leo
226f594539 1 2025-04-11 22:26:01 +08:00
Leo
14a1dfd85d 加入erp 2025-04-11 20:11:18 +08:00
雷欧(林平凡)
f1c7710fb3 一个微信对多个京粉 2025-04-11 16:06:13 +08:00
雷欧(林平凡)
d88bcfbca2 一个微信对多个京粉 2025-04-11 15:54:32 +08:00
雷欧(林平凡)
2876964c64 一个微信对多个京粉 2025-04-11 15:51:42 +08:00
雷欧(林平凡)
00dc3d2fc1 1 2025-04-11 15:31:47 +08:00
雷欧(林平凡)
aaaf79aafd 1 2025-04-11 15:21:32 +08:00
雷欧(林平凡)
3f15d22d89 1 2025-04-11 14:50:30 +08:00
Leo
ec5a50ca35 加入erp 2025-04-10 19:29:47 +08:00
Leo
f8e189c5f4 2025-04-09 14:48:34 +08:00
Leo
11aa10d8db 2025-04-09 14:46:08 +08:00
Leo
e1477026a3 2025-04-08 20:28:06 +08:00
Leo
b0a85a0c4f 2025-04-08 20:24:39 +08:00
Leo
1b38b7004d 2025-04-08 20:19:14 +08:00
Leo
464af3b9e1 2025-04-08 20:15:50 +08:00
Leo
def2e1434e 2025-04-08 20:13:54 +08:00
Leo
3204de5f56 2025-04-08 20:10:00 +08:00
Leo
653102b351 2025-04-08 20:07:05 +08:00
Leo
bb044fe7fa 尝试重置 2025-04-08 20:06:29 +08:00
Leo
aa0d15f64d 尝试重置 2025-04-08 18:05:30 +08:00
Leo
3c18e42e25 尝试重置 2025-04-08 17:07:31 +08:00
Leo
bb41323253 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/cn/van/business/util/JDUtil.java
2025-04-08 16:40:30 +08:00
Leo
aa6ee5b694 尝试重置 2025-04-08 16:40:06 +08:00
Leo
8f6709e752 尝试精简 2025-04-08 16:18:14 +08:00
Leo
adde86ff06 Revert "拼多多地址"
This reverts commit 173dd0cd4c.
2025-04-08 16:11:38 +08:00
Leo
18d7e6aaf4 回滚代码 2025-04-08 16:08:49 +08:00
Leo
ef3f50259e 抽离礼金 2025-04-08 16:00:57 +08:00
Leo
9c48e02531 抽离礼金 2025-04-08 15:36:04 +08:00
Leo
02346ebff0 抽离礼金 2025-04-08 15:16:08 +08:00
Leo
173dd0cd4c 拼多多地址 2025-04-07 17:06:08 +08:00
Leo
10a1d11962 拼多多地址 2025-04-07 16:31:43 +08:00
Leo
d54c35cd2c 拼多多地址 2025-04-07 16:28:34 +08:00
Leo
aa35e8a49d 拼多多地址 2025-04-07 11:11:58 +08:00
Leo
49d27bbc10 拼多多地址 2025-04-07 10:51:31 +08:00
Leo
109f1b382d 拼多多地址 2025-04-07 10:38:02 +08:00
Leo
4e058fd816 拼多多地址 2025-04-07 10:21:46 +08:00
Leo
2e4d9d97cd 拼多多地址 2025-04-06 19:18:16 +08:00
Leo
ef8225be06 加入群聊 2025-04-06 16:02:02 +08:00
Leo
787033a129 加入群聊 2025-04-05 16:44:05 +08:00
Leo
85c8cbd238 加入群聊 2025-04-05 15:39:37 +08:00
Leo
ae46ce4a98 加入群聊 2025-04-05 15:11:37 +08:00
Leo
ee771c0343 加入群聊 2025-04-05 14:41:47 +08:00
Leo
01d7b10f5a 加入群聊 2025-04-05 14:38:09 +08:00
Leo
c7534437d8 基本完成转链。富贵指日可待! 2025-04-05 02:44:14 +08:00
Leo
6b2900e442 基本完成转链。富贵指日可待! 2025-04-05 02:33:11 +08:00
Leo
f523b057dc 基本完成转链。富贵指日可待! 2025-04-05 02:30:15 +08:00
Leo
16ae2952a4 基本完成转链。富贵指日可待! 2025-04-05 02:28:53 +08:00
Leo
fca0a1d871 基本完成转链。富贵指日可待! 2025-04-05 02:18:06 +08:00
Leo
c81d0224c8 基本完成转链。富贵指日可待! 2025-04-05 02:01:22 +08:00
Leo
e1df955d07 基本完成转链。富贵指日可待! 2025-04-05 01:49:04 +08:00
Leo
82c54f409c 基本完成转链。富贵指日可待! 2025-04-05 01:45:09 +08:00
Leo
79775dde63 重构前 2025-04-04 20:27:55 +08:00
Leo
4e1e4c77ec 1 2025-04-04 18:10:47 +08:00
Leo
066382ae6c 1 2025-04-04 18:03:35 +08:00
Leo
ddf181420b 1 2025-04-04 17:56:51 +08:00
Leo
82b6052825 1 2025-04-04 17:41:08 +08:00
Leo
d1bfd28460 1 2025-04-04 17:20:37 +08:00
Leo
3ef1b9b220 1 2025-04-04 17:03:32 +08:00
Leo
c64cd986f1 1 2025-04-04 15:40:28 +08:00
Leo
f0ab445bd8 1 2025-04-04 15:31:48 +08:00
Leo
d6165109e3 1 2025-04-04 15:15:19 +08:00
Leo
f5715aa683 1 2025-04-04 15:10:00 +08:00
Leo
2f87f4a38a 1 2025-04-04 03:04:38 +08:00
Leo
1187f8ef68 1 2025-04-04 02:45:41 +08:00
Leo
76c114b06d 1 2025-04-04 02:40:16 +08:00
Leo
f4d06f6861 1 2025-04-04 02:12:40 +08:00
Leo
c2b473f6a2 1 2025-04-04 01:56:25 +08:00
Leo
78865292a9 1 2025-04-04 01:50:27 +08:00
Leo
3be388f8e8 1 2025-04-04 01:41:58 +08:00
Leo
ca5a51d55c 1 2025-04-02 22:33:11 +08:00
Leo
2da47a1439 1 2025-04-02 22:28:25 +08:00
Leo
a4d274211b 1 2025-04-02 22:22:55 +08:00
Leo
eb6b8a4d92 1 2025-04-02 22:13:59 +08:00
Leo
370c446bfd 继续写这个接口的商品信息,够用了 2025-04-02 22:09:56 +08:00
Leo
3e614aab58 继续写这个接口的商品信息,够用了 2025-04-02 21:57:16 +08:00
Leo
642f75d034 继续写这个接口的商品信息,够用了 2025-04-02 17:05:44 +08:00
Leo
6ef1bb0412 继续写这个接口的商品信息,够用了 2025-04-02 00:34:37 +08:00
Leo
5c72c3da3e 统计美化 2025-04-01 20:33:40 +08:00
雷欧(林平凡)
a6682ad2bf 1 2025-04-01 17:58:40 +08:00
雷欧(林平凡)
fb59f1e065 1 2025-04-01 17:48:31 +08:00
雷欧(林平凡)
0e09457f0e 1 2025-04-01 17:40:20 +08:00
雷欧(林平凡)
0fdc491f9e 1 2025-04-01 17:31:29 +08:00
Leo
e9f01b7e5b 统计美化 2025-04-01 00:32:22 +08:00
雷欧(林平凡)
63aa72fea4 1 2025-03-25 14:55:06 +08:00
雷欧(林平凡)
837276845e Merge remote-tracking branch '群晖/master'
# Conflicts:
#	src/main/java/cn/van/business/util/JDUtil.java
2025-03-25 14:54:07 +08:00
雷欧(林平凡)
2ca42bd3f3 1 2025-03-25 14:53:09 +08:00
Leo
be3da7f2b1 统计美化 2025-03-20 19:35:01 +08:00
雷欧(林平凡)
c655d9154a 1 2025-03-20 19:26:24 +08:00
雷欧(林平凡)
4109622e42 1 2025-03-20 17:35:03 +08:00
雷欧(林平凡)
cbd7e7d26c 1 2025-03-20 15:29:37 +08:00
雷欧(林平凡)
118b25096b 1 2025-03-19 16:42:35 +08:00
雷欧(林平凡)
d9dba2c6ba 1 2025-03-18 09:30:48 +08:00
雷欧(林平凡)
da417b6f00 1 2025-03-17 17:29:02 +08:00
雷欧(林平凡)
be050b3922 1 2025-03-17 16:31:55 +08:00
雷欧(林平凡)
f17a4be78f 1 2025-03-17 16:25:19 +08:00
雷欧(林平凡)
fc9e345514 1 2025-03-17 16:00:56 +08:00
雷欧(林平凡)
21e5cc3dd0 1 2025-03-17 15:57:13 +08:00
雷欧(林平凡)
335c940353 1 2025-03-17 15:07:10 +08:00
雷欧(林平凡)
a3388168cf 1 2025-03-17 14:57:41 +08:00
雷欧(林平凡)
581a3a5962 1 2025-03-17 14:55:42 +08:00
雷欧(林平凡)
a5104fa6ed 菜单优化 2025-03-17 11:55:52 +08:00
雷欧(林平凡)
7408d3a1cb 菜单优化 2025-03-17 11:54:10 +08:00
雷欧(林平凡)
bb2970e05b 菜单优化 2025-03-17 09:39:58 +08:00
雷欧(林平凡)
6d38db33a7 菜单优化 2025-03-17 09:28:24 +08:00
Leo
e7f5b419c7 统计美化 2025-03-16 20:32:56 +08:00
Leo
0429b24ffc 统计美化 2025-03-16 20:25:54 +08:00
Leo
1c4259e13f 统计美化 2025-03-16 20:14:23 +08:00
Leo
6d8962e006 统计美化 2025-03-16 20:10:01 +08:00
Leo
25e8f4504c 统计美化 2025-03-16 20:04:53 +08:00
Leo
02ea973d24 统计美化 2025-03-16 20:00:02 +08:00
Leo
c53a5c6f4f 统计美化 2025-03-16 19:54:27 +08:00
Leo
df4a73cd73 统计美化 2025-03-16 19:44:07 +08:00
Leo
3e93ab2506 0007暴力拉取 2025-03-16 18:40:57 +08:00
Leo
9834dcb2b9 分开京东拉订单的调度类 2025-03-16 18:28:35 +08:00
Leo
c4aeace21a 分开京东拉订单的调度类 2025-03-16 18:25:17 +08:00
Leo
fb3b8bdf84 分开京东拉订单的调度类 2025-03-16 17:10:43 +08:00
Leo
85ba779a1c 分开京东拉订单的调度类 2025-03-16 17:09:37 +08:00
Leo
a0d185da73 抽取统计打印方法 2025-03-16 16:51:55 +08:00
Leo
caa0a24c24 抽取统计打印方法 2025-03-16 01:13:03 +08:00
Leo
5faf591987 抽取统计打印方法 2025-03-16 01:10:45 +08:00
Leo
30f5cb7a3d 抽取统计打印方法 2025-03-16 01:08:23 +08:00
Leo
d1ad973028 抽取统计打印方法 2025-03-16 01:08:05 +08:00
Leo
98dcaae37f 抽取统计打印方法 2025-03-16 01:00:06 +08:00
Leo
29c0ba7308 抽取统计打印方法 2025-03-16 00:46:06 +08:00
Leo
176b3f5034 抽取统计打印方法 2025-03-16 00:27:29 +08:00
雷欧(林平凡)
39301f49c3 菜单优化 2025-03-13 17:58:32 +08:00
Leo
8d003e5181 1 2025-03-13 17:40:41 +08:00
雷欧(林平凡)
11b453e0d6 Merge remote-tracking branch '群晖/master'
# Conflicts:
#	src/main/java/cn/van/business/util/JDUtil.java
2025-03-13 17:17:27 +08:00
雷欧(林平凡)
d2393d7fe2 菜单优化 2025-03-13 17:14:08 +08:00
Leo
800811a140 1 2025-03-12 22:28:58 +08:00
Leo
b37e3c05dd 1 2025-03-12 22:14:46 +08:00
Leo
918700fdbc 1 2025-03-12 22:07:13 +08:00
Leo
751e2c9584 1 2025-03-12 22:02:58 +08:00
Leo
946a86d48d 1 2025-03-12 19:22:51 +08:00
雷欧(林平凡)
f936baea47 菜单优化 2025-03-12 17:44:19 +08:00
雷欧(林平凡)
1edabd4eb4 菜单优化 2025-03-12 17:34:48 +08:00
雷欧(林平凡)
c60a782728 菜单优化 2025-03-12 17:32:08 +08:00
雷欧(林平凡)
a012cd99fc 菜单优化 2025-03-12 17:22:00 +08:00
雷欧(林平凡)
4964d973d0 菜单优化 2025-03-12 17:19:44 +08:00
雷欧(林平凡)
727b1a5200 菜单优化 2025-03-12 17:10:20 +08:00
雷欧(林平凡)
face5a7f32 菜单优化 2025-03-12 17:04:37 +08:00
雷欧(林平凡)
232b86d32a 菜单优化 2025-03-12 16:58:30 +08:00
雷欧(林平凡)
57873075c1 菜单优化 2025-03-12 16:54:21 +08:00
雷欧(林平凡)
e4e0e9a2f2 菜单优化 2025-03-12 16:50:03 +08:00
雷欧(林平凡)
434ed21ffc 礼金测试 2025-03-12 15:01:27 +08:00
雷欧(林平凡)
a7d06c8906 礼金测试 2025-03-12 14:51:11 +08:00
雷欧(林平凡)
d87c2597dc 测试 2025-03-12 14:50:53 +08:00
雷欧(林平凡)
9d4bc33076 jdk17 2025-03-11 16:10:59 +08:00
雷欧(林平凡)
5223239000 jdk17 2025-03-11 16:09:51 +08:00
雷欧(林平凡)
a20807c607 jdk17 2025-03-11 15:56:30 +08:00
雷欧(林平凡)
461c5346d9 jdk17 2025-03-11 15:48:22 +08:00
Leo
77df5fa16a 重构第一版,没有明显bug 2025-03-11 14:49:49 +08:00
Leo
25bca0df53 重构第一版,没有明显bug 2025-03-11 14:36:47 +08:00
Leo
b91ef6487d 重构第一版,没有明显bug 2025-03-11 14:30:49 +08:00
Leo
d721fa7e0c 重构第一版,没有明显bug 2025-03-11 14:22:32 +08:00
Leo
a47efda7b5 重构第一版,没有明显bug 2025-03-11 14:22:11 +08:00
Leo
2316951e7f 重构第一版,没有明显bug 2025-03-11 14:11:51 +08:00
Leo
9aa02430fd 重构第一版,没有明显bug 2025-03-06 10:56:35 +08:00
Leo
645b025172 稳定版。没重构之前。稳定的限流。 2025-03-04 14:29:48 +08:00
Leo
72b7a125e0 1 2025-03-02 21:43:34 +08:00
Leo
f1524159d5 1 2025-03-02 21:12:56 +08:00
Leo
31a5178d9c 1 2025-03-02 16:25:53 +08:00
Leo
d8e97e7e2b 1 2025-03-02 14:39:31 +08:00
Leo
41fae55b3f 1 2025-03-01 23:39:55 +08:00
雷欧(林平凡)
a394f7f268 1 2025-03-01 19:11:00 +08:00
雷欧(林平凡)
765abc5889 jdk17 2025-02-28 15:58:15 +08:00
雷欧(林平凡)
77a4b61c61 1 2025-02-28 14:44:52 +08:00
雷欧(林平凡)
f960fa2df6 1 2025-02-28 14:09:34 +08:00
雷欧(林平凡)
d26954a0bb 1 2025-02-28 11:37:29 +08:00
雷欧(林平凡)
44b4e5a78c 1 2025-02-26 17:53:29 +08:00
雷欧(林平凡)
532d74bcb2 1 2025-02-26 17:51:38 +08:00
雷欧(林平凡)
121b753d66 1 2025-02-26 16:23:39 +08:00
雷欧(林平凡)
ba98e1191a 1 2025-02-26 16:12:45 +08:00
雷欧(林平凡)
5bd8a6a5e2 1 2025-02-26 16:05:29 +08:00
雷欧(林平凡)
5b9d3efe32 1 2025-02-26 15:58:55 +08:00
雷欧(林平凡)
79c98932a9 1 2025-02-26 15:50:13 +08:00
雷欧(林平凡)
691d215741 1 2025-02-26 15:45:43 +08:00
雷欧(林平凡)
333551f4a0 1 2025-02-26 15:37:15 +08:00
雷欧(林平凡)
d8c3450426 1 2025-02-26 15:02:44 +08:00
Leo
5b0ea491a5 1 2025-02-18 22:33:05 +08:00
Leo
8589460fa5 1 2025-02-08 23:20:53 +08:00
Leo
8fa377fb99 1 2025-02-08 22:58:12 +08:00
Leo
fb6fa476a2 1 2025-02-04 13:39:59 +08:00
Leo
59cec4879e 1 2025-01-24 15:27:18 +08:00
Leo
d058b58cd0 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/cn/van/business/mq/MessageConsumerService.java
2025-01-24 13:03:43 +08:00
Leo
8df1fa9fc8 1 2025-01-24 13:00:57 +08:00
雷欧(林平凡)
888702297f 1 2025-01-22 16:57:39 +08:00
雷欧(林平凡)
b62ab3ea5c 1 2025-01-22 16:53:11 +08:00
雷欧(林平凡)
d95a7271c3 1 2025-01-22 16:32:00 +08:00
雷欧(林平凡)
cf28b6c8ea 1 2025-01-22 15:50:15 +08:00
雷欧(林平凡)
c9215b2e70 1 2025-01-22 15:48:54 +08:00
雷欧(林平凡)
10f3396b8c 企业微信推送 2025-01-22 13:41:52 +08:00
雷欧(林平凡)
1bb4a3be7b 企业微信推送 2025-01-22 11:49:23 +08:00
雷欧(林平凡)
13fbc16557 企业微信推送 2025-01-22 11:44:10 +08:00
雷欧(林平凡)
530f8ce0a6 企业微信推送 2025-01-22 11:35:48 +08:00
雷欧(林平凡)
7b5233a380 企业微信推送 2025-01-22 11:27:44 +08:00
雷欧(林平凡)
5b0fa54e3a jenkins 2025-01-22 11:18:30 +08:00
雷欧(林平凡)
016a99b17c jenkins 2025-01-21 17:48:51 +08:00
雷欧(林平凡)
249c486459 Revert "jenkins"
This reverts commit 9bda4419fe.
2025-01-21 17:38:32 +08:00
雷欧(林平凡)
ca0474c081 Revert "jenkins"
This reverts commit ebf51e48c4.
2025-01-21 17:38:31 +08:00
雷欧(林平凡)
ebf51e48c4 jenkins 2025-01-21 17:11:29 +08:00
雷欧(林平凡)
9bda4419fe jenkins 2025-01-21 16:39:21 +08:00
雷欧(林平凡)
d685d1c5eb jenkins 2025-01-21 16:35:09 +08:00
雷欧(林平凡)
dac626755b jenkins 2025-01-21 16:26:58 +08:00
雷欧(林平凡)
c05595a3b5 jenkins 2025-01-21 16:25:48 +08:00
雷欧(林平凡)
6e117ce2d7 jenkins 2025-01-21 15:54:42 +08:00
雷欧(林平凡)
1ad343806b jenkins 2025-01-21 15:47:23 +08:00
雷欧(林平凡)
0cd88c40e5 jenkins 2025-01-21 15:44:28 +08:00
雷欧(林平凡)
4ff7a9efe9 jenkins 2025-01-21 15:38:23 +08:00
雷欧(林平凡)
ef8cba2bba jenkins 2025-01-21 15:25:21 +08:00
雷欧(林平凡)
2e10d95ffb jenkins 2025-01-21 15:12:46 +08:00
雷欧(林平凡)
53a86ec5df pom 文件的京东jar 引入 2025-01-21 15:08:00 +08:00
雷欧(林平凡)
52ad2d4b97 pom 文件的京东jar 引入 2025-01-21 14:52:19 +08:00
雷欧(林平凡)
da9128443e pom 文件的京东jar 引入 2025-01-21 14:44:51 +08:00
cc
344f927990 1 2025-01-16 14:04:49 +08:00
cc
195b207a71 1 2025-01-16 13:40:59 +08:00
雷欧(林平凡)
6e2d5ba6fb 1 2025-01-16 13:33:05 +08:00
雷欧(林平凡)
1efc77d57c 1 2025-01-16 11:36:38 +08:00
cc
a6afb713da 1 2025-01-16 11:30:54 +08:00
雷欧(林平凡)
f2ae6014be 1 2025-01-16 10:41:28 +08:00
cc
0dacbfab24 1 2025-01-10 09:13:39 +08:00
雷欧(林平凡)
7d6dc23266 1 2025-01-10 09:10:08 +08:00
雷欧(林平凡)
cad6037169 1 2025-01-10 09:05:28 +08:00
雷欧(林平凡)
42606ce94e 1 2025-01-09 18:03:49 +08:00
cc
d155678636 1 2025-01-09 18:02:22 +08:00
cc
e9d21c41a6 1 2025-01-09 17:54:50 +08:00
cc
a31feca107 1 2025-01-09 17:38:56 +08:00
雷欧(林平凡)
5df9add885 1 2025-01-09 15:18:14 +08:00
雷欧(林平凡)
4d4a8ff36e 1 2025-01-09 15:01:42 +08:00
雷欧(林平凡)
f5f4bc281b 1 2025-01-09 14:52:17 +08:00
雷欧(林平凡)
ad2e300b3d 1 2025-01-09 14:38:17 +08:00
cc
e1d764a49e 1 2025-01-09 14:29:40 +08:00
雷欧(林平凡)
87d25d1f9b 1 2025-01-09 14:25:38 +08:00
雷欧(林平凡)
243ff323c6 1 2025-01-09 13:47:25 +08:00
雷欧(林平凡)
6b357b0727 1 2025-01-09 13:46:20 +08:00
雷欧(林平凡)
20b555ee46 1 2025-01-09 11:56:23 +08:00
雷欧(林平凡)
67aa3e47bc 1 2025-01-09 11:49:40 +08:00
cc
38ba262d9c 1 2025-01-09 11:36:16 +08:00
雷欧(林平凡)
3e617ad656 1 2025-01-09 11:22:35 +08:00
雷欧(林平凡)
e54de8e55e 1 2025-01-09 09:46:32 +08:00
雷欧(林平凡)
6cd1d60827 1 2025-01-09 09:44:47 +08:00
雷欧(林平凡)
d140d7d1ac 1 2025-01-09 09:42:22 +08:00
雷欧(林平凡)
f8a62e6c3d 1 2025-01-09 09:33:36 +08:00
雷欧(林平凡)
0f4dc7599f 1 2025-01-08 17:56:55 +08:00
雷欧(林平凡)
de98c3e1bb 1 2025-01-08 17:53:46 +08:00
雷欧(林平凡)
8dc4f2d170 1 2025-01-07 13:49:36 +08:00
雷欧(林平凡)
ed6ee47d53 1 2025-01-07 13:47:12 +08:00
雷欧(林平凡)
3eff113894 1 2025-01-07 13:44:46 +08:00
雷欧(林平凡)
9d63f6e180 1 2025-01-07 13:40:21 +08:00
雷欧(林平凡)
4fddc08818 1 2025-01-06 15:06:05 +08:00
雷欧(林平凡)
b39fe302a1 1 2024-12-19 00:25:17 +08:00
cc
9dfd4e4633 1 2024-12-15 14:44:39 +08:00
cc
553b013180 1 2024-12-14 21:15:46 +08:00
雷欧(林平凡)
d970f3f177 1 2024-12-13 09:35:50 +08:00
雷欧(林平凡)
5605dd08a5 1 2024-12-12 17:22:31 +08:00
雷欧(林平凡)
89fb711251 1 2024-12-12 17:21:44 +08:00
雷欧(林平凡)
11a13fe5ec 1 2024-12-12 17:18:41 +08:00
雷欧(林平凡)
3d454ea30f 1 2024-12-12 17:11:41 +08:00
雷欧(林平凡)
77f2ff5052 1 2024-12-12 17:09:48 +08:00
雷欧(林平凡)
7ec08b69bb 1 2024-12-12 17:05:02 +08:00
雷欧(林平凡)
ba58e77bca 1 2024-12-12 16:57:28 +08:00
雷欧(林平凡)
35d8cc7eec 1 2024-12-09 17:48:45 +08:00
cc
4ffead8554 1 2024-12-09 10:23:27 +08:00
雷欧(林平凡)
c59e7c398a 1 2024-12-06 17:06:30 +08:00
雷欧(林平凡)
442bca0b3e 1 2024-12-06 16:56:06 +08:00
雷欧(林平凡)
2c6cabba15 Merge remote-tracking branch '群晖/master' 2024-12-06 16:38:34 +08:00
雷欧(林平凡)
b6f1bd8253 1 2024-12-06 16:38:29 +08:00
cc
338414b5a6 1 2024-12-06 16:37:42 +08:00
cc
a1d5eb9db2 1 2024-12-06 16:05:29 +08:00
雷欧(林平凡)
2cedbd49f4 1 2024-12-06 16:02:54 +08:00
cc
8a65c3f21f 1 2024-12-06 14:42:42 +08:00
cc
c9c8b9850b 1 2024-12-06 11:48:50 +08:00
雷欧(林平凡)
6ecd8963b7 1 2024-12-05 16:05:15 +08:00
雷欧(林平凡)
0411cb647b 1 2024-12-05 11:47:35 +08:00
雷欧(林平凡)
2073f8c9db 1 2024-12-05 11:39:48 +08:00
雷欧(林平凡)
acc0dbace6 1 2024-12-05 11:09:02 +08:00
雷欧(林平凡)
1deb229328 1 2024-12-05 10:58:32 +08:00
雷欧(林平凡)
8fd6bad912 1 2024-12-02 16:50:25 +08:00
雷欧(林平凡)
176f202f25 1 2024-12-02 13:44:25 +08:00
雷欧(林平凡)
f99f7f67d2 1 2024-12-02 11:56:45 +08:00
雷欧(林平凡)
6cc2c72789 1 2024-12-02 11:53:54 +08:00
雷欧(林平凡)
d2f1c977b4 1 2024-12-02 11:44:58 +08:00
雷欧(林平凡)
dbbd1de26d 1 2024-12-02 11:35:32 +08:00
cc
b673fc93c7 1 2024-12-02 11:27:12 +08:00
cc
b65b53dc53 1 2024-12-02 11:14:17 +08:00
雷欧(林平凡)
4f28f15406 1 2024-12-02 11:12:05 +08:00
cc
641d52902a 1 2024-12-02 10:19:27 +08:00
cc
1d4d0eed86 1 2024-12-02 10:10:57 +08:00
雷欧(林平凡)
4a6a477c0c 1 2024-12-02 10:08:02 +08:00
雷欧(林平凡)
ae3e2149c0 1 2024-12-02 09:59:56 +08:00
cc
2d8e343e4f 1 2024-12-02 09:48:25 +08:00
cc
ffc93f26fa 1 2024-12-02 09:43:54 +08:00
雷欧(林平凡)
293cbbfd0a 1 2024-12-02 09:38:34 +08:00
Leo
a159f315a9 1 2024-12-01 15:51:14 +08:00
Leo
b73f1b0f52 1 2024-12-01 15:30:15 +08:00
Leo
eb02d7f825 1 2024-12-01 15:26:25 +08:00
Leo
4ef5ae256b 1 2024-12-01 15:22:15 +08:00
cc
6ba281b7ed 1 2024-12-01 12:46:43 +08:00
cc
9cfbf2ae2d 1 2024-12-01 02:43:21 +08:00
cc
8011eaf9fd 1 2024-12-01 02:23:42 +08:00
Leo
965b9d56d1 1 2024-12-01 02:20:20 +08:00
Leo
c798d78179 1 2024-12-01 01:52:15 +08:00
cc
05e0bdff96 1 2024-11-30 22:44:01 +08:00
cc
aad3989165 1 2024-11-29 22:32:06 +08:00
cc
3dfc6f0b3d 1 2024-11-29 14:11:49 +08:00
雷欧(林平凡)
cd8dd2ee4a 1 2024-11-29 11:58:16 +08:00
雷欧(林平凡)
a89d8e3377 1 2024-11-29 11:56:41 +08:00
雷欧(林平凡)
2a77c74ba8 1 2024-11-29 11:51:08 +08:00
雷欧(林平凡)
f3a9536685 1 2024-11-29 11:38:13 +08:00
cc
34c2e8638e 1 2024-11-29 11:32:24 +08:00
雷欧(林平凡)
f34c402757 1 2024-11-29 11:28:09 +08:00
雷欧(林平凡)
74e5d34c9f 1 2024-11-29 11:24:46 +08:00
雷欧(林平凡)
213cf32012 1 2024-11-29 10:15:07 +08:00
雷欧(林平凡)
7ac0e5f94d 1 2024-11-25 15:30:24 +08:00
雷欧(林平凡)
535e6bf9bc 1 2024-11-25 15:29:56 +08:00
Leo
70b339b009 1 2024-11-24 12:50:44 +08:00
Leo
5346a12c3c 1 2024-11-24 02:10:52 +08:00
Leo
8c28084b7a Merge remote-tracking branch 'origin/master' 2024-11-24 02:07:59 +08:00
Leo
463af418e5 1 2024-11-24 02:07:42 +08:00
cc
b6dc0b8608 1 2024-11-24 01:59:04 +08:00
cc
c53341adb6 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/cn/van/business/util/JDUtils.java
2024-11-24 01:58:22 +08:00
cc
adb5a044c1 1 2024-11-24 01:57:39 +08:00
Leo
854aca293b 1 2024-11-24 01:56:41 +08:00
雷欧(林平凡)
6808d26890 1 2024-11-24 01:48:36 +08:00
雷欧(林平凡)
e19c59e491 1 2024-11-21 16:25:53 +08:00
cc
647b360093 1 2024-11-21 16:24:43 +08:00
cc
6120884641 Merge remote-tracking branch 'origin/master' 2024-11-21 16:20:15 +08:00
cc
ceb540a3e9 Merge remote-tracking branch 'origin/master' 2024-11-19 14:49:55 +08:00
cc
2991fc0188 1 2024-11-19 14:49:42 +08:00
94 changed files with 11654 additions and 1760 deletions

4
.gitignore vendored
View File

@@ -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

Binary file not shown.

File diff suppressed because one or more lines are too long

28
.idea/dataSources.xml generated
View File

@@ -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&amp;useSSL=true&amp;serverTimezone=GMT</jdbc-url>
<jdbc-url>jdbc:mysql://192.168.8.88:3306/jd?characterEncoding=utf-8&amp;useSSL=true&amp;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>

View File

@@ -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
View 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
View File

@@ -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>

View File

@@ -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
View File

@@ -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
View File

@@ -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
View 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天前的数据。

View 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
View 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)

View 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)
);

View 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
View File

@@ -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
View 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='图片转换记录表';

View File

@@ -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));
}
}

View 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();
}
}

View 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));
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}
}

View File

@@ -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";
}
}

View 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;
}
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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", "发送小程序"),

View 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 可省略或设为私有,根据实际需求
}

View 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;
}

View 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;
}

View 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;
}

View File

@@ -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;
/**

View File

@@ -1,6 +1,6 @@
package cn.van.business.model.jd;
import javax.persistence.*;
import jakarta.persistence.*;
/**
* @author Leo

View 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;
}

View File

@@ -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;

View 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;
}

View 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;
}

View 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;
}
}

View 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)
);

View 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
}

View 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();
}
}
}

View 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;
}

View File

@@ -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

View 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();
}
}

View File

@@ -1,9 +1,9 @@
package cn.van.business.model.wx;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Getter
@Setter

View File

@@ -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

View File

@@ -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

View 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());
}
}
}

View 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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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> {
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View 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());
// 不抛出异常,因为转换本身已成功
}
}
}

View 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;
}
}
}

View 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();
}
}

File diff suppressed because it is too large Load Diff

View 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 == '.';
}
}

View 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)) {
// POP7天后过期提前一天提醒
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键清理完成 ===");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,753 +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.GoodsService.request.query.BigFieldGoodsReq;
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.UnionOpenGoodsBigfieldQueryRequest;
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.UnionOpenGoodsBigfieldQueryResponse;
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); // 真实代表实时订单
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 startDate = LocalDateTime.now().minusMonths(2).truncatedTo(ChronoUnit.HOURS);
LocalDateTime now = LocalDateTime.now();
LocalDateTime lastHour = now.truncatedTo(ChronoUnit.HOURS);
while (!startDate.isEqual(lastHour)) {
startDate = startDate.plusHours(1);
UnionOpenOrderRowQueryResponse response = fetchOrdersForDateTime(startDate, false); // 假的代表历史订单
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);
}
}
}
}
}
}
/**
* 手动调用 将订单发送到微信
*/
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) 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);
// 标记已拉取
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");
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);
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")) {
String orderId = order;
OrderRow orderRow = orderRowRepository.findById(orderId).orElse(null);
if (orderRow != null) {
content.append(getFormattedOrderInfo(orderRow, orderRow.getValidCode()));
} else {
content.append("订单不存在");
}
}
if (order.startsWith("SKU")){
order = order.replace("SKU", "");
String[] split = order.split("\r\n");
for (String s : split) {
content.append("https://item.jd.com/").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) 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(1);
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();
}
}

View 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;// 违规佣金
}

View 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);
}
}

View 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;// 违规佣金
}
}

View 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:&quot;font-size:large;background-color:#FFFFFF;\">1.项目玩法介绍mp4</span><br />\r\n<br />\r\n<span style=\"color:#555555;font-family:&quot;font-size:large;background-color:#FFFFFF;\">2.账号注册.mp4</span><br />\r\n<br />\r\n<span style=\"color:#555555;font-family:&quot;font-size:large;background-color:#FFFFFF;\">3.素材获取mp4</span><br />\r\n<br />\r\n<span style=\"color:#555555;font-family:&quot;font-size:large;background-color:#FFFFFF;\">4.视频批星制作.mp4</span><br />\r\n<br />\r\n<span style=\"color:#555555;font-family:&quot;font-size:large;background-color:#FFFFFF;\">5.视频发布.mp4</span><br />\r\n<br />\r\n<span style=\"color:#555555;font-family:&quot;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);
}
}
}

View 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();
}
}

View File

@@ -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数组倒序
*

View File

@@ -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@chatroomwxid_ytpc72mdoskt221
public static String chatRoom_BY = "50322578882@chatroom";
//群50006079425@chatroomwxid_cfmrk2upjtf3221
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@chatroomwxid_gca9mnidqhkq11加入群聊
chatRoom_admin_pl.add("43745034055@chatroom");
//群43835433515@chatroomwxid_ytpc72mdoskt221
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@chatroomwxid_ytpc72mdoskt221
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'作为topicjsonObject作为消息内容。
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'作为topicjsonObject作为消息内容。
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;
}
}

View File

@@ -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时为好友wxidfromType=2时为群wxidfromType=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/envsopenapi是/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);
}
}
}
}

View 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);
}
}
}

View File

@@ -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();
}
}

View 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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) {
// // 统一响应处理逻辑...
// }
//}

View File

@@ -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() ;
}

View File

@@ -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();
}
}
}

View 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;
}

View 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
}
}
}

View File

@@ -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

View 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

View File

@@ -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:

View 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>