Compare commits

..

550 Commits

Author SHA1 Message Date
Mrx
2fff537bfc f 2026-06-12 14:45:09 +08:00
Mrx
97f17e0cb7 f 2026-06-12 14:40:57 +08:00
Mrx
ad44b190b7 f 2026-06-12 11:03:40 +08:00
Mrx
1c7b00e29c f 2026-06-11 18:00:07 +08:00
Mrx
47ade3de94 f 2026-06-11 16:39:46 +08:00
Mrx
0642d92069 f 2026-06-11 10:53:22 +08:00
Mrx
0780ddfff8 f 2026-06-11 10:36:44 +08:00
Mrx
e8fef3b832 fadd 2026-06-11 10:31:24 +08:00
Mrx
3e0b100bbf f 2026-06-11 10:24:49 +08:00
Mrx
d5fa2f0a15 f 2026-06-10 21:12:37 +08:00
Mrx
0cfcd6a7ee f 2026-06-10 20:27:18 +08:00
Mrx
66e8bb267d f 2026-06-10 17:47:36 +08:00
Mrx
9d4a3f584c f 2026-06-10 16:05:47 +08:00
Mrx
50f21283e8 f 2026-06-09 16:46:01 +08:00
Mrx
21f7643b06 f 2026-06-09 16:44:10 +08:00
Mrx
fd1f83a0f0 f 2026-06-09 16:28:08 +08:00
Mrx
6d36c86c0f f 2026-06-09 16:20:33 +08:00
Mrx
9f06027820 f 2026-06-09 15:58:29 +08:00
Mrx
a90dfb90ef add f 2026-06-09 11:28:47 +08:00
Mrx
11d93067a2 f 2026-06-08 15:54:05 +08:00
Mrx
937c0a29ff add 2026-06-08 14:56:38 +08:00
Mrx
57ca7be795 f++ 2026-06-08 12:06:07 +08:00
Mrx
a4b2d493db Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-06-07 21:49:12 +08:00
Mrx
0deefd9b0e tax_income_level_v8 2026-06-07 21:49:11 +08:00
32f9299576 f 2026-06-07 12:52:10 +08:00
59c09d6a33 f 2026-06-06 14:45:22 +08:00
Mrx
e4eb41ce10 f 2026-06-05 21:11:25 +08:00
Mrx
85212f4dd2 f 2026-06-04 22:53:59 +08:00
Mrx
962599d970 f 2026-06-04 22:18:06 +08:00
Mrx
4b78edc071 f 2026-06-04 15:01:15 +08:00
Mrx
f08a940807 ff 2026-06-04 12:21:44 +08:00
Mrx
1a1d98194e f 2026-06-04 11:07:39 +08:00
Mrx
be8b516483 f 2026-06-04 11:06:13 +08:00
Mrx
486e586ea1 f 2026-06-02 15:33:41 +08:00
Mrx
6d892643ba f 2026-06-02 12:26:22 +08:00
Mrx
aed109d589 f 2026-06-01 20:47:44 +08:00
Mrx
8ab2a6d81d f 2026-06-01 14:39:45 +08:00
Mrx
928ff4d766 f 2026-05-30 11:13:17 +08:00
Mrx
5ee816d6c1 fix 2026-05-29 21:13:46 +08:00
Mrx
4b1176b1e3 f 2026-05-29 20:13:12 +08:00
Mrx
2a174e49e5 ff 2026-05-29 17:51:30 +08:00
Mrx
a5a0522c91 f 2026-05-29 17:06:27 +08:00
Mrx
a5c955d04a Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-05-29 15:41:53 +08:00
Mrx
708f42fbd1 f 2026-05-29 15:41:52 +08:00
636d9eaf7a Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-05-29 12:37:38 +08:00
0d3a116820 f 2026-05-29 12:37:37 +08:00
Mrx
3c2cb5b6ab Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-05-29 12:28:09 +08:00
Mrx
0973bb099c f 2026-05-29 12:28:08 +08:00
f8bf88f635 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-05-28 12:23:05 +08:00
8a9b87be0e f 2026-05-28 12:23:04 +08:00
Mrx
43acbeb8f4 f 2026-05-28 10:55:28 +08:00
Mrx
b04b43cb82 f 2026-05-27 15:30:59 +08:00
Mrx
6cde82ee69 f 2026-05-27 15:18:43 +08:00
Mrx
b16e68f3bd f 2026-05-27 14:16:41 +08:00
Mrx
f2c4618649 f 2026-05-27 14:12:11 +08:00
Mrx
9e136bbd6b f 2026-05-27 13:10:00 +08:00
Mrx
5cee8ff035 f 2026-05-27 13:00:51 +08:00
Mrx
b7fb2a73c9 f 2026-05-27 12:11:26 +08:00
Mrx
2a33369639 f 2026-05-26 18:09:49 +08:00
Mrx
28fc40525e f 2026-05-26 16:25:11 +08:00
Mrx
4fe180e4c8 f 2026-05-26 16:21:55 +08:00
Mrx
f10c5dd626 f 2026-05-26 16:17:09 +08:00
Mrx
760c5812ee f 2026-05-26 11:28:25 +08:00
Mrx
9bd83ddaba f 2026-05-26 11:21:58 +08:00
Mrx
1cb3363b5c f 2026-05-26 11:14:24 +08:00
Mrx
95c3fc0315 f 2026-05-23 10:30:17 +08:00
Mrx
2020bd732a f 2026-05-21 10:27:41 +08:00
Mrx
1dfe688db7 f 2026-05-21 10:27:05 +08:00
Mrx
9fb5db56e3 f+ 2026-05-20 20:30:50 +08:00
Mrx
95006a2455 f 2026-05-20 16:16:50 +08:00
Mrx
a2913c26ab f 2026-05-20 15:52:04 +08:00
Mrx
40f12ac1cf f 2026-05-19 17:21:16 +08:00
Mrx
8c301de7d2 f 2026-05-11 13:36:49 +08:00
Mrx
38410a7378 f 2026-05-11 13:24:59 +08:00
Mrx
a55d881a3a f 2026-05-11 13:06:25 +08:00
Mrx
be446de86d f 2026-05-11 12:13:32 +08:00
Mrx
07394a4ffa f 2026-05-10 13:36:47 +08:00
Mrx
609a35fad6 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-05-09 21:44:55 +08:00
Mrx
f927f75637 f 2026-05-09 21:44:50 +08:00
ff66c341f5 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-05-09 19:08:04 +08:00
04a683273c f 2026-05-09 19:08:02 +08:00
Mrx
3c71431e55 f 2026-05-09 18:50:45 +08:00
Mrx
b22fdd297b f 2026-05-08 12:07:05 +08:00
Mrx
dfd7e86f6c f 2026-05-07 12:39:37 +08:00
Mrx
1ca6e1c81d f 2026-05-06 16:03:18 +08:00
Mrx
c8a5e6a908 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-05-01 15:22:20 +08:00
Mrx
1ef1da070f f 2026-05-01 15:22:18 +08:00
45b0ddcf9f Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-05-01 11:28:48 +08:00
40f4fab1d4 f 2026-05-01 11:28:46 +08:00
Mrx
c10a54c5b3 f 2026-04-30 10:28:34 +08:00
Mrx
1ca9885e38 f 2026-04-29 16:32:17 +08:00
Mrx
e08a25ba5a f 2026-04-29 16:07:40 +08:00
Mrx
4e33927255 ff 2026-04-29 16:07:31 +08:00
Mrx
af91d8c7f3 f 2026-04-29 16:04:55 +08:00
98a78c3170 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-04-28 12:25:53 +08:00
3b7bf1052e add huibo ivyz4y27 2026-04-28 12:25:41 +08:00
Mrx
6253389d1b f 2026-04-28 11:25:29 +08:00
Mrx
1b6d59a55d Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-04-27 21:15:16 +08:00
Mrx
972d1a0217 f 2026-04-27 21:15:15 +08:00
4f64c22ebc f 2026-04-27 18:21:37 +08:00
20b63bde0c f 2026-04-27 18:14:07 +08:00
fcbd534b57 f 2026-04-25 21:00:22 +08:00
d564f4eb1b f 2026-04-25 20:44:34 +08:00
e89459f093 f 2026-04-25 20:36:28 +08:00
18c92584d9 f 2026-04-25 19:17:19 +08:00
ba463ae38d f 2026-04-25 11:59:10 +08:00
Mrx
e246271a24 f 2026-04-23 18:18:47 +08:00
Mrx
a1024ed4b2 f 2026-04-23 17:59:23 +08:00
Mrx
d6b78a5d6d Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-04-23 10:55:02 +08:00
Mrx
61c6cc4f35 370982199012037272 2026-04-23 10:55:01 +08:00
cdd1e00745 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-04-21 21:03:56 +08:00
46ba4e048c f 2026-04-21 21:02:02 +08:00
Mrx
3156539319 f 2026-04-21 17:03:03 +08:00
Mrx
dad8abad16 f 2026-04-21 16:24:57 +08:00
Mrx
5f62261c11 f 2026-04-21 16:23:35 +08:00
a0b2105339 f 2026-04-20 19:41:29 +08:00
83e71ae81b f 2026-04-20 19:11:49 +08:00
8675961207 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-04-20 18:45:35 +08:00
4bd6f51728 f 2026-04-20 18:45:34 +08:00
Mrx
cd1db5276a f 2026-04-20 16:58:19 +08:00
Mrx
2f653be375 f 2026-04-20 15:22:25 +08:00
Mrx
9c3fb97b3f f 2026-04-20 10:27:55 +08:00
Mrx
b6053983d9 f 2026-04-18 17:45:18 +08:00
Mrx
c3b16c0ffe ff 2026-04-18 16:41:06 +08:00
Mrx
5f6cca5369 f 2026-04-17 18:41:54 +08:00
Mrx
a01226c7c0 f 2026-04-17 18:37:19 +08:00
Mrx
e67465a58d Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-04-16 19:32:23 +08:00
Mrx
75316b10cb f 2026-04-16 19:32:22 +08:00
ebcf3be923 f 2026-04-14 19:23:00 +08:00
cff3fb8814 f 2026-04-11 14:59:07 +08:00
e76fcd89bb f 2026-04-08 19:29:36 +08:00
10605afe1e f 2026-04-08 19:25:36 +08:00
d3554e8b44 f 2026-04-07 13:55:50 +08:00
Mrx
35a2eb03d8 f 2026-04-07 12:54:44 +08:00
Mrx
2a6ec6e3ca f 2026-04-07 12:39:11 +08:00
Mrx
44d8b3d28c f 2026-04-07 12:35:54 +08:00
Mrx
cad1d354f5 f 2026-04-03 11:36:19 +08:00
Mrx
711dc83e47 f 2026-04-02 14:45:48 +08:00
Mrx
65fdc9bf21 addf 2026-04-02 14:01:19 +08:00
Mrx
e9fe7ac303 f 2026-04-02 12:46:42 +08:00
Mrx
130f49fb9d f 2026-04-01 14:18:23 +08:00
Mrx
d66ef0b15f f 2026-03-30 10:57:32 +08:00
Mrx
a6a2d8d9c5 f 2026-03-26 16:08:48 +08:00
Mrx
e095553ba8 f 2026-03-26 15:31:37 +08:00
Mrx
a73097aed3 add 2026-03-26 11:31:11 +08:00
Mrx
8bbd098f97 f 2026-03-25 15:17:24 +08:00
Mrx
5f0224ad3b f 2026-03-25 15:03:45 +08:00
Mrx
9438ccee5e f 2026-03-25 11:07:58 +08:00
Mrx
8771261118 f 2026-03-24 18:23:59 +08:00
Mrx
96c5870aa0 f 2026-03-24 12:18:00 +08:00
Mrx
5e658f2527 add newapilist 2026-03-23 15:34:27 +08:00
Mrx
e03e6b983c add hotapi 2026-03-23 15:29:37 +08:00
Mrx
6ab9bb21e7 f 2026-03-23 14:08:35 +08:00
Mrx
7c4bcefc81 f 2026-03-23 12:59:45 +08:00
Mrx
8eec9685db f 2026-03-23 12:36:23 +08:00
6a801acee1 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-21 19:23:23 +08:00
6120020a7c f 2026-03-21 19:23:08 +08:00
Mrx
da0990e015 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-21 19:17:19 +08:00
Mrx
80faf3cac0 f 2026-03-21 19:17:17 +08:00
df1e8f25ed f 2026-03-21 19:14:52 +08:00
bfe2f065c5 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-21 19:11:11 +08:00
2fcf55deee f 2026-03-21 19:10:50 +08:00
Mrx
9a1cf0d1d1 f 2026-03-21 19:08:11 +08:00
Mrx
895b38ab88 f 2026-03-21 19:01:26 +08:00
Mrx
a96c153286 f 2026-03-21 18:56:30 +08:00
Mrx
040f6eef65 f 2026-03-21 18:51:51 +08:00
Mrx
947a983c67 f 2026-03-21 18:47:04 +08:00
Mrx
df6a51ae62 f 2026-03-21 18:36:42 +08:00
Mrx
06b5aa97ec f 2026-03-21 18:32:55 +08:00
Mrx
3775101081 f 2026-03-21 15:21:57 +08:00
Mrx
39db1e9c1d f 2026-03-21 15:19:24 +08:00
Mrx
15c6257762 f 2026-03-21 14:37:40 +08:00
Mrx
f9a6204b40 up 2026-03-20 18:45:47 +08:00
Mrx
4de32c4c39 verify 2026-03-20 17:14:35 +08:00
Mrx
a6f309e472 f 2026-03-20 16:23:08 +08:00
Mrx
c27b15af18 f 2026-03-20 13:27:08 +08:00
Mrx
249ea0b15a f 2026-03-20 13:26:47 +08:00
Mrx
c193211463 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-20 13:25:47 +08:00
Mrx
521bfeb4ef add 2026-03-20 13:24:45 +08:00
e0d9fd2791 f 2026-03-19 18:02:34 +08:00
58ba7e9f70 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-19 17:47:11 +08:00
Mrx
3779a7d66d f 2026-03-19 17:29:06 +08:00
Mrx
8eb6dfc962 f 2026-03-19 15:21:02 +08:00
Mrx
c8af22f981 f 2026-03-19 13:25:00 +08:00
Mrx
d837624c0a f 2026-03-19 13:23:48 +08:00
Mrx
faf4b7f6a7 f 2026-03-19 11:07:52 +08:00
Mrx
baa45a8a05 f 2026-03-18 16:45:23 +08:00
Mrx
ec1decfdd9 f 2026-03-18 16:27:16 +08:00
Mrx
ca45be642b f 2026-03-18 16:05:01 +08:00
Mrx
bba34f817e f 2026-03-18 13:30:19 +08:00
Mrx
4e8f9317f5 f 2026-03-18 13:20:41 +08:00
Mrx
ce2d4087bb f 2026-03-18 12:51:54 +08:00
Mrx
0ce793ac61 f and add 2026-03-18 11:44:37 +08:00
Mrx
12ed1c81e3 add 2026-03-17 17:18:54 +08:00
Mrx
6f0a8e0519 f 2026-03-16 13:10:42 +08:00
Mrx
14b2c53eeb f 2026-03-16 12:32:41 +08:00
Mrx
09db8d003e f 2026-03-13 18:21:09 +08:00
Mrx
209ffec51d f 2026-03-13 18:07:24 +08:00
Mrx
f16274d1e9 f 2026-03-12 14:35:13 +08:00
869b269fb1 f 2026-03-11 19:36:26 +08:00
9e76fd467b f 2026-03-11 19:19:18 +08:00
1a5e771420 f 2026-03-11 19:10:55 +08:00
2114f602de f 2026-03-11 18:12:14 +08:00
5650e78254 f 2026-03-11 18:05:10 +08:00
67c6e2e144 f 2026-03-11 17:23:09 +08:00
ba1a72aa8f f 2026-03-11 15:35:50 +08:00
058e355d77 f 2026-03-11 15:31:36 +08:00
2741839cf3 f 2026-03-11 15:27:32 +08:00
454e60dd72 f 2026-03-11 15:21:53 +08:00
03cb6fd92b f 2026-03-11 15:00:25 +08:00
8441e66e93 f 2026-03-11 14:55:45 +08:00
16a2e4ff09 f 2026-03-11 14:13:57 +08:00
c5970da195 f 2026-03-10 19:15:54 +08:00
1bcb4a9c2e f 2026-03-10 19:12:35 +08:00
8877cf9691 f 2026-03-10 19:07:29 +08:00
f63e6df9f9 f 2026-03-10 19:03:42 +08:00
a00fe12141 f 2026-03-10 17:56:31 +08:00
6b80182986 f 2026-03-10 17:49:53 +08:00
bf4c114ee2 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-10 17:28:22 +08:00
4cd3954574 f 2026-03-10 17:28:04 +08:00
Mrx
d7a5589873 f 2026-03-09 11:31:40 +08:00
Mrx
b0ec75d1af f 2026-03-06 16:39:00 +08:00
Mrx
57d18be972 f 2026-03-06 16:28:25 +08:00
Mrx
3d8775b6dc f 2026-03-06 15:20:27 +08:00
Mrx
f40950f890 f 2026-03-06 15:12:58 +08:00
Mrx
ba21a8f965 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-06 10:56:37 +08:00
Mrx
7d2716da7a f 2026-03-06 10:56:36 +08:00
9a7bda9527 f 2026-03-05 19:03:18 +08:00
abdae033f0 f 2026-03-05 18:44:17 +08:00
96abacd392 f 2026-03-05 18:41:00 +08:00
4e6c93413e Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-05 17:44:59 +08:00
9c8dbd458f f 2026-03-05 17:44:50 +08:00
Mrx
9e9cee02f5 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-05 16:24:56 +08:00
Mrx
360bd579ce f 2026-03-05 16:24:53 +08:00
db889ccba0 f 2026-03-05 14:17:48 +08:00
25a4961328 f 2026-03-05 12:26:02 +08:00
Mrx
578e68a76b f 2026-03-05 11:05:01 +08:00
Mrx
019e47896d f 2026-03-05 10:57:06 +08:00
Mrx
c0898e6829 f 2026-03-05 10:54:16 +08:00
Mrx
4ee6e891cd f 2026-03-04 14:10:30 +08:00
Mrx
44b5f6b145 f 2026-03-04 13:20:44 +08:00
Mrx
677b7362cf f 2026-03-04 13:19:55 +08:00
Mrx
02dbc02fe8 f 2026-03-04 12:59:45 +08:00
Mrx
374143995e f 2026-03-04 12:41:08 +08:00
Mrx
7a957a6b87 f 2026-03-04 12:39:01 +08:00
Mrx
c885d562ee f 2026-03-02 19:21:23 +08:00
Mrx
9f36cd8b63 f 2026-03-02 12:21:25 +08:00
Mrx
4122f874fc f 2026-03-02 12:16:27 +08:00
Mrx
9a32387b21 f 2026-03-02 11:56:47 +08:00
Mrx
7bf9150cfc f 2026-03-02 11:39:53 +08:00
Mrx
fecd5a38fd Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-02 11:38:30 +08:00
Mrx
2636d9dff6 f 2026-03-02 11:38:19 +08:00
927b08b871 f 2026-02-28 14:49:42 +08:00
dedd4a60a4 f 2026-02-28 14:05:54 +08:00
a54a19e439 f 2026-02-27 16:49:45 +08:00
6dd392f673 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-02-27 16:43:06 +08:00
8d5da9d88e f 2026-02-27 16:42:38 +08:00
Mrx
bc6dce21ee f 2026-02-27 14:59:23 +08:00
Mrx
5630d93de6 f 2026-02-27 14:53:08 +08:00
Mrx
d12529307b add 2026-02-27 14:49:29 +08:00
f17e22f4c8 f 2026-02-25 19:33:56 +08:00
8c0c16006e f 2026-02-25 17:50:28 +08:00
532503ffe3 f 2026-02-25 17:45:14 +08:00
af37f8620c f 2026-02-25 14:54:49 +08:00
fb495bc0ca f 2026-02-14 17:42:59 +08:00
2c57ac0dab Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-02-14 17:26:15 +08:00
4b437ecf56 f 2026-02-14 17:26:01 +08:00
Mrx
abf6284482 open judiciaRiskInfos 2026-02-14 16:06:28 +08:00
e6ab833099 f 2026-02-12 13:43:03 +08:00
47cbc5b3a5 f 2026-02-12 13:36:24 +08:00
f400052f95 f 2026-02-12 13:27:08 +08:00
Mrx
a38c58c357 de|| paramsDto.IDCard == "320682198910134998" 2026-02-12 12:48:36 +08:00
Mrx
ede446fb90 f 2026-02-12 11:35:09 +08:00
e23897a13f f 2026-02-11 20:15:24 +08:00
debd0a973f f 2026-02-11 20:12:58 +08:00
b424082f01 f 2026-02-11 20:08:27 +08:00
4afbd7dec7 f 2026-02-11 19:39:27 +08:00
070310cfa7 f 2026-02-11 19:15:38 +08:00
Mrx
62e220a7b8 f 2026-02-10 18:00:20 +08:00
Mrx
c37cf2b54a f 2026-02-10 17:37:15 +08:00
Mrx
25a3b4a761 f 2026-02-10 15:36:48 +08:00
Mrx
1c83102e06 f 2026-02-10 14:32:58 +08:00
Mrx
d55520e69c f 2026-02-10 11:47:24 +08:00
Mrx
4ea7cf4ecb ff 2026-02-10 11:27:33 +08:00
Mrx
3eae08a576 f 2026-02-08 12:32:26 +08:00
Mrx
c9126dd780 ff 2026-02-08 11:54:32 +08:00
Mrx
f48289b32b f 2026-01-31 17:43:31 +08:00
Mrx
0091e01574 f 2026-01-31 12:27:11 +08:00
Mrx
d2b806eda0 f 2026-01-31 12:20:32 +08:00
Mrx
ff86cb6fb9 f 2026-01-31 12:15:42 +08:00
Mrx
a6f858dbd3 f 2026-01-31 11:32:53 +08:00
Mrx
6a1a59de8d f 2026-01-30 18:32:16 +08:00
Mrx
dbcecde4e0 f 2026-01-30 18:25:30 +08:00
Mrx
3158bf8c04 f 2026-01-29 17:10:44 +08:00
Mrx
860756b767 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-01-29 16:51:15 +08:00
Mrx
3c90144d51 f 2026-01-29 16:51:11 +08:00
29c49d6a00 f 2026-01-29 16:40:19 +08:00
5bff33547c f 2026-01-29 16:34:37 +08:00
f50e11a052 f 2026-01-29 16:12:42 +08:00
d1e06984ac f 2026-01-29 16:00:51 +08:00
2325a110b6 f 2026-01-29 15:48:07 +08:00
167dd63a7a f 2026-01-29 15:35:25 +08:00
360fed3907 f 2026-01-29 15:27:45 +08:00
88787c6145 f 2026-01-29 15:25:30 +08:00
bfa8bbcfcb Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-01-29 15:03:42 +08:00
2fea046981 f 2026-01-29 15:03:38 +08:00
Mrx
4e359060e6 无缝替换西部 2026-01-28 16:58:57 +08:00
Mrx
7e5a69ffaa f 2026-01-28 16:25:40 +08:00
Mrx
482644a914 f 2026-01-28 16:21:57 +08:00
Mrx
b2f0b47896 f 去掉66sl 2026-01-28 15:55:38 +08:00
Mrx
b1f573c230 f 2026-01-28 12:21:21 +08:00
2363a51a6a f 2026-01-27 19:12:02 +08:00
847d48d276 f 2026-01-27 19:00:09 +08:00
43e4daa45b f 2026-01-27 18:12:38 +08:00
2005f09248 f 2026-01-27 17:33:09 +08:00
eb8886d961 f 2026-01-27 16:50:13 +08:00
411aeb8e25 f 2026-01-27 16:42:25 +08:00
f8806eb71c f 2026-01-27 16:26:48 +08:00
Mrx
3ef7b7d1fb f 2026-01-26 18:14:24 +08:00
32336e4ba0 f 2026-01-25 14:35:50 +08:00
Mrx
168a7c7f5f f 2026-01-24 15:38:15 +08:00
Mrx
ebaff673c7 f 2026-01-24 14:13:18 +08:00
Mrx
1cfd4bf0d0 f 2026-01-24 12:28:49 +08:00
Mrx
21d312f143 f 2026-01-24 11:48:45 +08:00
Mrx
37ce65d6f7 f 2026-01-24 11:36:41 +08:00
Mrx
4664c06c03 f 2026-01-24 11:02:05 +08:00
Mrx
ea59a8ddff f 2026-01-24 10:44:09 +08:00
Mrx
b171288361 f 2026-01-24 10:25:13 +08:00
Mrx
426e6f537c f 2026-01-24 10:22:43 +08:00
Mrx
bce7ba9ef4 f 2026-01-23 19:05:57 +08:00
Mrx
da1eef618e f 2026-01-23 19:03:01 +08:00
Mrx
791147e520 f 2026-01-23 18:58:40 +08:00
Mrx
82959f74a8 f 2026-01-23 18:50:52 +08:00
Mrx
6fccd4b223 f 2026-01-23 18:39:11 +08:00
Mrx
e48efb4566 f 2026-01-23 18:37:22 +08:00
Mrx
e6e38013b8 f 2026-01-23 18:33:14 +08:00
Mrx
9b223c996d f 2026-01-23 18:18:49 +08:00
Mrx
78035ca1ab 替换shumai 2026-01-23 18:10:07 +08:00
Mrx
38ca033e31 f 2026-01-23 18:02:39 +08:00
Mrx
b05c459b81 f 2026-01-23 17:53:11 +08:00
Mrx
6104bfb84f f 2026-01-22 18:10:21 +08:00
Mrx
d15d2f2499 f 2026-01-22 17:31:32 +08:00
Mrx
7e45a45309 f 2026-01-22 14:44:39 +08:00
Mrx
473468e680 f 2026-01-22 14:26:15 +08:00
Mrx
7785d3b6ef f 2026-01-22 12:23:59 +08:00
Mrx
6607129083 f 2026-01-21 18:02:31 +08:00
Mrx
0036180979 s 2026-01-21 18:00:38 +08:00
Mrx
0d3a006c46 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-01-21 16:12:58 +08:00
Mrx
17f2433fee 重新启用QYGL3F8E upQYGL6S1B addFXGL5a3b dQYGL8271 2026-01-21 16:10:23 +08:00
8a222a0b7f f 2026-01-20 18:32:16 +08:00
03cfddee93 f 2026-01-20 17:31:35 +08:00
Mrx
abc7d655ce -f 2026-01-20 14:26:47 +08:00
Mrx
bdb8395615 -f 2026-01-20 14:25:50 +08:00
Mrx
6905936cf3 良心老板空不扣费的修改 2026-01-20 11:29:58 +08:00
Mrx
85bd011fd3 f 2026-01-19 14:28:21 +08:00
2d5c2120fb f+ 2026-01-19 11:33:21 +08:00
3e5016b439 f 2026-01-17 14:09:42 +08:00
96d530a67d -f 2026-01-16 18:37:47 +08:00
96dfa3d758 -f单独处理,null不计费 2026-01-15 15:30:39 +08:00
1e2687522b F 2026-01-15 13:26:09 +08:00
f6e7d46067 F 2026-01-15 13:05:37 +08:00
1e4e2f4b6d add 2026-01-14 17:51:03 +08:00
21fae5c486 f 2026-01-14 16:22:02 +08:00
6b902f68f1 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-01-14 16:00:32 +08:00
745fbc05d5 f 2026-01-14 16:00:27 +08:00
6618e35869 -f 2026-01-14 15:59:15 +08:00
7fdfa02189 f 2026-01-14 13:25:31 +08:00
d60dc70798 change sms template code 2026-01-14 13:21:11 +08:00
7a589a9c13 -f 2026-01-14 12:57:53 +08:00
afc2ab9f4d -f 2026-01-14 12:54:04 +08:00
8b3a80b93f -f 2026-01-14 12:46:29 +08:00
a36d188701 -f 2026-01-14 12:07:58 +08:00
ef2d73a9ec f 2026-01-14 11:58:06 +08:00
c29c1bceff f 2026-01-12 13:39:41 +08:00
46a181d027 f 2026-01-12 13:31:13 +08:00
ff8a946d13 f 2026-01-12 13:26:33 +08:00
68a9e32131 f 2026-01-12 13:20:17 +08:00
ead5f17b7c add f 2026-01-09 15:58:09 +08:00
bd76520d22 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-01-07 13:05:27 +08:00
ee55b068a6 fix 2026-01-07 13:05:12 +08:00
45397891b8 f 2026-01-07 12:43:26 +08:00
ae482b7888 add try 2026-01-07 10:42:09 +08:00
ddbae6f82a f 2026-01-06 18:11:37 +08:00
90f16911e9 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-01-06 17:42:11 +08:00
7746c5c8e3 f 2026-01-06 17:42:09 +08:00
8098c13de3 f 2026-01-06 17:06:33 +08:00
e61f03a2dd add 极光数据 2026-01-06 16:37:31 +08:00
4fcf370dcd fix 2026-01-05 17:19:55 +08:00
b0ed5b04ee fix add 2026-01-05 16:41:05 +08:00
269ff38604 fix add 2026-01-05 16:36:50 +08:00
23909c44f1 Resolve merge conflicts 2026-01-05 14:34:58 +08:00
aabe34b7d5 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-01-05 14:31:11 +08:00
c262894372 add 全国企业司法模型服务查询_V1 2026-01-05 14:26:26 +08:00
39f799bc41 fix 2025-12-31 18:06:59 +08:00
1d4411a940 add 2025-12-31 17:46:03 +08:00
fe44b452e3 新增:极光config 2025-12-31 16:12:09 +08:00
f1ec9bfe7f 新增:新增极光源,极光婚姻接口(测试) 2025-12-31 15:42:05 +08:00
a70e736cdd fix 2025-12-27 19:18:12 +08:00
53d2c75a9c 更改:给yysy09cd无缝切换换源 2025-12-26 18:48:53 +08:00
0bfa7b4f50 移除王起航 2025-12-26 14:41:31 +08:00
e2e729eec8 add 人脸123 2025-12-25 18:25:37 +08:00
5f7fb43804 fix 2025-12-25 12:40:40 +08:00
89c5c0f9ad 1 2025-12-24 17:52:51 +08:00
6c949a4a1c fix id_car 2025-12-24 17:52:33 +08:00
8556e7331d add fix id_car 2025-12-24 17:50:33 +08:00
311d7a9b01 fix 1 2025-12-24 12:32:25 +08:00
ce45ce3ed0 fix 2025-12-23 18:54:38 +08:00
34e2c1bc41 fixdele 2025-12-23 17:17:41 +08:00
2618105140 fix 2025-12-23 16:16:45 +08:00
6b41f3833a fix 2025-12-23 15:04:53 +08:00
446a5c7661 fix 2025-12-23 11:26:25 +08:00
7f8554fa12 add购买记录功能 2025-12-22 18:32:34 +08:00
65a61d0336 fix2 2025-12-19 18:16:22 +08:00
8dd6f71baf fix 2025-12-19 18:13:31 +08:00
e96653751d fix 2025-12-19 17:50:29 +08:00
39c46937ea addui 2025-12-19 17:05:09 +08:00
cc3472ff40 Enhance ProductRoutes to include component report handling and related routes 2025-12-17 16:13:13 +08:00
451d869361 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2025-12-16 14:32:42 +08:00
08ea153cac uplogo 2025-12-16 14:32:41 +08:00
6147878dfe Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2025-12-14 21:38:49 +08:00
be47a0f045 fix 2025-12-14 21:38:01 +08:00
810696e0f0 fix 2025-12-13 15:09:55 +08:00
17dbaf1ccb fix 2025-12-12 15:47:20 +08:00
18f3d10518 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2025-12-12 15:27:17 +08:00
0d4953c6d3 微信支付 2025-12-12 15:27:15 +08:00
3f64600f02 change 2025-12-11 19:32:46 +08:00
2c89b8cb26 fix组合包文档 2025-12-11 11:14:31 +08:00
09d7a4f076 fix_step null false 2025-12-10 17:51:28 +08:00
83d0fd6587 fix身份证最后一个字母提醒 2025-12-10 14:17:33 +08:00
0fd28054f1 fix 2025-12-10 10:32:33 +08:00
ce858983ee 503接口问题 2025-12-09 18:32:29 +08:00
9b2bffae15 移动端弹幕优化 2025-12-09 16:42:37 +08:00
c68ece5bea 企业五要素 2025-12-09 15:12:03 +08:00
398d2cee74 企业五要素 2025-12-09 14:42:32 +08:00
b6c8d93af5 1 2025-12-09 10:48:13 +08:00
b423aa6be8 2 2025-12-09 10:25:26 +08:00
a47c306c87 18278715334@163.com 2025-12-08 14:03:14 +08:00
af88bcc8eb 18278715334@163.com 2025-12-06 15:46:46 +08:00
89367fb2ee 18278715334@163.com 2025-12-06 13:56:00 +08:00
05b6623e75 18278715334@163.com 2025-12-05 14:59:23 +08:00
bfedec249f 18278715334@163.com 2025-12-04 18:10:14 +08:00
9f669a9c94 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2025-12-04 16:19:20 +08:00
0f5c4f4303 18278715334@163.com 2025-12-04 16:17:27 +08:00
d9c2d9f103 18278715334@163.com 2025-12-04 16:17:04 +08:00
7e0d58b295 change esign template 2025-12-04 15:11:25 +08:00
a17ff2140e fix 2025-12-04 14:26:56 +08:00
6a2241bc66 fix 2025-12-04 14:21:58 +08:00
e57bef6609 fix 2025-12-04 14:00:22 +08:00
81639a81e6 fix 2025-12-04 13:42:32 +08:00
aaf17321ff fix 2025-12-04 13:28:03 +08:00
a8a4ff2d37 fix 2025-12-04 13:20:03 +08:00
619deeb456 fix pdf path 2025-12-04 13:09:59 +08:00
f12c3fb8ad fix pdf 2025-12-04 12:56:39 +08:00
4ce8fe4023 18278715334@163.com 2025-12-04 12:30:33 +08:00
7b45b43a0e 2 2025-12-04 10:47:58 +08:00
752b90b048 1 2025-12-04 10:41:07 +08:00
68def7e08b 18287815334@163.com 2025-12-04 10:35:11 +08:00
b0e8974d6c 将字体文件添加到 .gitignore,不再进行版本控制 2025-12-04 10:27:31 +08:00
b41d41ddf3 18278781533@163.com 2025-12-03 18:25:04 +08:00
b08a63fc99 18278715334@163.com 2025-12-03 18:02:49 +08:00
1f06f21faf 18278715334@163.com 2025-12-03 16:53:31 +08:00
3f5a126bfa 18278715334@163.com 2025-12-03 16:53:19 +08:00
17ff48a642 18278715334@163.com 2025-12-03 15:53:31 +08:00
af629e96c2 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2025-12-03 12:03:47 +08:00
63252fa30f 18278715334@163.cmo 2025-12-03 12:03:42 +08:00
1cf64e831c 18278715334@163.com 2025-12-03 12:02:47 +08:00
577c2bc581 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2025-12-02 12:24:22 +08:00
6d73dad88e fix document version 2025-12-02 12:24:09 +08:00
937c812ea5 abc 2025-12-01 18:35:17 +08:00
63e2fba464 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2025-11-29 15:31:17 +08:00
9c776b8bf3 add 11/29 2025-11-29 15:23:57 +08:00
500264e9e5 add 11.29 2025-11-29 15:16:24 +08:00
b90935a7c3 fix 2025-11-29 14:28:16 +08:00
c404e797f3 fix 2025-11-26 20:33:11 +08:00
ce9052f85b fix 2025-11-26 20:33:04 +08:00
11fe48809e Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2025-11-26 18:23:13 +08:00
785818f73d 18278715334@163.com 2025-11-26 18:23:02 +08:00
c10fb27b93 custom combwd01 2025-11-25 18:56:37 +08:00
4b0ab842f4 fix 2025-11-22 16:13:05 +08:00
4c16e7a333 fix 2025-11-22 15:58:30 +08:00
8d0f1e6aa3 fix 2025-11-22 15:43:24 +08:00
7fc072e608 add ivyz9k2l 2025-11-20 20:16:18 +08:00
a53727757c fix 2025-11-20 20:01:49 +08:00
90d0324a1a add ivyz9k2l 2025-11-20 18:02:18 +08:00
15d0759cfb fix ivyz3p9m 2025-11-19 13:41:41 +08:00
604174cce7 add auto comb cost price 2025-11-13 22:13:05 +08:00
1bfeac0504 added: Cost price adjustment 2025-11-13 21:28:08 +08:00
00a3f0f1e9 add new processor 2025-11-13 20:43:35 +08:00
f00cee7410 fix 2025-11-12 23:44:53 +08:00
3745a3768f add COMBHZY2 2025-11-12 23:16:37 +08:00
a3d0b341a9 fix 2025-11-12 22:13:45 +08:00
b74c02b9f0 fix 2025-11-12 22:04:32 +08:00
c740ae5639 feat: 添加 muzi 服务并接入 ivyz3p9m 2025-11-09 16:08:58 +08:00
5233f0f0f0 fix DecryptParams 2025-11-09 16:08:05 +08:00
b4134d7942 fix 2025-11-03 13:32:05 +08:00
7e2af0e4f5 fix 2025-11-02 20:36:33 +08:00
2773c1a60b fix 2025-11-02 20:33:28 +08:00
bb88c78c82 fix form_config_service 2025-11-01 21:19:21 +08:00
90bb1d017e fix comb form_config_service 2025-11-01 16:57:58 +08:00
da37b4d7bc fix JRZQ8A2D 2025-10-29 22:09:00 +08:00
657d51ad57 fix 2025-10-29 22:05:53 +08:00
368dc2669c add jrzq0l85 2025-10-24 17:18:46 +08:00
79cd87bd18 add yysy9e4a 2025-10-22 16:40:14 +08:00
99d9405db0 fix 2025-10-21 15:23:03 +08:00
3d3ca98eb7 fix 2025-10-17 18:40:02 +08:00
96f22b4249 fix 2025-10-17 18:36:17 +08:00
e5a5e85e5d fix QCXG9P1C 2025-10-17 18:32:48 +08:00
adc9db7f78 new 2025-10-17 17:59:54 +08:00
b92dfd0d58 fix 2025-10-16 19:58:01 +08:00
b05d694755 fix 2025-10-16 18:46:44 +08:00
5a6e95906c new 2025-10-16 18:35:18 +08:00
a49d58365e fix 2025-10-14 20:50:32 +08:00
309a9a4c96 fix 2025-10-11 19:10:09 +08:00
532b92713b fix 2025-09-30 12:03:51 +08:00
1b931cb816 new 2025-09-20 23:29:49 +08:00
7b1b75e7a9 fix 2025-09-20 19:05:07 +08:00
2685df85c3 fix 2025-09-20 17:46:33 +08:00
353c57c98b fix ivyz7f3a 2025-09-17 19:02:50 +08:00
cfad2bce09 fix rank 2025-09-15 21:15:23 +08:00
6874f67c45 fix 2025-09-14 16:34:55 +08:00
a0fc9dc246 fix 2025-09-12 13:29:03 +08:00
c46c1e23a1 fix 2025-09-12 13:20:08 +08:00
e05ad9e223 new 2025-09-12 01:15:09 +08:00
c563b2266b fix dto 2025-09-11 16:18:26 +08:00
c1f127e9b1 fix 2025-09-03 13:51:52 +08:00
c579e53ad1 fix 2025-09-02 20:46:10 +08:00
d73659fed3 fix 2025-09-02 17:13:16 +08:00
c7c4ab7a19 add timed 2025-09-02 16:37:28 +08:00
2f3817c8f0 fix 2025-09-01 21:15:15 +08:00
16a8cd5506 fix 2025-09-01 21:02:19 +08:00
ebacec8e16 fix 2025-09-01 20:48:54 +08:00
5c5c2abfcd fix 2025-09-01 20:46:56 +08:00
5d5372e359 add article 2025-09-01 18:29:59 +08:00
34ff6ce916 fix: 修复validAuthDate验证器未定义错误 2025-08-30 01:34:54 +08:00
a2008e66e6 fix 2025-08-29 16:14:36 +08:00
ecc7495954 fix 2025-08-28 17:09:21 +08:00
f324f15397 fix 2025-08-28 01:05:46 +08:00
50a4fa86ce add cors 2025-08-28 00:50:30 +08:00
5051aea55c fix 2025-08-27 22:19:19 +08:00
4031277a91 fix 2025-08-26 21:40:02 +08:00
958f23487c fix 2025-08-26 16:52:31 +08:00
b1049cd984 fix processor 2025-08-26 16:47:24 +08:00
a91bde0c67 fix ivyz7c9d
fix external_logger
2025-08-26 16:03:46 +08:00
2a93d120f1 add new api 2025-08-26 14:43:27 +08:00
267ff92998 add JRZQ09J8、FLXGDEA8、FLXGDEA9、JRZQ1D09
add external_services log
2025-08-25 15:44:06 +08:00
365a2a8886 add Subscribe Discount 2025-08-23 16:30:34 +08:00
5dad8a3ccb add subscription gorm preload 2025-08-23 16:02:03 +08:00
4c9bb7cbf7 fix alipay return 2025-08-23 14:57:37 +08:00
9f8630784d change database config 2025-08-19 01:49:19 +08:00
ecfe7a6fd6 fix validator 2025-08-18 18:18:04 +08:00
736 changed files with 229492 additions and 8923 deletions

11
.gitignore vendored
View File

@@ -26,6 +26,7 @@ Thumbs.db
tmp/ tmp/
temp/ temp/
console console
worker
# 依赖目录 # 依赖目录
vendor/ vendor/
@@ -34,8 +35,18 @@ vendor/
coverage.out coverage.out
coverage.html coverage.html
# 字体文件(大文件,不进行版本控制)
internal/shared/pdf/fonts/*.ttf
internal/shared/pdf/fonts/*.ttc
internal/shared/pdf/fonts/*.otf
# Pure Component 目录(用于持久化存储,不进行版本控制)
resources/Pure_Component/
# 其他 # 其他
*.exe *.exe
*.exe*
*.dll *.dll
*.so *.so
*.dylib *.dylib
cmd/api/__debug_bin*

View File

@@ -1,288 +0,0 @@
# DDD规范企业认证信息自动填充实现总结
## 概述
根据DDD领域驱动设计架构规范重新设计了企业认证信息自动填充功能。在DDD中跨域操作应该通过应用服务层来协调而不是在领域服务层直接操作其他领域的仓储。
## DDD架构规范
### 1. 领域边界原则
- **领域服务层**:只能操作本领域的仓储和实体
- **应用服务层**:负责跨域协调,调用不同领域的服务
- **聚合根**:每个领域有自己的聚合根,不能直接访问其他领域的聚合根
### 2. 依赖方向
```
应用服务层 → 领域服务层 → 仓储层
跨域协调
```
## 重新设计架构
### 1. 领域服务层Finance Domain
#### `UserInvoiceInfoService`接口更新
```go
type UserInvoiceInfoService interface {
// 基础方法
GetUserInvoiceInfo(ctx context.Context, userID string) (*entities.UserInvoiceInfo, error)
CreateOrUpdateUserInvoiceInfo(ctx context.Context, userID string, invoiceInfo *value_objects.InvoiceInfo) (*entities.UserInvoiceInfo, error)
// 新增:包含企业认证信息的方法
GetUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error)
CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, invoiceInfo *value_objects.InvoiceInfo, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error)
ValidateInvoiceInfo(ctx context.Context, invoiceInfo *value_objects.InvoiceInfo, invoiceType value_objects.InvoiceType) error
DeleteUserInvoiceInfo(ctx context.Context, userID string) error
}
```
#### 实现特点
- **移除跨域依赖**:不再直接依赖`user_repo.UserRepository`
- **参数化设计**:通过方法参数接收企业认证信息
- **保持纯净性**:领域服务只处理本领域的业务逻辑
### 2. 应用服务层Application Layer
#### `InvoiceApplicationService`更新
```go
type InvoiceApplicationServiceImpl struct {
invoiceRepo finance_repo.InvoiceApplicationRepository
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository
userRepo user_repo.UserRepository
userAggregateService user_service.UserAggregateService // 新增:用户聚合服务
rechargeRecordRepo finance_repo.RechargeRecordRepository
walletRepo finance_repo.WalletRepository
invoiceDomainService services.InvoiceDomainService
invoiceAggregateService services.InvoiceAggregateService
userInvoiceInfoService services.UserInvoiceInfoService
storageService *storage.QiNiuStorageService
logger *zap.Logger
}
```
#### 跨域协调逻辑
```go
func (s *InvoiceApplicationServiceImpl) GetUserInvoiceInfo(ctx context.Context, userID string) (*dto.InvoiceInfoResponse, error) {
// 1. 通过用户聚合服务获取企业认证信息
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取用户企业认证信息失败: %w", err)
}
// 2. 提取企业认证信息
var companyName, taxpayerID string
var companyNameReadOnly, taxpayerIDReadOnly bool
if user.EnterpriseInfo != nil {
companyName = user.EnterpriseInfo.CompanyName
taxpayerID = user.EnterpriseInfo.UnifiedSocialCode
companyNameReadOnly = true
taxpayerIDReadOnly = true
}
// 3. 调用领域服务,传入企业认证信息
userInvoiceInfo, err := s.userInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo(ctx, userID, companyName, taxpayerID)
if err != nil {
return nil, err
}
// 4. 构建响应DTO
return &dto.InvoiceInfoResponse{
CompanyName: userInvoiceInfo.CompanyName,
TaxpayerID: userInvoiceInfo.TaxpayerID,
// ... 其他字段
CompanyNameReadOnly: companyNameReadOnly,
TaxpayerIDReadOnly: taxpayerIDReadOnly,
}, nil
}
```
### 3. 依赖注入更新
#### 容器配置
```go
// 用户聚合服务
fx.Annotate(
user_service.NewUserAggregateService,
fx.ResultTags(`name:"userAggregateService"`),
),
// 用户开票信息服务移除userRepo依赖
fx.Annotate(
finance_service.NewUserInvoiceInfoService,
fx.ParamTags(
`name:"userInvoiceInfoRepo"`,
),
fx.ResultTags(`name:"userInvoiceInfoService"`),
),
// 发票应用服务添加userAggregateService依赖
fx.Annotate(
finance.NewInvoiceApplicationService,
fx.As(new(finance.InvoiceApplicationService)),
fx.ParamTags(
`name:"invoiceRepo"`,
`name:"userInvoiceInfoRepo"`,
`name:"userRepo"`,
`name:"userAggregateService"`, // 新增
`name:"rechargeRecordRepo"`,
`name:"walletRepo"`,
`name:"domainService"`,
`name:"aggregateService"`,
`name:"userInvoiceInfoService"`,
`name:"storageService"`,
`name:"logger"`,
),
),
```
## 架构优势
### 1. 符合DDD规范
- **领域边界清晰**:每个领域只处理自己的业务逻辑
- **依赖方向正确**:应用服务层负责跨域协调
- **聚合根隔离**:不同领域的聚合根不直接交互
### 2. 可维护性
- **职责分离**:领域服务专注于本领域逻辑
- **易于测试**:可以独立测试每个领域服务
- **扩展性好**:新增跨域功能时只需修改应用服务层
### 3. 业务逻辑清晰
- **数据流向明确**:企业认证信息 → 应用服务 → 开票信息
- **错误处理统一**:在应用服务层统一处理跨域错误
- **权限控制集中**:在应用服务层统一控制访问权限
## 工作流程
### 1. 获取开票信息流程
```
用户请求 → 应用服务层
调用UserAggregateService.GetUserWithEnterpriseInfo()
获取企业认证信息
调用UserInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo()
返回开票信息(包含企业认证信息)
```
### 2. 更新开票信息流程
```
用户请求 → 应用服务层
调用UserAggregateService.GetUserWithEnterpriseInfo()
验证企业认证状态
调用UserInvoiceInfoService.CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo()
保存开票信息(企业认证信息自动填充)
```
## 技术实现要点
### 1. 接口设计
- **向后兼容**:保留原有的基础方法
- **功能扩展**:新增包含企业认证信息的方法
- **参数传递**:通过方法参数传递跨域数据
### 2. 错误处理
- **分层处理**:在应用服务层处理跨域错误
- **错误传播**:领域服务层错误向上传播
- **用户友好**:提供清晰的错误信息
### 3. 性能优化
- **减少查询**:应用服务层缓存企业认证信息
- **批量操作**:支持批量获取和更新
- **异步处理**:非关键路径支持异步处理
## 代码示例
### 1. 领域服务实现
```go
// GetUserInvoiceInfoWithEnterpriseInfo 获取用户开票信息(包含企业认证信息)
func (s *UserInvoiceInfoServiceImpl) GetUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error) {
info, err := s.userInvoiceInfoRepo.FindByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取用户开票信息失败: %w", err)
}
// 如果没有找到开票信息记录,创建新的实体
if info == nil {
info = &entities.UserInvoiceInfo{
ID: uuid.New().String(),
UserID: userID,
CompanyName: "",
TaxpayerID: "",
BankName: "",
BankAccount: "",
CompanyAddress: "",
CompanyPhone: "",
ReceivingEmail: "",
}
}
// 使用传入的企业认证信息填充公司名称和纳税人识别号
if companyName != "" {
info.CompanyName = companyName
}
if taxpayerID != "" {
info.TaxpayerID = taxpayerID
}
return info, nil
}
```
### 2. 应用服务实现
```go
func (s *InvoiceApplicationServiceImpl) UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error {
// 获取用户企业认证信息
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return fmt.Errorf("获取用户企业认证信息失败: %w", err)
}
// 检查用户是否有企业认证信息
if user.EnterpriseInfo == nil {
return fmt.Errorf("用户未完成企业认证,无法创建开票信息")
}
// 创建开票信息对象
invoiceInfo := value_objects.NewInvoiceInfo(
"", // 公司名称将由服务层从企业认证信息中获取
"", // 纳税人识别号将由服务层从企业认证信息中获取
req.BankName,
req.BankAccount,
req.CompanyAddress,
req.CompanyPhone,
req.ReceivingEmail,
)
// 使用包含企业认证信息的方法
_, err = s.userInvoiceInfoService.CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(
ctx,
userID,
invoiceInfo,
user.EnterpriseInfo.CompanyName,
user.EnterpriseInfo.UnifiedSocialCode,
)
return err
}
```
## 总结
通过按照DDD规范重新设计我们实现了
1.**架构规范**严格遵循DDD的领域边界和依赖方向
2.**职责分离**:领域服务专注于本领域逻辑,应用服务负责跨域协调
3.**可维护性**:代码结构清晰,易于理解和维护
4.**可扩展性**:新增跨域功能时只需修改应用服务层
5.**业务逻辑**:企业认证信息自动填充功能完整实现
这种设计既满足了业务需求又符合DDD架构规范是一个优秀的架构实现。

View File

@@ -37,12 +37,27 @@ FROM alpine:3.19
# 设置Alpine镜像源 # 设置Alpine镜像源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 安装必要的包 # 安装必要的包(包含 headless Chrome 所需依赖)
RUN apk --no-cache add tzdata curl # - tzdata: 时区
# - curl: 健康检查
# - chromium: 无头浏览器,用于 chromedp 生成 HTML 报告 PDF
# - nss、freetype、harfbuzz、ttf-freefont、font-noto-cjk: 字体及渲染依赖,避免中文/图标丢失和乱码
RUN apk --no-cache add \
tzdata \
curl \
chromium \
nss \
freetype \
harfbuzz \
ttf-freefont \
font-noto-cjk
# 设置时区 # 设置时区
ENV TZ=Asia/Shanghai ENV TZ=Asia/Shanghai
# 为 chromedp 指定默认的 Chrome 路径Alpine 下 chromium 包的可执行文件)
ENV CHROME_BIN=/usr/bin/chromium-browser
# 设置工作目录 # 设置工作目录
WORKDIR /app WORKDIR /app
@@ -50,9 +65,11 @@ WORKDIR /app
COPY --from=builder /app/tyapi-server . COPY --from=builder /app/tyapi-server .
# 复制配置文件 # 复制配置文件
COPY --chown=tyapi:tyapi config.yaml . COPY config.yaml .
COPY --chown=tyapi:tyapi configs/ ./configs/ COPY configs/ ./configs/
# 复制资源文件报告模板、PDF、组件等
COPY resources ./resources
# 暴露端口 # 暴露端口
EXPOSE 8080 EXPOSE 8080

64
Dockerfile.worker Normal file
View File

@@ -0,0 +1,64 @@
# 第一阶段:构建阶段
FROM golang:1.23.4-alpine AS builder
# 设置Go代理和Alpine镜像源
ENV GOPROXY https://goproxy.cn,direct
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 设置工作目录
WORKDIR /app
# 安装必要的包
RUN apk add --no-cache git tzdata
# 复制模块文件
COPY go.mod go.sum ./
# 下载依赖
RUN go mod download
# 复制源代码
COPY . .
# 构建应用程序
ARG VERSION=1.0.0
ARG COMMIT=dev
ARG BUILD_TIME
RUN BUILD_TIME=${BUILD_TIME:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")} && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_TIME} -w -s" \
-a -installsuffix cgo \
-o worker \
cmd/worker/main.go
# 第二阶段:运行阶段
FROM alpine:3.19
# 设置Alpine镜像源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 安装必要的包
RUN apk --no-cache add tzdata curl
# 设置时区
ENV TZ=Asia/Shanghai
# 设置工作目录
WORKDIR /app
# 从构建阶段复制二进制文件
COPY --from=builder /app/worker .
# 复制配置文件
COPY --chown=tyapi:tyapi config.yaml .
COPY --chown=tyapi:tyapi configs/ ./configs/
# 暴露端口(如果需要)
# EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 启动命令
CMD ["./worker", "-env=production"]

View File

@@ -1,171 +0,0 @@
# Handler请求绑定方式更新总结
## 概述
根据用户要求将handler中的请求体参数绑定方式从`ShouldBindJSON`统一更新为使用`h.validator.BindAndValidate`,以保持代码风格的一致性和更好的验证处理。
## 主要变更
### 1. 更新的文件
#### `internal/infrastructure/http/handlers/finance_handler.go`
**更新的方法:**
1. **ApplyInvoice** - 申请开票
```go
// 更新前
if err := c.ShouldBindJSON(&req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
// 更新后
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
```
2. **UpdateUserInvoiceInfo** - 更新用户发票信息
```go
// 更新前
if err := c.ShouldBindJSON(&req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
// 更新后
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
```
3. **RejectInvoiceApplication** - 拒绝发票申请
```go
// 更新前
if err := c.ShouldBindJSON(&req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
// 更新后
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
```
#### `internal/infrastructure/http/handlers/api_handler.go`
**更新的方法:**
1. **AddWhiteListIP** - 添加白名单IP
```go
// 更新前
if err := c.ShouldBindJSON(&req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
// 更新后
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
```
### 2. 保持不变的文件
#### `internal/shared/validator/validator.go`
- `BindAndValidate`方法内部仍然使用`c.ShouldBindJSON(dto)`
- 这是正确的因为validator需要先绑定JSON数据然后再进行验证
- 这是validator的实现细节不需要修改
## 技术优势
### 1. 统一的验证处理
- 所有handler都使用相同的验证方式
- 统一的错误处理和响应格式
- 更好的代码一致性
### 2. 更好的错误信息
- `BindAndValidate`提供了更详细的验证错误信息
- 支持中文字段名显示
- 更友好的用户错误提示
### 3. 验证规则支持
- 支持自定义验证规则
- 支持字段翻译
- 支持复杂的业务验证逻辑
### 4. 代码维护性
- 统一的验证逻辑
- 便于后续验证规则的修改
- 减少重复代码
## 验证流程
### 1. 使用`h.validator.BindAndValidate`的流程
```
请求到达 → BindAndValidate → JSON绑定 → 结构体验证 → 返回结果
```
### 2. 错误处理流程
```
验证失败 → 格式化错误信息 → 返回BadRequest响应 → 前端显示错误
```
## 验证器功能
### 1. 支持的验证标签
- `required` - 必填字段
- `email` - 邮箱格式
- `min/max` - 长度限制
- `phone` - 手机号格式
- `strong_password` - 强密码
- `social_credit_code` - 统一社会信用代码
- `id_card` - 身份证号
- `price` - 价格格式
- `uuid` - UUID格式
- `url` - URL格式
- 等等...
### 2. 错误信息本地化
- 支持中文字段名
- 支持中文错误消息
- 支持自定义错误消息
## 兼容性
### 1. API接口保持不变
- 请求和响应格式完全一致
- 前端调用方式无需修改
- 向后兼容
### 2. 错误响应格式
- 保持原有的错误响应结构
- 错误信息更加详细和友好
- 支持字段级别的错误信息
## 后续建议
### 1. 代码审查
- 检查其他handler文件是否还有使用`ShouldBindJSON`的地方
- 确保所有新的handler都使用`BindAndValidate`
### 2. 测试验证
- 验证所有API接口的请求绑定是否正常工作
- 测试各种验证错误场景
- 确保错误信息显示正确
### 3. 文档更新
- 更新开发指南,说明使用`BindAndValidate`的最佳实践
- 更新API文档说明验证规则和错误格式
## 总结
通过这次更新我们成功统一了handler中的请求绑定方式使用`h.validator.BindAndValidate`替代了`ShouldBindJSON`。这样的更改带来了更好的代码一致性、更友好的错误处理和更强的验证能力同时保持了API的向后兼容性。
所有更改都经过了编译测试,确保没有引入任何错误。这为后续的开发工作奠定了良好的基础。

View File

@@ -1,209 +0,0 @@
# 发票功能外部服务集成 TODO
## 1. 短信服务集成
### 位置
- `tyapi-server-gin/internal/domains/finance/services/invoice_aggregate_service_impl.go`
- `tyapi-server-gin/internal/domains/finance/events/invoice_events.go`
### 需要实现的功能
- [ ] 发票申请创建时发送短信通知管理员
- [ ] 配置管理员手机号
- [ ] 短信内容模板
### 示例代码
```go
// 在 InvoiceAggregateServiceImpl 中注入短信服务
type InvoiceAggregateServiceImpl struct {
// ... 其他依赖
smsService SMSService
}
// 在事件处理器中发送短信
func (s *InvoiceEventHandler) HandleInvoiceApplicationCreated(ctx context.Context, event *events.InvoiceApplicationCreatedEvent) error {
// TODO: 发送短信通知管理员
// message := fmt.Sprintf("新的发票申请:用户%s申请开票%.2f元", event.UserID, event.Amount)
// return s.smsService.SendSMS(ctx, adminPhone, message)
return nil
}
```
## 2. 邮件服务集成
### 位置
- `tyapi-server-gin/internal/domains/finance/services/invoice_aggregate_service_impl.go`
- `tyapi-server-gin/internal/domains/finance/events/invoice_events.go`
### 需要实现的功能
- [ ] 发票文件上传后发送邮件给用户
- [ ] 发票申请被拒绝时发送邮件通知用户
- [ ] 邮件模板设计
### 示例代码
```go
// 在 SendInvoiceToEmail 方法中
func (s *InvoiceAggregateServiceImpl) SendInvoiceToEmail(ctx context.Context, invoiceID string) error {
// ... 获取发票信息
// TODO: 调用邮件服务发送发票
// emailData := &EmailData{
// To: invoice.ReceivingEmail,
// Subject: "您的发票已开具",
// Template: "invoice_issued",
// Data: map[string]interface{}{
// "CompanyName": invoice.CompanyName,
// "Amount": invoice.Amount,
// "FileURL": invoice.FileURL,
// },
// }
// return s.emailService.SendEmail(ctx, emailData)
return nil
}
```
## 3. 文件存储服务集成
### 位置
- `tyapi-server-gin/internal/domains/finance/services/invoice_aggregate_service_impl.go`
### 需要实现的功能
- [ ] 上传发票PDF文件
- [ ] 生成文件访问URL
- [ ] 文件存储配置
### 示例代码
```go
// 在 UploadInvoiceFile 方法中
func (s *InvoiceAggregateServiceImpl) UploadInvoiceFile(ctx context.Context, invoiceID string, file multipart.File) error {
// ... 获取发票信息
// TODO: 调用文件存储服务上传文件
// uploadResult, err := s.fileStorageService.UploadFile(ctx, &UploadRequest{
// File: file,
// Path: fmt.Sprintf("invoices/%s", invoiceID),
// Filename: fmt.Sprintf("invoice_%s.pdf", invoiceID),
// })
// if err != nil {
// return fmt.Errorf("上传文件失败: %w", err)
// }
// invoice.SetFileInfo(uploadResult.FileID, uploadResult.FileName, uploadResult.FileURL, uploadResult.FileSize)
return nil
}
```
## 4. 事件处理器实现
### 需要创建的文件
- `tyapi-server-gin/internal/domains/finance/events/invoice_event_handler.go`
- `tyapi-server-gin/internal/domains/finance/events/invoice_event_publisher.go`
### 服务文件结构
- `tyapi-server-gin/internal/domains/finance/services/invoice_domain_service.go` - 领域服务(接口+实现)
- `tyapi-server-gin/internal/domains/finance/services/invoice_aggregate_service.go` - 聚合服务(接口+实现)
### 需要实现的功能
- [ ] 事件发布器实现
- [ ] 事件处理器实现
- [ ] 事件订阅配置
### 示例代码
```go
// 事件发布器实现
type InvoiceEventPublisherImpl struct {
// 可以使用消息队列、Redis发布订阅等
}
// 事件处理器实现
type InvoiceEventHandlerImpl struct {
smsService SMSService
emailService EmailService
}
func (h *InvoiceEventHandlerImpl) HandleInvoiceApplicationCreated(ctx context.Context, event *events.InvoiceApplicationCreatedEvent) error {
// 发送短信通知管理员
return h.smsService.SendSMS(ctx, adminPhone, "新的发票申请")
}
func (h *InvoiceEventHandlerImpl) HandleInvoiceFileUploaded(ctx context.Context, event *events.InvoiceFileUploadedEvent) error {
// 发送邮件给用户
return h.emailService.SendInvoiceEmail(ctx, event.ReceivingEmail, event.FileURL, event.FileName)
}
```
## 5. 数据库迁移
### 需要创建的表
- [ ] `invoice_applications` - 发票申请表(包含文件信息)
### 迁移文件位置
- `tyapi-server-gin/migrations/`
## 6. 仓储实现
### 需要实现的文件
- [ ] `tyapi-server-gin/internal/infrastructure/database/repositories/invoice_application_repository_impl.go`
## 7. HTTP接口实现
### 已完成的文件
- [x] `tyapi-server-gin/internal/infrastructure/http/handlers/invoice_handler.go` - 用户发票处理器
- [x] `tyapi-server-gin/internal/infrastructure/http/handlers/admin_invoice_handler.go` - 管理员发票处理器
- [x] `tyapi-server-gin/internal/infrastructure/http/routes/invoice_routes.go` - 发票路由配置
- [x] `tyapi-server-gin/docs/发票API接口文档.md` - API接口文档
### 用户接口
- [x] 申请开票 `POST /api/v1/invoices/apply`
- [x] 获取用户发票信息 `GET /api/v1/invoices/info`
- [x] 更新用户发票信息 `PUT /api/v1/invoices/info`
- [x] 获取用户开票记录 `GET /api/v1/invoices/records`
- [x] 获取可开票金额 `GET /api/v1/invoices/available-amount`
- [x] 下载发票文件 `GET /api/v1/invoices/{application_id}/download`
### 管理员接口
- [x] 获取待处理申请列表 `GET /api/v1/admin/invoices/pending`
- [x] 通过发票申请 `POST /api/v1/admin/invoices/{application_id}/approve`
- [x] 拒绝发票申请 `POST /api/v1/admin/invoices/{application_id}/reject`
## 8. 依赖注入配置
### 已完成的文件
- [x] `tyapi-server-gin/internal/infrastructure/http/handlers/finance_handler.go` - 合并发票相关handler方法
- [x] `tyapi-server-gin/internal/infrastructure/http/routes/finance_routes.go` - 合并发票相关路由
- [x] 删除多余文件:`invoice_handler.go``admin_invoice_handler.go``invoice_routes.go`
### 已完成的文件
- [x] `tyapi-server-gin/internal/infrastructure/database/repositories/finance/invoice_application_repository_impl.go` - 实现发票申请仓储
- [x] `tyapi-server-gin/internal/application/finance/invoice_application_service.go` - 实现发票应用服务(合并用户端和管理员端)
- [x] `tyapi-server-gin/internal/container/container.go` - 添加发票相关服务的依赖注入
### 已完成的工作
- [x] 删除 `tyapi-server-gin/internal/application/finance/admin_invoice_application_service.go` - 已合并到主服务文件
- [x] 修复 `tyapi-server-gin/internal/application/finance/invoice_application_service.go` - 所有编译错误已修复
- [x] 使用 `*storage.QiNiuStorageService` 替换 `interfaces.StorageService`
- [x] 更新仓储接口以包含所有必要的方法
- [x] 修复DTO字段映射和类型转换
- [x] 修复聚合服务调用参数
## 8. 前端页面
### 需要创建的前端页面
- [ ] 发票申请页面
- [ ] 发票信息编辑页面
- [ ] 发票记录列表页面
- [ ] 管理员发票申请处理页面
## 优先级
1. **高优先级**: 数据库迁移、仓储实现、依赖注入配置
2. **中优先级**: 事件处理器实现、基础API接口
3. **低优先级**: 外部服务集成(短信、邮件、文件存储)
## 注意事项
- 所有外部服务调用都应该有适当的错误处理和重试机制
- 事件发布失败不应该影响主业务流程
- 文件上传需要验证文件类型和大小
- 邮件发送需要支持模板和国际化

View File

@@ -0,0 +1,79 @@
// 将 raw fixture各处理器原始 JSON经 BuildReportFromRawSources 转化为与线上一致的完整报告 JSON。
//
// go run ./cmd/qygl_report_build -in resources/dev-report/fixture.raw.example.json -out resources/dev-report/built.json
// go run ./cmd/qygl_report_build -in raw.json -out - # 输出到 stdout
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"tyapi-server/internal/domains/api/services/processors/qygl"
)
type rawBundle struct {
Kind string `json:"kind"`
JiguangFull map[string]interface{} `json:"jiguangFull"`
JudicialCertFull map[string]interface{} `json:"judicialCertFull"`
EquityPanorama map[string]interface{} `json:"equityPanorama"`
AnnualReport map[string]interface{} `json:"annualReport"`
TaxViolation map[string]interface{} `json:"taxViolation"`
TaxArrears map[string]interface{} `json:"taxArrears"`
CustomsCredit map[string]interface{} `json:"customsCredit"`
}
func main() {
inPath := flag.String("in", "", "raw fixture JSON 路径(含 jiguangFull 等字段,可参考 fixture.raw.example.json")
outPath := flag.String("out", "", "输出文件;- 或留空表示输出到 stdout")
flag.Parse()
if *inPath == "" {
log.Fatal("请指定 -in <raw.json>")
}
raw, err := os.ReadFile(*inPath)
if err != nil {
log.Fatalf("读取输入失败: %v", err)
}
var b rawBundle
if err := json.Unmarshal(raw, &b); err != nil {
log.Fatalf("解析 JSON 失败: %v", err)
}
if b.Kind == "full" {
log.Fatal("输入为 kind=full已是 build 结果),无需再转化;预览请用: go run ./cmd/qygl_report_preview")
}
if b.Kind != "" && b.Kind != "raw" {
log.Fatalf("若填写 kind仅支持 raw当前: %q", b.Kind)
}
report := qygl.BuildReportFromRawSources(
b.JiguangFull,
b.JudicialCertFull,
b.EquityPanorama,
b.AnnualReport,
b.TaxViolation,
b.TaxArrears,
b.CustomsCredit,
)
out, err := json.MarshalIndent(report, "", " ")
if err != nil {
log.Fatalf("序列化报告失败: %v", err)
}
if *outPath == "" || *outPath == "-" {
if _, err := os.Stdout.Write(append(out, '\n')); err != nil {
log.Fatal(err)
}
return
}
if err := os.WriteFile(*outPath, append(out, '\n'), 0644); err != nil {
log.Fatalf("写入失败: %v", err)
}
fmt.Fprintf(os.Stderr, "已写入 %s\n", *outPath)
}

View File

@@ -0,0 +1,60 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"go.uber.org/zap"
"tyapi-server/internal/shared/pdf"
)
// 一个本地调试用的小工具:
// 从 JSON 文件(企业报告.json读取 QYGL 聚合结果,使用 gofpdf 生成企业全景报告 PDF输出到当前目录。
func main() {
var (
jsonPath string
outPath string
)
flag.StringVar(&jsonPath, "json", "企业报告.json", "企业报告 JSON 数据源文件路径")
flag.StringVar(&outPath, "out", "企业全景报告_gofpdf.pdf", "输出 PDF 文件路径")
flag.Parse()
logger, _ := zap.NewDevelopment()
defer logger.Sync()
absJSON, _ := filepath.Abs(jsonPath)
fmt.Printf("读取 JSON 数据源:%s\n", absJSON)
data, err := os.ReadFile(jsonPath)
if err != nil {
fmt.Printf("读取 JSON 文件失败: %v\n", err)
os.Exit(1)
}
var report map[string]interface{}
if err := json.Unmarshal(data, &report); err != nil {
fmt.Printf("解析 JSON 失败: %v\n", err)
os.Exit(1)
}
fmt.Println("开始使用 gofpdf 生成企业全景报告 PDF...")
pdfBytes, err := pdf.GenerateQYGLReportPDF(context.Background(), logger, report)
if err != nil {
fmt.Printf("生成 PDF 失败: %v\n", err)
os.Exit(1)
}
if err := os.WriteFile(outPath, pdfBytes, 0644); err != nil {
fmt.Printf("写入 PDF 文件失败: %v\n", err)
os.Exit(1)
}
absOut, _ := filepath.Abs(outPath)
fmt.Printf("PDF 生成完成:%s\n", absOut)
}

View File

@@ -0,0 +1,159 @@
// 仅读取 build 后的报告 JSON本地渲染 qiye.html不执行 BuildReportFromRawSources
//
// go run ./cmd/qygl_report_preview -in resources/dev-report/built.json
// go run ./cmd/qygl_report_preview -in built.json -addr :8899 -watch
//
// 每次打开/刷新页面都会重新读取 -in 文件;加 -watch 后保存 JSON 会自动刷新浏览器。
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"html/template"
"log"
"net/http"
"os"
"path/filepath"
)
func parseBuiltReport(data []byte) (map[string]interface{}, error) {
var root map[string]interface{}
if err := json.Unmarshal(data, &root); err != nil {
return nil, err
}
if _, ok := root["jiguangFull"]; ok {
return nil, fmt.Errorf("检测到 raw 字段 jiguangFull请先执行: go run ./cmd/qygl_report_build -in <raw.json> -out built.json")
}
if k, _ := root["kind"].(string); k == "full" {
r, ok := root["report"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("kind=full 时缺少 report 对象")
}
return r, nil
}
if r, ok := root["report"].(map[string]interface{}); ok {
return r, nil
}
if root["entName"] != nil || root["basic"] != nil || root["reportTime"] != nil {
return root, nil
}
return nil, fmt.Errorf("不是有效的 build 后报告(根级应有 entName、basic、reportTime 之一,或 {\"report\":{...}} / kind=full")
}
func fileVersionTag(path string) (string, error) {
st, err := os.Stat(path)
if err != nil {
return "", err
}
return fmt.Sprintf("%d-%d", st.ModTime().UnixNano(), st.Size()), nil
}
func renderPage(tmpl *template.Template, report map[string]interface{}, injectLive bool) ([]byte, error) {
reportBytes, err := json.Marshal(report)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, map[string]interface{}{
"ReportJSON": template.JS(reportBytes),
}); err != nil {
return nil, err
}
b := buf.Bytes()
if !injectLive {
return b, nil
}
script := `<script>(function(){var v0=null;function tick(){fetch("/__version?="+Date.now(),{cache:"no-store"}).then(function(r){return r.text();}).then(function(v){if(v==="")return;if(v0===null)v0=v;else if(v0!==v){v0=v;location.reload();}}).catch(function(){});}setInterval(tick,600);tick();})();</script>`
closing := []byte("</body>")
idx := bytes.LastIndex(b, closing)
if idx < 0 {
return append(b, []byte(script)...), nil
}
out := make([]byte, 0, len(b)+len(script))
out = append(out, b[:idx]...)
out = append(out, script...)
out = append(out, b[idx:]...)
return out, nil
}
func main() {
addr := flag.String("addr", ":8899", "监听地址")
root := flag.String("root", ".", "项目根目录(含 resources/qiye.html")
inPath := flag.String("in", "", "build 后的 JSON由 qygl_report_build 生成,或 fixture.full 中的 report 形态)")
watch := flag.Bool("watch", false, "监听 -in 文件变化并自动刷新浏览器(轮询)")
flag.Parse()
if *inPath == "" {
log.Fatal("请指定 -in <built.json>")
}
rootAbs, err := filepath.Abs(*root)
if err != nil {
log.Fatalf("解析 root: %v", err)
}
tplPath := filepath.Join(rootAbs, "resources", "qiye.html")
if _, err := os.Stat(tplPath); err != nil {
log.Fatalf("未找到模板 %s: %v", tplPath, err)
}
var inAbs string
if filepath.IsAbs(*inPath) {
inAbs = *inPath
} else {
inAbs = filepath.Join(rootAbs, *inPath)
}
if _, err := os.Stat(inAbs); err != nil {
log.Fatalf("读取 %s: %v", inAbs, err)
}
tmpl, err := template.ParseFiles(tplPath)
if err != nil {
log.Fatalf("解析模板: %v", err)
}
http.HandleFunc("/__version", func(w http.ResponseWriter, r *http.Request) {
tag, err := fileVersionTag(inAbs)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte(tag))
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
raw, err := os.ReadFile(inAbs)
if err != nil {
http.Error(w, "读取报告文件失败: "+err.Error(), http.StatusInternalServerError)
return
}
report, err := parseBuiltReport(raw)
if err != nil {
http.Error(w, "解析 JSON 失败: "+err.Error(), http.StatusInternalServerError)
return
}
html, err := renderPage(tmpl, report, *watch)
if err != nil {
http.Error(w, "渲染失败: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(html)
})
log.Printf("报告预览: http://127.0.0.1%s/ (每请求重读 %s", *addr, inAbs)
if *watch {
log.Printf("已启用 -watch保存 JSON 后约 0.6s 内自动刷新页面")
}
if err := http.ListenAndServe(*addr, nil); err != nil {
log.Fatal(err)
}
}

193
cmd/worker/main.go Normal file
View File

@@ -0,0 +1,193 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"tyapi-server/internal/config"
"tyapi-server/internal/domains/article/entities"
"tyapi-server/internal/infrastructure/database"
"github.com/hibiken/asynq"
"go.uber.org/zap"
"gorm.io/gorm"
)
const (
TaskTypeArticlePublish = "article:publish"
TaskTypeAnnouncementPublish = "announcement_publish"
)
func main() {
// 加载配置
cfg, err := config.LoadConfig()
if err != nil {
log.Fatal("加载配置失败:", err)
}
// 创建日志器
logger, err := zap.NewProduction()
if err != nil {
log.Fatal("创建日志器失败:", err)
}
defer logger.Sync()
// 连接数据库
// 使用配置文件中的数据库配置
dbCfg := database.Config{
Host: cfg.Database.Host,
Port: cfg.Database.Port,
User: cfg.Database.User,
Password: cfg.Database.Password,
Name: cfg.Database.Name,
SSLMode: cfg.Database.SSLMode,
Timezone: cfg.Database.Timezone,
MaxOpenConns: cfg.Database.MaxOpenConns,
MaxIdleConns: cfg.Database.MaxIdleConns,
ConnMaxLifetime: cfg.Database.ConnMaxLifetime,
}
logger.Info("数据库配置", zap.Any("dbCfg", dbCfg))
dbWrapper, err := database.NewConnection(dbCfg)
if err != nil {
logger.Fatal("连接数据库失败", zap.Error(err))
}
db := dbWrapper.DB
// 使用配置文件中的Redis配置
redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
// 创建 Asynq Server
server := asynq.NewServer(
asynq.RedisClientOpt{Addr: redisAddr},
asynq.Config{
Concurrency: 10,
Queues: map[string]int{
"critical": 6,
"default": 3,
"low": 1,
},
},
)
// 创建任务处理器
mux := asynq.NewServeMux()
mux.HandleFunc(TaskTypeArticlePublish, func(ctx context.Context, t *asynq.Task) error {
return handleArticlePublish(ctx, t, db, logger)
})
mux.HandleFunc(TaskTypeAnnouncementPublish, func(ctx context.Context, t *asynq.Task) error {
return handleAnnouncementPublish(ctx, t, db, logger)
})
// 启动 Worker
go func() {
logger.Info("启动 Asynq Worker", zap.String("redis_addr", redisAddr))
if err := server.Run(mux); err != nil {
logger.Fatal("启动 Worker 失败", zap.Error(err))
}
}()
// 等待信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// 优雅关闭
logger.Info("正在关闭 Worker...")
server.Stop()
server.Shutdown()
logger.Info("Worker 已关闭")
}
// handleArticlePublish 处理文章定时发布任务
func handleArticlePublish(ctx context.Context, t *asynq.Task, db *gorm.DB, logger *zap.Logger) error {
var payload map[string]interface{}
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
logger.Error("解析任务载荷失败", zap.Error(err))
return fmt.Errorf("解析任务载荷失败: %w", err)
}
articleID, ok := payload["article_id"].(string)
if !ok {
logger.Error("任务载荷中缺少文章ID")
return fmt.Errorf("任务载荷中缺少文章ID")
}
// 获取文章
var article entities.Article
if err := db.WithContext(ctx).First(&article, "id = ?", articleID).Error; err != nil {
logger.Error("获取文章失败", zap.String("article_id", articleID), zap.Error(err))
return fmt.Errorf("获取文章失败: %w", err)
}
// 发布文章
if err := article.Publish(); err != nil {
logger.Error("发布文章失败", zap.String("article_id", articleID), zap.Error(err))
return fmt.Errorf("发布文章失败: %w", err)
}
// 保存更新
if err := db.WithContext(ctx).Save(&article).Error; err != nil {
logger.Error("保存文章失败", zap.String("article_id", articleID), zap.Error(err))
return fmt.Errorf("保存文章失败: %w", err)
}
logger.Info("定时发布文章成功", zap.String("article_id", articleID))
return nil
}
// handleAnnouncementPublish 处理公告定时发布任务
func handleAnnouncementPublish(ctx context.Context, t *asynq.Task, db *gorm.DB, logger *zap.Logger) error {
var payload map[string]interface{}
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
logger.Error("解析任务载荷失败", zap.Error(err))
return fmt.Errorf("解析任务载荷失败: %w", err)
}
announcementID, ok := payload["announcement_id"].(string)
if !ok {
logger.Error("任务载荷中缺少公告ID")
return fmt.Errorf("任务载荷中缺少公告ID")
}
// 获取公告
var announcement entities.Announcement
if err := db.WithContext(ctx).First(&announcement, "id = ?", announcementID).Error; err != nil {
logger.Error("获取公告失败", zap.String("announcement_id", announcementID), zap.Error(err))
return fmt.Errorf("获取公告失败: %w", err)
}
// 检查是否已取消定时发布
if !announcement.IsScheduled() {
logger.Info("公告定时发布已取消,跳过执行",
zap.String("announcement_id", announcementID),
zap.String("status", string(announcement.Status)))
return nil // 静默返回,不报错
}
// 检查定时发布时间是否匹配
if announcement.ScheduledAt == nil {
logger.Info("公告没有定时发布时间,跳过执行",
zap.String("announcement_id", announcementID))
return nil
}
// 发布公告
if err := announcement.Publish(); err != nil {
logger.Error("发布公告失败", zap.String("announcement_id", announcementID), zap.Error(err))
return fmt.Errorf("发布公告失败: %w", err)
}
// 保存更新
if err := db.WithContext(ctx).Save(&announcement).Error; err != nil {
logger.Error("保存公告失败", zap.String("announcement_id", announcementID), zap.Error(err))
return fmt.Errorf("保存公告失败: %w", err)
}
logger.Info("定时发布公告成功", zap.String("announcement_id", announcementID))
return nil
}

View File

@@ -5,6 +5,8 @@ app:
name: "TYAPI Server" name: "TYAPI Server"
version: "1.0.0" version: "1.0.0"
env: "development" env: "development"
# 子账号入口与主站可同域;邀请链接 {sub_portal_base_url}/sub/auth/register?invite=...
sub_portal_base_url: "http://localhost:5173/"
server: server:
host: "0.0.0.0" host: "0.0.0.0"
@@ -22,8 +24,8 @@ database:
name: "tyapi_dev" name: "tyapi_dev"
sslmode: "disable" sslmode: "disable"
timezone: "Asia/Shanghai" timezone: "Asia/Shanghai"
max_open_conns: 25 max_open_conns: 50
max_idle_conns: 10 max_idle_conns: 20
conn_max_lifetime: 300s conn_max_lifetime: 300s
auto_migrate: true auto_migrate: true
@@ -44,17 +46,73 @@ cache:
cleanup_interval: 600s cleanup_interval: 600s
max_size: 1000 max_size: 1000
# 🚀 日志系统配置 - 基于 Zap 官方推荐
logger: logger:
level: "info" # 基础配置
format: "console" level: "info" # 日志级别: debug, info, warn, error, fatal, panic
output: "file" format: "json" # 输出格式: json, console
log_dir: "logs" output: "file" # 输出方式: stdout, stderr, file
max_size: 100 log_dir: "logs" # 日志目录
use_daily: true # 是否按日分包
use_color: false # 是否使用彩色输出仅console格式有效
# 文件配置
max_size: 100 # 单个文件最大大小(MB)
max_backups: 5 # 最大备份文件数
max_age: 30 # 最大保留天数
compress: true # 是否压缩
# 高级功能
enable_level_separation: true # 是否启用按级别分文件
enable_request_logging: true # 是否启用请求日志
enable_performance_log: true # 是否启用性能日志
# 开发环境配置
development: true # 是否为开发环境
sampling: false # 是否启用采样
# 各级别配置(按级别分文件时使用)
level_configs:
debug:
max_size: 50 # 50MB
max_backups: 3 max_backups: 3
max_age: 7 max_age: 7 # 7天
compress: true compress: true
use_color: true info:
use_daily: false max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
warn:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
error:
max_size: 200 # 200MB
max_backups: 10
max_age: 90 # 90天
compress: true
fatal:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
panic:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
# 全面日志配置
comprehensive_logging:
enable_request_logging: true
enable_response_logging: true
enable_request_body_logging: true # 开发环境记录请求体
enable_error_logging: true
enable_business_logging: true
enable_performance_logging: true
max_body_size: 10240 # 10KB
exclude_paths: ["/health", "/metrics", "/favicon.ico", "/swagger"]
jwt: jwt:
secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW" secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW"
@@ -63,16 +121,25 @@ jwt:
api: api:
domain: "api.tianyuanapi.com" domain: "api.tianyuanapi.com"
# public_base_url: "" # 可选,无尾斜杠;空则按 https://{domain} 推导;环境变量 API_PUBLIC_BASE_URL 优先
sms: sms:
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9" access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65" access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
endpoint_url: "dysmsapi.aliyuncs.com" endpoint_url: "dysmsapi.aliyuncs.com"
sign_name: "天远数据" sign_name: "海南海宇大数据"
template_code: "SMS_474525324" template_code: "SMS_302641455"
code_length: 6 code_length: 6
expire_time: 5m expire_time: 5m
mock_enabled: false mock_enabled: false
# 签名验证配置(用于防止接口被刷)
signature_enabled: true # 是否启用签名验证
signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥)
# 滑块验证码配置
captcha_enabled: true # 是否启用滑块验证码
captcha_secret: "" # 阿里云验证码密钥加密模式时需要可选EKEY
captcha_endpoint: "captcha.cn-shanghai.aliyuncs.com" # 阿里云验证码服务Endpoint
scene_id: "wynt39to" # 阿里云验证码场景ID
rate_limit: rate_limit:
daily_limit: 10 daily_limit: 10
hourly_limit: 5 hourly_limit: 5
@@ -102,16 +169,16 @@ ocr:
secret_key: "your-baidu-secret-key" secret_key: "your-baidu-secret-key"
ratelimit: ratelimit:
requests: 5000 requests: 7500
window: 60s window: 70s
# 每日请求限制配置 # 每日请求限制配置
daily_ratelimit: daily_ratelimit:
max_requests_per_day: 200 # 每日最大请求次数 max_requests_per_day: 300 # 每日最大请求次数
max_requests_per_ip: 10 # 每个IP每日最大请求次数 max_requests_per_ip: 15 # 每个IP每日最大请求次数
key_prefix: "daily_limit" # Redis键前缀 key_prefix: "daily_limit" # Redis键前缀
ttl: 24h # 键过期时间 ttl: 24h # 键过期时间
max_concurrent: 5 # 最大并发请求数 max_concurrent: 8 # 最大并发请求数
# 安全配置 # 安全配置
enable_ip_whitelist: false # 是否启用IP白名单 enable_ip_whitelist: false # 是否启用IP白名单
@@ -125,7 +192,7 @@ daily_ratelimit:
- "0.0.0.0" # 无效IP - "0.0.0.0" # 无效IP
- "255.255.255.255" # 广播IP - "255.255.255.255" # 广播IP
enable_user_agent: true # 是否检查User-Agent enable_user_agent: false # 是否检查User-Agent
blocked_user_agents: # 被阻止的User-Agent blocked_user_agents: # 被阻止的User-Agent
- "bot" # 机器人 - "bot" # 机器人
- "crawler" # 爬虫 - "crawler" # 爬虫
@@ -136,13 +203,14 @@ daily_ratelimit:
- "python" # Python脚本 - "python" # Python脚本
- "java" # Java脚本 - "java" # Java脚本
- "go-http-client" # Go HTTP客户端 - "go-http-client" # Go HTTP客户端
- "LangShen"
enable_referer: true # 是否检查Referer enable_referer: true # 是否检查Referer
allowed_referers: # 允许的Referer allowed_referers: # 允许的Referer
- "https://console.tianyuanapi.com" # 天元API控制台 - "https://console.tianyuanapi.com" # 天元API控制台
- "https://consoletest.tianyuanapi.com" # 天元API测试控制台 - "https://subsole.tianyuanapi.com" # 天元API子账号控制台
enable_proxy_check: true # 是否检查代理 enable_proxy_check: false # 是否检查代理
enable_geo_block: false # 是否启用地理位置阻止 enable_geo_block: false # 是否启用地理位置阻止
blocked_countries: # 被阻止的国家/地区 blocked_countries: # 被阻止的国家/地区
- "XX" # 示例国家代码 - "XX" # 示例国家代码
@@ -171,14 +239,15 @@ development:
debug: true debug: true
enable_profiler: true enable_profiler: true
enable_cors: true enable_cors: true
cors_allowed_origins: "http://localhost:3000,http://localhost:3001" cors_allowed_origins: "http://localhost:5173,https://console.tianyuanapi.com,https://subsole.tianyuanapi.com"
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS" cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With" cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
# 企业微信配置 # 企业微信配置
wechat_work: wechat_work:
webhook_url: "" webhook_url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=649bf737-28ca-4f30-ad5f-cfb65b2af113"
secret: "" secret: ""
# =========================================== # ===========================================
# 📝 e签宝服务配置 # 📝 e签宝服务配置
# =========================================== # ===========================================
@@ -186,7 +255,7 @@ esign:
app_id: "7439073138" app_id: "7439073138"
app_secret: "d76e27fdd169b391e09262a0959dac5c" app_secret: "d76e27fdd169b391e09262a0959dac5c"
server_url: "https://smlopenapi.esign.cn" server_url: "https://smlopenapi.esign.cn"
template_id: "1fd7ed9c6d134d1db7b5af9582633d76" template_id: "6c91bfd5b1bb48c585f5eaceeea893d4"
contract: contract:
name: "天远数据API合作协议" name: "天远数据API合作协议"
expire_days: 7 expire_days: 7
@@ -210,6 +279,8 @@ wallet:
default_credit_limit: 50.00 default_credit_limit: 50.00
min_amount: "100.00" # 生产环境最低充值金额 min_amount: "100.00" # 生产环境最低充值金额
max_amount: "100000.00" # 单次最高充值金额 max_amount: "100000.00" # 单次最高充值金额
recharge_bonus_enabled: true # 是否启用充值赠送,设为 false 时仅展示商务洽谈提示
api_store_recharge_tip: "" # 关闭赠送时展示的提示文案,为空则使用默认文案
# 支付宝充值赠送配置 # 支付宝充值赠送配置
alipay_recharge_bonus: alipay_recharge_bonus:
- recharge_amount: 1000.00 # 充值1000元 - recharge_amount: 1000.00 # 充值1000元
@@ -219,6 +290,12 @@ wallet:
- recharge_amount: 10000.00 # 充值10000元 - recharge_amount: 10000.00 # 充值10000元
bonus_amount: 800.00 # 赠送800元 bonus_amount: 800.00 # 赠送800元
# 余额预警配置
balance_alert:
default_enabled: true # 默认启用余额预警
default_threshold: 200.00 # 默认预警阈值
alert_cooldown_hours: 24 # 预警冷却时间(小时)
# =========================================== # ===========================================
# 🌍 西部数据配置 # 🌍 西部数据配置
# =========================================== # ===========================================
@@ -228,6 +305,32 @@ westdex:
secret_id: "449159" secret_id: "449159"
secret_second_id: "296804" secret_second_id: "296804"
# 西部数据日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "westdex"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# =========================================== # ===========================================
# 🌍 羽山配置 # 🌍 羽山配置
# =========================================== # ===========================================
@@ -236,6 +339,32 @@ yushan:
api_key: "4c566c4a4b543164535455685655316c" api_key: "4c566c4a4b543164535455685655316c"
acct_id: "YSSJ843926726" acct_id: "YSSJ843926726"
# 羽山日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "yushan"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# =========================================== # ===========================================
# 💰 支付宝支付配置 # 💰 支付宝支付配置
# =========================================== # ===========================================
@@ -247,9 +376,32 @@ alipay:
notify_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/callback" notify_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/callback"
return_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/return" return_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/return"
# ===========================================
# 💰 微信支付配置
# ===========================================
Wxpay:
app_id: "wxa581992dc74d860e"
mch_id: "1683589176"
mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D"
mch_apiv3_key: "4g9ff87IJMebGmQOG0IshdMbM97YcBnt"
mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem"
mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800"
mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem"
notify_url: "https://console.tianyuanapi.com/api/v1/pay/wechat/callback"
refund_notify_url: "https://console.tianyuanapi.com/api/v1/wechat/refund_callback"
# 微信小程序配置
WechatMini:
app_id: "wxa581992dc74d860e"
# 微信H5配置
WechatH5:
app_id: "wxa581992dc74d860e"
# =========================================== # ===========================================
# 🔍 天眼查配置 # 🔍 天眼查配置
# =========================================== # ===========================================
tianyancha: tianyancha:
base_url: http://open.api.tianyancha.com/services base_url: http://open.api.tianyancha.com/services
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2 api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
@@ -260,3 +412,335 @@ tianyancha:
alicloud: alicloud:
host: "https://kzidcardv1.market.alicloudapi.com" host: "https://kzidcardv1.market.alicloudapi.com"
app_code: "d55b58829efb41c8aa8e86769cba4844" app_code: "d55b58829efb41c8aa8e86769cba4844"
# ===========================================
# 🔍 智查金控配置
# ===========================================
zhicha:
url: "https://www.zhichajinkong.com/dataMiddle/api/handle"
app_id: "4b78fff61ab8426f"
app_secret: "1128f01b94124ae899c2e9f2b1f37681"
encrypt_key: "af4ca0098e6a202a5c08c413ebd9fd62"
# 智查金控日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "zhicha"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 🌐 木子数据配置
# ===========================================
muzi:
url: "https://carv.m0101.com/magic/carv/pubin/service"
app_id: "713014138179585"
app_secret: "bd4090ac652c404c80e90ebbdcd6ba1d"
timeout: 60s
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "muzi"
use_daily: true
enable_level_separation: true
level_configs:
info:
max_size: 50
max_backups: 3
max_age: 7
compress: true
error:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 🎯 行为数据配置
# ===========================================
xingwei:
url: "https://sjztyh.chengdaoji.cn/dataCenterManageApi/manage/interface/doc/api/handle"
api_id: "jGtqla2FQv1zuXuH"
api_key: "iR1qS9725N4JA70gwlwohqT3ogl2zBf3"
# 行为数据日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "xingwei"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# ✨ 极光配置
# ===========================================
jiguang:
url: "http://api.jiguangcloud.com/jg-open-api-gateway/api"
app_id: "66ZA28w5" # 请替换为实际的 appId
app_secret: "e5261d0f6f003ae7b9fc1b0255b21761bb618d56" # 请替换为实际的 appSecret
sign_method: "hmac" # 签名方法md5 或 hmac默认 hmac
timeout: 60s # 请求超时时间,默认 60 秒
# 极光日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "jiguang"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 📄 PDF生成服务配置
# ===========================================
pdfgen:
# 服务地址配置
development_url: "http://pdfg.tianyuanapi.com" # 开发环境服务地址
production_url: "http://1.117.67.95:15990" # 生产环境服务地址
# API路径配置
api_path: "/api/v1/generate/guangzhou" # PDF生成API路径
# 超时配置
timeout: 120s # 请求超时时间120秒
# 缓存配置
cache:
ttl: 24h # 缓存过期时间24小时
cache_dir: "" # 缓存目录(空则使用默认目录)
max_size: 0 # 最大缓存大小0表示不限制单位字节
# ===========================================
# ✨ 数脉配置走实时接口
# ===========================================
shumai:
url: "https://api.shumaidata.com"
app_id: "pIfqx8MsoTOjhbB762qi5BfkjJ4D7w0O"
app_secret: "BnJWo61hUgNEa5fqBCueiT1IZ1e0DxPU"
# ===========================================
# ✨ 数脉子账号配置走政务
# ===========================================
# 走政务接口使用这个
app_id2: "AwZZRzWkArtFDO2lDcT2jHfuoo9n35Tq"
app_secret2: "nCXN6fKLImjfvzI12hj8O1CMl1gJeaWh"
sign_method: "md5" # 签名方法md5 或 hmac默认 hmac
timeout: 60s # 请求超时时间,默认 60 秒
# 数脉日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "shumai"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# ✨ 数据宝配置走实时接口
# ===========================================
shujubao:
url: "https://api.chinadatapay.com"
app_secret: "iOk0ALBX0BSdTSTf"
sign_method: "md5" # 签名方法md5 或 hmac默认 hmac
timeout: 60s # 请求超时时间,默认 60 秒
# 数据宝日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "shujubao"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# ✨ 汇博BHSC配置
# ===========================================
huibo:
url: "http://47.111.187.101:12654/api/v1/project-api/bg_check_ssw"
app_id: "db0029527bb4558c"
app_key: "a6c9935e967894e731c62ecfcd9b7c95"
x_order_code: "cpdd219219725093"
secret_id: "cf581fe84aaf46ca"
aes_key: "NQYN3YO+pb/GEcCBNX0ptMb7cUlnXSPvcX7VvNofBkc="
work_order_code: "gd219219725093"
product_code: "22089"
baseUrl2: "https://napi.zhixin.net:9000/api/data"
app_code2: "1508795945301708800"
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "huibo"
use_daily: true
enable_level_separation: true
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 🌐 诺尔智汇配置
# ===========================================
nuoer:
url: "https://api.enolfax.com/enol/api"
app_id: "t4qO2mR3"
app_secret: "d1515bf9ed2f2fe063b5f4f7e2c50f0ec65bfd58"
timeout: 4s
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "nuoer"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 🌐 海宇API上游数据源配置
# ===========================================
haiyuapi:
base_url: "https://api.haiyudata.com"
access_id: "2c136588d34b47fd" # 请求头 Access-Id
secret_key: "39c5c2c02ff13bf2be0ac0b705e21e28" # Access Key16进制 AES-128 密钥)
timeout: 60s
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "haiyuapi"
use_daily: true
enable_level_separation: true
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true

View File

@@ -6,6 +6,8 @@
# =========================================== # ===========================================
app: app:
env: development env: development
# 子账号专属前端域名(用于邀请链接复制)
sub_portal_base_url: "http://localhost:5173"
# =========================================== # ===========================================
# 🗄️ 数据库配置 # 🗄️ 数据库配置
@@ -15,24 +17,15 @@ database:
name: "tyapi_dev" name: "tyapi_dev"
# =========================================== # ===========================================
# 📝 日志配置
# ===========================================
logger:
level: info
format: json
output: "console"
log_dir: "logs"
max_size: 100
max_backups: 5
max_age: 30
compress: true
use_daily: true
# ===========================================
# 🔐 JWT配置 # 🔐 JWT配置
# =========================================== # ===========================================
jwt: jwt:
secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW
# 本地联调:企业报告链接与 headless PDF 需能访问到本机服务;端口与 server 监听一致。环境变量 API_PUBLIC_BASE_URL 可覆盖。
api:
public_base_url: "http://127.0.0.1:8080"
# =========================================== # ===========================================
# 📁 存储服务配置 - 七牛云 # 📁 存储服务配置 - 七牛云
# =========================================== # ===========================================
@@ -53,10 +46,10 @@ ocr:
# 📝 e签宝服务配置 # 📝 e签宝服务配置
# =========================================== # ===========================================
esign: esign:
app_id: "7439073713" app_id: "5112008003"
app_secret: "c7d8cb0d701f7890601d221e9b6edfef" app_secret: "d487672273e7aa70c800804a1d9499b9"
server_url: "https://smlopenapi.esign.cn" server_url: "https://openapi.esign.cn"
template_id: "1fd7ed9c6d134d1db7b5af9582633d76" template_id: "6c91bfd5b1bb48c585f5eaceeea893d4"
contract: contract:
name: "天远数据API合作协议" name: "天远数据API合作协议"
expire_days: 7 expire_days: 7
@@ -80,6 +73,8 @@ westdex:
key: "121a1e41fc1690dd6b90afbcacd80cf4" key: "121a1e41fc1690dd6b90afbcacd80cf4"
secret_id: "449159" secret_id: "449159"
secret_second_id: "296804" secret_second_id: "296804"
yushan:
url: https://api2.yushanshuju.com/credit-gw/service
# =========================================== # ===========================================
# 💰 支付宝支付配置 # 💰 支付宝支付配置
# =========================================== # ===========================================
@@ -92,12 +87,35 @@ alipay:
return_url: "http://127.0.0.1:8080/api/v1/finance/alipay/return" return_url: "http://127.0.0.1:8080/api/v1/finance/alipay/return"
# =========================================== # ===========================================
# 💰 微信支付配置
# ===========================================
Wxpay:
app_id: "wxa581992dc74d860e"
mch_id: "1683589176"
mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D"
mch_apiv3_key: "4g9ff87IJMebGmQOG0IshdMbM97YcBnt"
mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem"
mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800"
mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem"
notify_url: "https://bx89915628g.vicp.fun/api/v1/pay/wechat/callback"
refund_notify_url: "https://bx89915628g.vicp.fun/api/v1/wechat/refund_callback"
# 微信小程序配置
WechatMini:
app_id: "wxa581992dc74d860e"
# 微信H5配置
WechatH5:
app_id: "wxa581992dc74d860e"
# ===========================================
# 💰 钱包配置 # 💰 钱包配置
# =========================================== # ===========================================
wallet: wallet:
default_credit_limit: 0.01 default_credit_limit: 0.01
min_amount: "0.01" # 生产环境最低充值金额 min_amount: "0.01" # 生产环境最低充值金额
max_amount: "100000.00" # 单次最高充值金额 max_amount: "100000.00" # 单次最高充值金额
recharge_bonus_enabled: false # 开发环境可设为 true 测试赠送
api_store_recharge_tip: "尊敬的客户,若您的充值金额较大或有批量调价需求,为获取专属商务优惠方案,请直接联系我司商务团队进行洽谈。感谢您的支持!"
# 支付宝充值赠送配置 # 支付宝充值赠送配置
alipay_recharge_bonus: alipay_recharge_bonus:
- recharge_amount: 0.01 # 充值1000元 - recharge_amount: 0.01 # 充值1000元
@@ -113,3 +131,64 @@ wallet:
tianyancha: tianyancha:
base_url: http://open.api.tianyancha.com/services base_url: http://open.api.tianyancha.com/services
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2 api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
# 智查金控配置示例
zhicha:
url: "http://proxy.tianyuanapi.com/dataMiddle/api/handle"
app_id: "4b78fff61ab8426f"
app_secret: "1128f01b94124ae899c2e9f2b1f37681"
encrypt_key: "af4ca0098e6a202a5c08c413ebd9fd62"
development:
enable_cors: true
cors_allowed_origins: "http://localhost:5173,http://localhost:8080"
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
# ===========================================
# 🚦 开发环境全局限流(放宽或近似关闭)
# ===========================================
ratelimit:
requests: 1000000 # 每窗口允许的请求数,足够大,相当于关闭
window: 1s # 时间窗口
burst: 1000000 # 令牌桶突发容量
# ===========================================
# 🚀 开发环境频率限制配置(放宽限制)
# ===========================================
daily_ratelimit:
max_requests_per_day: 1000000 # 开发环境每日最大请求次数
max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数
max_concurrent: 50 # 开发环境最大并发请求数
# 排除频率限制的路径
exclude_paths:
- "/health" # 健康检查接口
- "/metrics" # 监控指标接口
# 排除频率限制的域名
exclude_domains:
- "api.*" # API二级域名不受频率限制
- "*.api.*" # 支持多级API域名
# 开发环境安全配置(放宽限制)
enable_ip_whitelist: true # 启用IP白名单
ip_whitelist: # 开发环境IP白名单
- "127.0.0.1" # 本地回环
- "localhost" # 本地主机
- "192.168.*" # 内网IP段
- "10.*" # 内网IP段
- "172.16.*" # 内网IP段
enable_ip_blacklist: false # 开发环境禁用IP黑名单
enable_user_agent: false # 开发环境禁用User-Agent检查
enable_referer: false # 开发环境禁用Referer检查
enable_proxy_check: false # 开发环境禁用代理检查
# ===========================================
# 📱 短信服务配置
# ===========================================
sms:
# 滑块验证码配置
captcha_enabled: true # 是否启用滑块验证码
captcha_secret: "" # 阿里云验证码密钥(可选)
scene_id: "wynt39to" # 阿里云验证码场景ID

View File

@@ -6,6 +6,8 @@
# =========================================== # ===========================================
app: app:
env: production env: production
# 子账号专属前端域名(用于邀请链接复制)
sub_portal_base_url: "https://subsole.tianyuanapi.com"
# =========================================== # ===========================================
# 🌐 服务器配置 # 🌐 服务器配置
@@ -13,6 +15,15 @@ app:
server: server:
mode: release mode: release
# ===========================================
# 🔒 CORS配置 - 生产环境
# ===========================================
development:
enable_cors: true
cors_allowed_origins: "https://console.tianyuanapi.com,https://subsole.tianyuanapi.com"
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
# =========================================== # ===========================================
# 🗄️ 数据库配置 # 🗄️ 数据库配置
# =========================================== # ===========================================
@@ -34,63 +45,7 @@ redis:
port: "6379" port: "6379"
password: "" password: ""
db: 0 db: 0
# ===========================================
# 📝 日志配置
# ===========================================
logger:
level: info
format: json
output: "file"
log_dir: "/app/logs"
max_size: 100
max_backups: 5
max_age: 30
compress: true
use_daily: true
# 启用按级别分文件
enable_level_separation: true
# 各级别日志文件配置
level_configs:
debug:
max_size: 50 # 50MB
max_backups: 3
max_age: 7 # 7天
compress: true
info:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
warn:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
error:
max_size: 200 # 200MB
max_backups: 10
max_age: 90 # 90天
compress: true
fatal:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
panic:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
# 生产环境全面日志配置
comprehensive_logging:
enable_request_logging: true
enable_response_logging: true
enable_request_body_logging: false # 生产环境不记录请求体(安全考虑)
enable_error_logging: true
enable_business_logging: true
enable_performance_logging: true
max_body_size: 10240 # 10KB
exclude_paths: ["/health", "/metrics", "/favicon.ico", "/swagger"]
# =========================================== # ===========================================
# 🔐 JWT配置 # 🔐 JWT配置
# =========================================== # ===========================================
@@ -99,6 +54,8 @@ jwt:
api: api:
domain: "api.tianyuanapi.com" domain: "api.tianyuanapi.com"
# 可选:对外可访问的 API 完整基址(无尾斜杠),用于企业报告 reportUrl、PDF 预生成等;不设则按 https://{domain} 推导。环境变量 API_PUBLIC_BASE_URL 优先于本项。
# public_base_url: "https://api.tianyuanapi.com"
# =========================================== # ===========================================
# 📁 存储服务配置 - 七牛云 # 📁 存储服务配置 - 七牛云
# =========================================== # ===========================================
@@ -122,7 +79,7 @@ esign:
app_id: "5112008003" app_id: "5112008003"
app_secret: "d487672273e7aa70c800804a1d9499b9" app_secret: "d487672273e7aa70c800804a1d9499b9"
server_url: "https://openapi.esign.cn" server_url: "https://openapi.esign.cn"
template_id: "c82af4df2790430299c81321f309eef3" template_id: "6c91bfd5b1bb48c585f5eaceeea893d4"
contract: contract:
name: "天远数据API合作协议" name: "天远数据API合作协议"
expire_days: 7 expire_days: 7
@@ -139,14 +96,6 @@ esign:
client_type: "ALL" client_type: "ALL"
redirect_url: "https://console.tianyuanapi.com/certification/callback/sign" redirect_url: "https://console.tianyuanapi.com/certification/callback/sign"
# =========================================== # ===========================================
# 🌍 西部数据配置
# ===========================================
westdex:
url: "http://proxy.tianyuanapi.com/api/invoke"
key: "121a1e41fc1690dd6b90afbcacd80cf4"
secret_id: "449159"
secret_second_id: "296804"
# ===========================================
# 💰 支付宝支付配置 # 💰 支付宝支付配置
# =========================================== # ===========================================
alipay: alipay:
@@ -164,7 +113,9 @@ wallet:
default_credit_limit: 50.00 default_credit_limit: 50.00
min_amount: "100.00" # 生产环境最低充值金额 min_amount: "100.00" # 生产环境最低充值金额
max_amount: "100000.00" # 单次最高充值金额 max_amount: "100000.00" # 单次最高充值金额
# 支付宝充值赠送配置 recharge_bonus_enabled: false # 暂不赠送,展示商务洽谈提示
api_store_recharge_tip: "尊敬的客户,若您的充值金额较大或有批量调价需求,为获取专属商务优惠方案,请直接联系我司商务团队进行洽谈。感谢您的支持!"
# 支付宝充值赠送配置recharge_bonus_enabled 为 true 时生效)
alipay_recharge_bonus: alipay_recharge_bonus:
- recharge_amount: 1000.00 # 充值1000元 - recharge_amount: 1000.00 # 充值1000元
bonus_amount: 50.00 # 赠送50元 bonus_amount: 50.00 # 赠送50元
@@ -173,3 +124,52 @@ wallet:
- recharge_amount: 10000.00 # 充值10000元 - recharge_amount: 10000.00 # 充值10000元
bonus_amount: 800.00 # 赠送800元 bonus_amount: 800.00 # 赠送800元
# ===========================================
# 🚦 频率限制配置 - 生产环境
# ===========================================
daily_ratelimit:
max_requests_per_day: 50000 # 生产环境每日最大请求次数
max_requests_per_ip: 5000 # 生产环境每个IP每日最大请求次数
max_concurrent: 200 # 生产环境最大并发请求数
# 排除频率限制的路径
exclude_paths:
- "/health" # 健康检查接口
- "/metrics" # 监控指标接口
# 排除频率限制的域名
exclude_domains:
- "api.*" # API二级域名不受频率限制
- "*.api.*" # 支持多级API域名
# 生产环境安全配置(严格限制)
enable_ip_whitelist: false # 生产环境不启用IP白名单
enable_ip_blacklist: true # 启用IP黑名单
ip_blacklist: # 生产环境IP黑名单
- "192.168.1.100" # 示例被禁止的IP
- "10.0.0.50" # 示例被禁止的IP
enable_user_agent: true # 启用User-Agent检查
blocked_user_agents: # 被阻止的User-Agent
- "curl" # 阻止curl请求
- "wget" # 阻止wget请求
- "python-requests" # 阻止Python requests
- "LangShen" # 阻止LangShen请求
enable_referer: true # 启用Referer检查
allowed_referers: # 允许的Referer
- "https://console.tianyuanapi.com"
- "https://subsole.tianyuanapi.com"
enable_geo_block: false # 生产环境暂时不启用地理位置阻止
enable_proxy_check: true # 启用代理检查
# ===========================================
# 📱 短信服务配置
# ===========================================
sms:
# 滑块验证码配置
captcha_enabled: true # 是否启用滑块验证码
captcha_secret: "" # 阿里云验证码密钥(可选)
scene_id: "wynt39to" # 阿里云验证码场景ID

View File

@@ -37,3 +37,12 @@ logger:
# =========================================== # ===========================================
jwt: jwt:
secret: test-jwt-secret-key-for-testing-only secret: test-jwt-secret-key-for-testing-only
# ===========================================
# 📱 短信服务配置
# ===========================================
sms:
# 滑块验证码配置
captcha_enabled: true # 是否启用滑块验证码
captcha_secret: "" # 阿里云验证码密钥(可选)
scene_id: "wynt39to" # 阿里云验证码场景ID

View File

@@ -1,166 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
// 测试发票申请通过的事件系统
func main() {
fmt.Println("🔍 开始测试发票事件系统...")
// 1. 首先获取待处理的发票申请列表
fmt.Println("\n📋 步骤1: 获取待处理的发票申请列表")
applications, err := getPendingApplications()
if err != nil {
fmt.Printf("❌ 获取申请列表失败: %v\n", err)
return
}
if len(applications) == 0 {
fmt.Println("⚠️ 没有待处理的发票申请")
return
}
// 选择第一个申请进行测试
application := applications[0]
fmt.Printf("✅ 找到申请: ID=%s, 公司=%s, 金额=%s\n",
application["id"], application["company_name"], application["amount"])
// 2. 创建一个测试PDF文件
fmt.Println("\n📄 步骤2: 创建测试PDF文件")
testFile, err := createTestPDF()
if err != nil {
fmt.Printf("❌ 创建测试文件失败: %v\n", err)
return
}
defer os.Remove(testFile)
// 3. 通过发票申请(上传文件)
fmt.Println("\n📤 步骤3: 通过发票申请并上传文件")
err = approveInvoiceApplication(application["id"].(string), testFile)
if err != nil {
fmt.Printf("❌ 通过申请失败: %v\n", err)
return
}
fmt.Println("✅ 发票申请通过成功!")
fmt.Println("📧 请检查日志中的邮件发送情况...")
}
// 获取待处理的发票申请列表
func getPendingApplications() ([]map[string]interface{}, error) {
url := "http://localhost:8080/api/v1/admin/invoices/pending"
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
if result["code"] != float64(200) {
return nil, fmt.Errorf("API返回错误: %s", result["message"])
}
data := result["data"].(map[string]interface{})
applications := data["applications"].([]interface{})
applicationsList := make([]map[string]interface{}, len(applications))
for i, app := range applications {
applicationsList[i] = app.(map[string]interface{})
}
return applicationsList, nil
}
// 创建测试PDF文件
func createTestPDF() (string, error) {
// 创建一个简单的PDF内容这里只是示例
pdfContent := []byte("%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n2 0 obj\n<<\n/Type /Pages\n/Kids [3 0 R]\n/Count 1\n>>\nendobj\n3 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/MediaBox [0 0 612 792]\n/Contents 4 0 R\n>>\nendobj\n4 0 obj\n<<\n/Length 44\n>>\nstream\nBT\n/F1 12 Tf\n72 720 Td\n(Test Invoice) Tj\nET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000204 00000 n \ntrailer\n<<\n/Size 5\n/Root 1 0 R\n>>\nstartxref\n297\n%%EOF\n")
tempFile := filepath.Join(os.TempDir(), "test_invoice.pdf")
err := os.WriteFile(tempFile, pdfContent, 0644)
if err != nil {
return "", err
}
return tempFile, nil
}
// 通过发票申请
func approveInvoiceApplication(applicationID, filePath string) error {
url := fmt.Sprintf("http://localhost:8080/api/v1/admin/invoices/%s/approve", applicationID)
// 创建multipart表单
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 添加文件
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
part, err := writer.CreateFormFile("file", "test_invoice.pdf")
if err != nil {
return err
}
_, err = io.Copy(part, file)
if err != nil {
return err
}
// 添加备注
writer.WriteField("admin_notes", "测试通过 - 调试事件系统")
writer.Close()
// 发送请求
req, err := http.NewRequest("POST", url, &buf)
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return err
}
if result["code"] != float64(200) {
return fmt.Errorf("API返回错误: %s", result["message"])
}
return nil
}

View File

@@ -44,6 +44,23 @@ services:
timeout: 3s timeout: 3s
retries: 5 retries: 5
# Asynq 任务监控
asynq-monitor:
image: hibiken/asynqmon:latest
container_name: tyapi-asynq-monitor
environment:
TZ: Asia/Shanghai
ports:
- "8081:8080"
command: --redis-addr=tyapi-redis:6379
networks:
- tyapi-network
depends_on:
redis:
condition: service_healthy
restart:
unless-stopped
# Jaeger 链路追踪 # Jaeger 链路追踪
jaeger: jaeger:
image: jaegertracing/all-in-one:1.70.0 image: jaegertracing/all-in-one:1.70.0

View File

@@ -20,7 +20,8 @@ services:
networks: networks:
- tyapi-network - tyapi-network
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U tyapi_user -d tyapi -h localhost"] test:
["CMD-SHELL", "pg_isready -U tyapi_user -d tyapi -h localhost"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 5 retries: 5
@@ -68,7 +69,6 @@ services:
# 生产环境不暴露端口到主机 # 生产环境不暴露端口到主机
# ports: # ports:
# - "6379:6379" # - "6379:6379"
# TYAPI 应用程序 # TYAPI 应用程序
tyapi-app: tyapi-app:
build: build:
@@ -89,6 +89,10 @@ services:
- "25000:8080" - "25000:8080"
volumes: volumes:
- ./logs:/app/logs - ./logs:/app/logs
# 挂载完整 resources 目录(包含 qiye.html、Pure_Component、pdf 等)
- ./resources:/app/resources
# 持久化PDF缓存目录确保生成的PDF在容器重启后仍然存在
- ./storage/pdfg-cache:/app/storage/pdfg-cache
# user: "1001:1001" # 注释掉使用root权限运行 # user: "1001:1001" # 注释掉使用root权限运行
networks: networks:
- tyapi-network - tyapi-network
@@ -104,20 +108,86 @@ services:
retries: 5 retries: 5
start_period: 60s start_period: 60s
restart: unless-stopped restart: unless-stopped
# TYAPI Worker 服务
tyapi-worker:
build:
context: .
dockerfile: Dockerfile.worker
args:
VERSION: 1.0.0
COMMIT: dev
BUILD_TIME: ""
container_name: tyapi-worker-prod
environment:
# 时区配置
TZ: Asia/Shanghai
# 环境设置
ENV: production
volumes:
- ./logs:/root/logs
# user: "1001:1001" # 注释掉使用root权限运行
networks:
- tyapi-network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "ps", "aux", "|", "grep", "worker"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
restart: unless-stopped
# Asynq 任务监控 (生产环境)
asynq-monitor:
image: hibiken/asynqmon:latest
container_name: tyapi-asynq-monitor-prod
environment:
TZ: Asia/Shanghai
ports:
- "25080:8080"
command: --redis-addr=tyapi-redis-prod:6379
networks:
- tyapi-network
depends_on:
redis:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:8080/health",
]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
restart: unless-stopped
deploy: deploy:
resources: resources:
limits: limits:
memory: 1G
cpus: "1.0"
reservations:
memory: 256M memory: 256M
cpus: "0.3" cpus: "0.3"
reservations:
memory: 64M
cpus: "0.1"
volumes: volumes:
postgres_data: postgres_data:
driver: local driver: local
redis_data: redis_data:
driver: local driver: local
pure_component:
driver: local
networks: networks:
tyapi-network: tyapi-network:

View File

@@ -0,0 +1,261 @@
# IVYZ9K2L - 身份认证三要素(人脸图像版) WestDex API 文档
## 接口信息
- **接口名称**: 身份认证三要素(人脸图像版)
- **接口代码**: IVYZ9K2L
- **WestDex API Code**: `idCardThreeElements`
- **请求方式**: POST
- **Content-Type**: application/json
## 请求URL
```
https://apimaster.westdex.com.cn/api/invoke/{secret_id}/{api_code}?timestamp={timestamp}
```
### URL 参数说明
| 参数 | 说明 | 示例值 |
|------|------|--------|
| secret_id | 西部数据 SecretID从配置获取 | `449159` |
| api_code | API代码 | `idCardThreeElements` |
| timestamp | 毫秒级时间戳URL参数 | `1713421668375` |
### 完整URL示例
```
https://apimaster.westdex.com.cn/api/invoke/449159/idCardThreeElements?timestamp=1713421668375
```
## 请求头
```
Content-Type: application/json
```
## 请求体
### 请求体结构
```json
{
"data": {
"timeStamp": "1713421668375",
"customNumber": "449159",
"xM": "fU4B3fR3Dw+UkHNkFsHIjA==",
"gMSFZHM": "qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg="
},
"photoData": "Qk3OlwAAAAAAADYAAAAoAAAAZgAAAH4AAAABABgAAA..."
}
```
### 参数说明
#### data 对象(必填)
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|--------|------|------|------|--------|
| timeStamp | string | 是 | 毫秒级时间戳与URL参数中的timestamp一致 | `"1713421668375"` |
| customNumber | string | 是 | 自定义编号,使用配置中的 secret_id | `"449159"` |
| xM | string | 是 | 加密后的姓名使用AES加密密钥为配置中的key | `"fU4B3fR3Dw+UkHNkFsHIjA=="` |
| gMSFZHM | string | 是 | 加密后的身份证号使用AES加密密钥为配置中的key | `"qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg="` |
#### photoData必填
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|--------|------|------|------|--------|
| photoData | string | 是 | Base64编码的人脸图片数据仅支持JPG、BMP、PNG格式 | `"Qk3OlwAAAAAAADYAAAAoAAAAZgAAAH4AAAABABgAAA..."` |
## 加密说明
### 姓名和身份证号加密
使用 AES-ECB 模式加密,密钥为配置中的 `key`(示例:`121a1e41fc1690dd6b90afbcacd80cf4`
**加密步骤**
1. 使用密钥生成 AES 密钥
2. 使用 AES-ECB 模式加密原始数据
3. 将加密结果进行 Base64 编码
**示例**
- 原始姓名:`"张三"`
- 加密后:`"fU4B3fR3Dw+UkHNkFsHIjA=="`
## 完整请求示例
### cURL 示例
```bash
curl -X POST "https://apimaster.westdex.com.cn/api/invoke/449159/idCardThreeElements?timestamp=1713421668375" \
-H "Content-Type: application/json" \
-d '{
"data": {
"timeStamp": "1713421668375",
"customNumber": "449159",
"xM": "fU4B3fR3Dw+UkHNkFsHIjA==",
"gMSFZHM": "qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg="
},
"photoData": "Qk3OlwAAAAAAADYAAAAoAAAAZgAAAH4AAAABABgAAA..."
}'
```
### JavaScript 示例
```javascript
const timestamp = Date.now().toString();
const url = `https://apimaster.westdex.com.cn/api/invoke/449159/idCardThreeElements?timestamp=${timestamp}`;
const requestBody = {
data: {
timeStamp: timestamp,
customNumber: "449159",
xM: "fU4B3fR3Dw+UkHNkFsHIjA==", // 加密后的姓名
gMSFZHM: "qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg=" // 加密后的身份证号
},
photoData: "Qk3OlwAAAAAAADYAAAAoAAAAZgAAAH4AAAABABgAAA..." // Base64图片数据
};
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
```
## 响应格式
### 成功响应
```json
{
"code": "00000",
"message": "成功",
"data": "加密后的响应数据(需要解密)",
"id": "响应ID",
"error_code": null,
"reason": ""
}
```
### 错误响应
```json
{
"code": "错误码",
"message": "错误信息",
"data": "加密后的错误数据(需要解密)",
"id": "响应ID",
"error_code": 错误码,
"reason": "错误原因"
}
```
### 响应状态码说明
| 状态码 | 说明 |
|--------|------|
| `00000` | 成功 |
| `200` | 成功 |
| `0` | 成功 |
| 其他 | 失败 |
## 响应数据解密
响应中的 `data` 字段是加密的,需要使用相同的密钥进行解密:
**解密步骤**
1. 使用配置中的 `key` 作为密钥
2.`data` 字段进行 Base64 解码
3. 使用 AES-ECB 模式解密
4. 得到原始 JSON 字符串
## Apifox 配置步骤
### 1. 创建新请求
- 方法:`POST`
- URL`https://apimaster.westdex.com.cn/api/invoke/449159/idCardThreeElements`
### 2. 设置URL参数
在"Params"标签页添加:
- `timestamp`: `{{$timestamp}}` 使用Apifox变量生成当前时间戳
### 3. 设置请求头
在"Headers"标签页添加:
- `Content-Type`: `application/json`
### 4. 设置请求体
在"Body"标签页选择 `raw` 类型,格式选择 `JSON`,内容如下:
```json
{
"data": {
"timeStamp": "{{$timestamp}}",
"customNumber": "449159",
"xM": "fU4B3fR3Dw+UkHNkFsHIjA==",
"gMSFZHM": "qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg="
},
"photoData": "Qk3OlwAAAAAAADYAAAAoAAAAZgAAAH4AAAABABgAAA..."
}
```
### 5. 配置环境变量(可选)
在Apifox中创建环境变量
- `westdex_secret_id`: `449159`
- `westdex_key`: `121a1e41fc1690dd6b90afbcacd80cf4`
- `westdex_url`: `https://apimaster.westdex.com.cn/api/invoke`
然后在URL中使用`{{westdex_url}}/{{westdex_secret_id}}/idCardThreeElements?timestamp={{$timestamp}}`
### 6. 前置脚本(用于生成时间戳)
在"前置脚本"中添加:
```javascript
// 生成毫秒级时间戳
pm.environment.set("timestamp", Date.now().toString());
```
然后在URL参数和请求体中使用 `{{timestamp}}`
## 注意事项
1. **时间戳同步**URL参数中的 `timestamp` 和请求体 `data.timeStamp` 必须一致
2. **加密密钥**:姓名和身份证号必须使用配置中的 `key` 进行加密
3. **图片格式**`photoData` 必须是纯Base64字符串不包含 `data:image/xxx;base64,` 前缀)
4. **图片格式限制**:仅支持 JPG、BMP、PNG 三种格式
5. **请求超时**建议设置60秒超时时间
6. **响应解密**:成功响应中的 `data` 字段需要解密后才能查看实际内容
## 配置信息
根据项目配置文件,当前使用的配置为:
- **URL**: `https://apimaster.westdex.com.cn/api/invoke`
- **Key**: `121a1e41fc1690dd6b90afbcacd80cf4`
- **SecretID**: `449159`
- **SecretSecondID**: `296804`
## 测试数据示例
### 原始数据
- 姓名:`张三`
- 身份证号:`110101199001011234`
- 人脸图片需要转换为Base64格式
### 加密后的数据(示例)
- 加密姓名:`fU4B3fR3Dw+UkHNkFsHIjA==`
- 加密身份证号:`qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg=`
**注意**:实际加密结果会根据密钥和原始数据不同而变化,以上仅为示例格式。

View File

@@ -0,0 +1,210 @@
# PDF接口文档下载缓存优化说明
## 📋 概述
本次优化为PDF接口文档下载功能添加了本地文件缓存机制显著提升了下载性能减少了重复生成PDF的开销。
## 🔍 问题分析
### 原有问题
1. **性能问题**
- 每次请求都重新生成PDF没有缓存机制
- PDF生成涉及复杂的字体加载、页面构建、表格渲染等操作耗时较长
- 同一产品的PDF被多次下载时会重复执行相同的生成过程
2. **资源浪费**
- CPU资源浪费在重复的PDF生成上
- 数据库查询重复执行
- 没有版本控制,即使产品文档没有变化,也会重新生成
## ✅ 解决方案
### 1. PDF缓存管理器 (`PDFCacheManager`)
创建了专门的PDF缓存管理器提供以下功能
- **本地文件缓存**将生成的PDF文件保存到本地文件系统
- **版本控制**基于产品ID和文档版本号生成缓存键确保版本更新时自动失效
- **自动过期**支持TTLTime To Live机制自动清理过期缓存
- **大小限制**:支持最大缓存大小限制,防止磁盘空间耗尽
- **定期清理**:后台任务每小时自动清理过期文件
### 2. 缓存键生成策略
```go
// 基于产品ID和文档版本号生成唯一的缓存键
cacheKey = MD5(productID + ":" + version)
```
- 当产品文档版本更新时,自动生成新的缓存
- 旧版本的缓存会在过期后自动清理
### 3. 缓存流程
```
请求下载PDF
检查缓存是否存在且有效
├─ 缓存命中 → 直接返回缓存的PDF文件
└─ 缓存未命中 → 生成PDF → 保存到缓存 → 返回PDF
```
### 4. 集成到下载接口
修改了 `DownloadProductDocumentation` 方法:
- **缓存优先**首先尝试从缓存获取PDF
- **异步保存**生成新PDF后异步保存到缓存不阻塞响应
- **缓存标识**:响应头中添加 `X-Cache: HIT/MISS` 标识,便于监控
## 🚀 性能提升
### 预期效果
1. **首次下载**与之前相同需要生成PDF约1-3秒
2. **后续下载**:直接从缓存读取(< 100ms性能提升 **10-30倍**
3. **缓存命中率**对于热门产品缓存命中率可达 **80-90%**
### 响应时间对比
| 场景 | 优化前 | 优化后 | 提升 |
|------|--------|--------|------|
| 首次下载 | 1-3秒 | 1-3秒 | - |
| 缓存命中 | 1-3秒 | < 100ms | **10-30倍** |
| 版本更新后首次 | 1-3秒 | 1-3秒 | - |
## ⚙️ 配置说明
### 环境变量配置
可以通过环境变量自定义缓存配置
```bash
# 缓存目录默认系统临时目录下的tyapi_pdf_cache
export PDF_CACHE_DIR="/path/to/cache"
# 缓存过期时间默认24小时
export PDF_CACHE_TTL="24h"
# 最大缓存大小默认500MB
export PDF_CACHE_MAX_SIZE="524288000" # 字节
```
### 默认配置
- **缓存目录**系统临时目录下的 `tyapi_pdf_cache`
- **TTL**24小时
- **最大缓存大小**500MB
## 📁 文件结构
```
tyapi-server/
├── internal/
│ └── shared/
│ └── pdf/
│ ├── pdf_cache_manager.go # 新增PDF缓存管理器
│ ├── pdf_generator.go # 原有PDF生成器
│ └── ...
├── internal/
│ └── infrastructure/
│ └── http/
│ └── handlers/
│ └── product_handler.go # 修改:集成缓存机制
└── internal/
└── container/
└── container.go # 修改:初始化缓存管理器
```
## 🔧 使用示例
### 基本使用
缓存机制已自动集成无需额外代码
```go
// 用户请求下载PDF
GET /api/v1/products/{id}/documentation/download
// 系统自动:
// 1. 检查缓存
// 2. 缓存命中 → 直接返回
// 3. 缓存未命中 → 生成PDF → 保存缓存 → 返回
```
### 手动管理缓存
如果需要手动管理缓存如产品更新后清除缓存
```go
// 使特定产品的缓存失效
cacheManager.InvalidateByProductID(productID)
// 使特定版本的缓存失效
cacheManager.Invalidate(productID, version)
// 清空所有缓存
cacheManager.Clear()
// 获取缓存统计信息
stats, _ := cacheManager.GetCacheStats()
```
## 📊 监控和日志
### 日志输出
系统会记录以下日志
- **缓存命中**`PDF缓存命中` - 包含产品ID版本文件大小
- **缓存未命中**`PDF缓存未命中开始生成PDF`
- **缓存保存**`PDF已缓存` - 包含产品ID缓存键文件大小
- **缓存清理**`已清理过期缓存文件` - 包含清理数量和释放空间
### 响应头标识
响应头中添加了缓存标识
- `X-Cache: HIT` - 缓存命中
- `X-Cache: MISS` - 缓存未命中
## 🔒 安全考虑
1. **文件权限**缓存文件权限设置为 `0644`仅所有者可写
2. **目录隔离**缓存文件存储在独立目录不影响其他文件
3. **自动清理**过期文件自动清理防止磁盘空间耗尽
## 🐛 故障处理
### 缓存初始化失败
如果缓存管理器初始化失败系统会
- 记录警告日志
- 继续正常运行禁用缓存功能
- 所有请求都会重新生成PDF
### 缓存读取失败
如果缓存读取失败系统会
- 记录警告日志
- 自动降级为重新生成PDF
- 不影响用户体验
## 🔄 后续优化建议
1. **分布式缓存**考虑使用Redis等分布式缓存支持多实例部署
2. **缓存预热**在系统启动时预生成热门产品的PDF
3. **压缩存储**对PDF文件进行压缩存储节省磁盘空间
4. **缓存统计**添加更详细的缓存统计和监控指标
5. **智能清理**基于LRU等算法优先清理不常用的缓存
## 📝 更新日志
- **2024-12-XX**初始版本实现本地文件缓存机制
- 添加PDF缓存管理器
- 集成到下载接口
- 支持版本控制和自动过期

View File

@@ -0,0 +1,242 @@
# Ubuntu服务器PDF字体配置指南
## 概述
本文档说明如何在Ubuntu 24.04 LTS服务器上配置PDF生成功能所需的中文字体。
## 字体文件位置
确保字体文件存在于以下任一位置:
### 推荐路径(按优先级)
1. **工作目录相对路径**(最常用)
```
{工作目录}/internal/shared/pdf/fonts/
```
例如:如果工作目录是 `/www/tyapi-server`,则字体应在:
```
/www/tyapi-server/internal/shared/pdf/fonts/
```
2. **可执行文件相对路径**
```
{可执行文件所在目录}/internal/shared/pdf/fonts/
```
3. **环境变量指定路径**
```bash
export PDF_FONT_DIR=/path/to/fonts
```
4. **硬编码路径**(后备方案)
- `/www/tyapi-server/internal/shared/pdf/fonts` ✅(已配置)
- `/app/internal/shared/pdf/fonts`Docker
- `/usr/local/tyapi-server/internal/shared/pdf/fonts`
- `/opt/tyapi-server/internal/shared/pdf/fonts`
- `/home/ubuntu/tyapi-server/internal/shared/pdf/fonts`
- `/root/tyapi-server/internal/shared/pdf/fonts`
- `/var/www/tyapi-server/internal/shared/pdf/fonts`
## 部署步骤
### 方法1直接复制字体文件推荐
```bash
# 1. 创建字体目录
sudo mkdir -p /www/tyapi-server/internal/shared/pdf/fonts
# 2. 复制字体文件从本地或Git仓库
# 需要以下字体文件:
# - simhei.ttf (黑体,必需)
# - simkai.ttf (楷体,可选)
# - simfang.ttf (仿宋,可选)
# - YunFengFeiYunTi-2.ttf (水印字体,可选)
# 3. 设置权限
sudo chmod -R 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttf
sudo chmod -R 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttc
# 4. 确保运行用户有读取权限
sudo chown -R $(whoami):$(whoami) /www/tyapi-server/internal/shared/pdf/fonts
```
### 方法2使用环境变量
```bash
# 设置字体目录环境变量
export PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts
# 或在 systemd 服务文件中添加
# Environment="PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts"
```
### 方法3使用符号链接
如果字体文件在其他位置,可以创建符号链接:
```bash
sudo mkdir -p /www/tyapi-server/internal/shared/pdf/fonts
sudo ln -s /path/to/actual/fonts/*.ttf /www/tyapi-server/internal/shared/pdf/fonts/
```
## 验证字体文件
### 1. 检查文件是否存在
```bash
ls -lh /www/tyapi-server/internal/shared/pdf/fonts/
```
应该看到:
```
-rw-r--r-- 1 user user 9.5M Dec 3 18:00 simhei.ttf
-rw-r--r-- 1 user user 8.2M Dec 3 18:00 simkai.ttf
-rw-r--r-- 1 user user 7.8M Dec 3 18:00 simfang.ttf
```
### 2. 检查文件权限
```bash
stat /www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf
```
确保有读取权限(至少 `-r--r--r--`)。
### 3. 检查文件类型
```bash
file /www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf
```
应该显示:`TrueType font data`
## 验证PDF生成功能
### 1. 查看日志
启动服务后,查看日志中是否有以下信息:
```json
{"level":"INFO","msg":"找到字体文件","count":3,"paths":["/www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf",...]}
{"level":"INFO","msg":"成功加载中文字体","font_path":"/www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf"}
```
### 2. 测试PDF生成
调用PDF下载接口检查
- PDF文件能正常生成
- 中文文字正常显示(不是乱码或空白)
- 没有字体相关的错误日志
### 3. 调试信息
如果字体未找到,查看日志中的调试信息:
```json
{"level":"DEBUG","msg":"查找字体文件","total_paths":20,"paths":[...]}
{"level":"DEBUG","msg":"字体文件不存在","font_path":"...","error":"..."}
```
## 常见问题
### 问题1字体文件找不到
**症状**:日志显示 `"未找到中文字体文件"`
**解决方案**
1. 确认字体文件路径是否正确
2. 检查文件权限:`chmod 644 *.ttf`
3. 检查文件所有者:`chown user:user *.ttf`
4. 查看日志中的 `"查找字体文件"` 调试信息,确认尝试的路径
### 问题2字体文件无权限读取
**症状**:日志显示 `"字体文件无读取权限"`
**解决方案**
```bash
sudo chmod 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttf
sudo chown -R $(whoami):$(whoami) /www/tyapi-server/internal/shared/pdf/fonts
```
### 问题3中文显示为乱码
**症状**PDF中中文显示为乱码或空白
**解决方案**
1. 确认字体文件已成功加载(查看日志)
2. 确认字体文件是有效的TTF格式
3. 检查字体文件是否损坏:`file *.ttf`
### 问题4Docker容器中找不到字体
**症状**在Docker容器中运行时找不到字体
**解决方案**
1. 确保Dockerfile中已复制字体文件
```dockerfile
COPY --from=builder /app/internal/shared/pdf/fonts/ ./internal/shared/pdf/fonts/
```
2. 或使用volume挂载
```yaml
volumes:
- /www/tyapi-server/internal/shared/pdf/fonts:/app/internal/shared/pdf/fonts:ro
```
## Systemd服务配置示例
如果使用systemd管理服务可以在服务文件中设置环境变量
```ini
[Unit]
Description=TYAPI Server
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/www/tyapi-server
ExecStart=/www/tyapi-server/tyapi-server -env=production
Environment="PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts"
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
## 字体文件获取
如果本地没有字体文件,可以从以下来源获取:
1. **Windows系统字体**如果服务器是Windows迁移过来的
- `C:\Windows\Fonts\simhei.ttf` → 复制到服务器
2. **Linux系统字体包**
```bash
# Ubuntu/Debian
sudo apt-get install fonts-wqy-zenhei fonts-wqy-microhei
# 然后从系统字体目录复制或创建符号链接
```
3. **从项目仓库**
- 确保字体文件已提交到Git仓库
- 使用 `git pull` 拉取最新代码
## 注意事项
1. **字体文件大小**每个TTF文件约8-10MB确保有足够磁盘空间
2. **文件权限**:确保运行服务的用户有读取权限
3. **路径一致性**:确保字体路径与代码中的查找路径一致
4. **日志级别**生产环境建议将字体查找日志设为DEBUG级别避免日志过多
## 技术支持
如果遇到问题,请提供以下信息:
1. 服务器操作系统版本:`lsb_release -a`
2. 字体文件位置和权限:`ls -lh /www/tyapi-server/internal/shared/pdf/fonts/`
3. 工作目录:`pwd`(服务运行时)
4. 可执行文件位置:`which tyapi-server` 或 `readlink -f $(which tyapi-server)`
5. 相关日志:包含 `"查找字体文件"` 和 `"字体文件"` 的日志条目

View File

@@ -0,0 +1,453 @@
# 🚀 Zap 官方最佳实践日志系统指南
## 概述
本日志系统完全基于 [Zap 官方最佳实践](https://betterstack.com/community/guides/logging/go/zap/) 设计,使用 `zap.NewProduction()``zap.NewDevelopment()` 预设,提供高性能、结构化的日志记录。
## ✨ 核心特性
### 1. **基于 Zap 官方预设**
- 使用 `zap.NewProduction()` 生产环境预设
- 使用 `zap.NewDevelopment()` 开发环境预设
- 自动添加调用者信息、堆栈跟踪等
### 2. **全局日志器支持**
- 支持 `zap.ReplaceGlobals()` 全局替换
- 提供 `logger.L()``logger.GetGlobalLogger()` 访问
- 符合 Zap 官方推荐的使用方式
### 3. **强类型字段支持**
- 使用 `zap.String()`, `zap.Int()`, `zap.Error()` 等强类型字段
- 避免运行时类型错误
- 提供最佳性能
### 4. **上下文日志记录**
- 自动从上下文提取 `request_id`, `user_id`, `trace_id`
- 支持 `WithContext()` 方法
- 便于分布式系统追踪
## 🏗️ 架构设计
### 核心接口
```go
type Logger interface {
// 基础日志方法
Debug(msg string, fields ...zapcore.Field)
Info(msg string, fields ...zapcore.Field)
Warn(msg string, fields ...zapcore.Field)
Error(msg string, fields ...zapcore.Field)
Fatal(msg string, fields ...zapcore.Field)
Panic(msg string, fields ...zapcore.Field)
// 结构化日志方法
With(fields ...zapcore.Field) Logger
WithContext(ctx context.Context) Logger
Named(name string) Logger
// 同步和清理
Sync() error
Core() zapcore.Core
// 获取原生 Zap Logger
GetZapLogger() *zap.Logger
}
```
### 实现类型
1. **ZapLogger**: 标准日志器,基于 Zap 官方预设
2. **LevelLogger**: 级别分文件日志器,支持按级别分离
3. **全局日志器**: 通过 `zap.ReplaceGlobals()` 提供全局访问
## 🚀 使用方法
### 1. 基础使用
```go
package main
import (
"go.uber.org/zap"
"tyapi-server/internal/shared/logger"
)
func main() {
// 初始化全局日志器
config := logger.Config{
Development: true,
Output: "stdout",
Format: "console",
}
if err := logger.InitGlobalLogger(config); err != nil {
panic(err)
}
// 使用全局日志器
logger.L().Info("应用启动成功")
// 或者获取全局日志器
globalLogger := logger.GetGlobalLogger()
globalLogger.Info("使用全局日志器")
}
```
### 2. 依赖注入使用
```go
type ProductService struct {
logger logger.Logger
}
func NewProductService(logger logger.Logger) *ProductService {
return &ProductService{logger: logger}
}
func (s *ProductService) CreateProduct(ctx context.Context, product *Product) error {
// 记录操作日志
s.logger.Info("创建产品",
zap.String("product_id", product.ID),
zap.String("product_name", product.Name),
zap.String("user_id", product.CreatedBy),
)
// 业务逻辑...
return nil
}
```
### 3. 上下文日志记录
```go
func (s *ProductService) GetProduct(ctx context.Context, id string) (*Product, error) {
// 自动从上下文提取字段
logger := s.logger.WithContext(ctx)
logger.Info("获取产品信息",
zap.String("product_id", id),
zap.String("operation", "get_product"),
)
// 业务逻辑...
return product, nil
}
```
### 4. 结构化字段
```go
// 使用强类型字段
s.logger.Info("用户登录",
zap.String("username", "john_doe"),
zap.Int("user_id", 12345),
zap.String("ip_address", "192.168.1.100"),
zap.String("user_agent", r.UserAgent()),
zap.Time("login_time", time.Now()),
)
// 记录错误
if err != nil {
s.logger.Error("数据库操作失败",
zap.Error(err),
zap.String("operation", "create_user"),
zap.String("table", "users"),
)
}
```
### 5. 级别分文件日志
```go
// 配置启用级别分文件
config := logger.Config{
EnableLevelSeparation: true,
Output: "file",
LogDir: "logs",
UseDaily: true,
LevelConfigs: map[string]interface{}{
"debug": map[string]interface{}{
"max_size": 50,
"max_backups": 3,
"max_age": 7,
},
"error": map[string]interface{}{
"max_size": 200,
"max_backups": 10,
"max_age": 90,
},
},
}
// 创建级别分文件日志器
levelLogger, err := logger.NewLevelLogger(logger.LevelLoggerConfig{
BaseConfig: config,
EnableLevelSeparation: true,
LevelConfigs: convertLevelConfigs(config.LevelConfigs),
})
```
## 📁 日志文件结构
### 按级别分文件
```
logs/
├── 2024-01-01/
│ ├── debug.log # 调试日志
│ ├── info.log # 信息日志
│ ├── warn.log # 警告日志
│ ├── error.log # 错误日志
│ ├── fatal.log # 致命错误日志
│ └── panic.log # 恐慌错误日志
└── app.log # 主日志文件
```
### 按日期分包
```
logs/
├── 2024-01-01/
│ ├── app.log
│ └── error.log
├── 2024-01-02/
│ ├── app.log
│ └── error.log
└── app.log # 当前日期
```
## ⚙️ 配置选项
### 基础配置
```yaml
logger:
# 环境配置
development: true # 是否为开发环境
# 输出配置
output: "file" # 输出方式: stdout, stderr, file
format: "json" # 输出格式: json, console
log_dir: "logs" # 日志目录
# 文件配置
max_size: 100 # 单个文件最大大小(MB)
max_backups: 5 # 最大备份文件数
max_age: 30 # 最大保留天数
compress: true # 是否压缩
# 高级功能
use_daily: true # 是否按日分包
enable_level_separation: true # 是否启用按级别分文件
use_color: false # 是否使用彩色输出
```
### 级别配置
```yaml
logger:
level_configs:
debug:
max_size: 50 # 50MB
max_backups: 3 # 3个备份
max_age: 7 # 7天
compress: true
info:
max_size: 100 # 100MB
max_backups: 5 # 5个备份
max_age: 30 # 30天
compress: true
error:
max_size: 200 # 200MB
max_backups: 10 # 10个备份
max_age: 90 # 90天
compress: true
```
## 🔧 最佳实践
### 1. **使用强类型字段**
```go
// ✅ 推荐:使用强类型字段
logger.Info("用户操作",
zap.String("user_id", userID),
zap.String("action", "login"),
zap.Time("timestamp", time.Now()),
)
// ❌ 避免:使用 Any 字段
logger.Info("用户操作",
zap.Any("user_id", userID),
zap.Any("action", "login"),
zap.Any("timestamp", time.Now()),
)
```
### 2. **合理使用日志级别**
```go
// Debug: 详细的调试信息
logger.Debug("SQL查询", zap.String("query", sql))
// Info: 重要的业务事件
logger.Info("用户注册成功", zap.String("user_id", userID))
// Warn: 警告信息,不影响功能
logger.Warn("数据库连接池使用率过高", zap.Int("usage", 85))
// Error: 错误信息,功能受影响
logger.Error("数据库连接失败", zap.Error(err))
// Fatal: 致命错误,应用无法继续
logger.Fatal("配置文件加载失败", zap.Error(err))
```
### 3. **上下文信息提取**
```go
// 在中间件中设置上下文
func LoggingMiddleware(logger logger.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// 生成请求ID
requestID := uuid.New().String()
// 设置上下文
ctx := context.WithValue(c.Request.Context(), "request_id", requestID)
ctx = context.WithValue(ctx, "user_id", getUserID(c))
ctx = context.WithValue(ctx, "trace_id", getTraceID(c))
c.Request = c.Request.WithContext(ctx)
// 记录请求日志
logger.WithContext(ctx).Info("收到请求",
zap.String("method", c.Request.Method),
zap.String("path", c.Request.URL.Path),
zap.String("client_ip", c.ClientIP()),
)
c.Next()
}
}
```
### 4. **性能优化**
```go
// ✅ 推荐:延迟计算
if logger.Core().Enabled(zapcore.DebugLevel) {
logger.Debug("调试信息", zap.String("data", expensiveOperation()))
}
// ❌ 避免:总是计算
logger.Debug("调试信息", zap.String("data", expensiveOperation()))
```
## 🚨 错误处理
### 1. **Panic 恢复**
```go
// 使用 panic 恢复中间件
func PanicRecoveryMiddleware(logger *zap.Logger) gin.HandlerFunc {
return gin.RecoveryWithWriter(&panicLogger{logger: logger})
}
type panicLogger struct {
logger *zap.Logger
}
func (pl *panicLogger) Write(p []byte) (n int, err error) {
pl.logger.Error("系统发生严重错误",
zap.String("error_type", "panic"),
zap.String("stack_trace", string(p)),
zap.String("timestamp", time.Now().Format("2006-01-02 15:04:05")),
)
return len(p), nil
}
```
### 2. **错误日志记录**
```go
// 记录错误详情
if err != nil {
logger.Error("操作失败",
zap.Error(err),
zap.String("operation", "create_user"),
zap.String("user_id", userID),
zap.String("stack_trace", string(debug.Stack())),
)
return err
}
```
## 📊 性能基准
基于 [Zap 官方基准测试](https://betterstack.com/community/guides/logging/go/zap/)
| 包 | 时间 | 相对于 Zap | 内存分配 |
|----|------|------------|----------|
| zap | 193 ns/op | +0% | 0 allocs/op |
| zap (sugared) | 227 ns/op | +18% | 1 allocs/op |
| zerolog | 81 ns/op | -58% | 0 allocs/op |
| slog | 322 ns/op | +67% | 0 allocs/op |
## 🔍 调试和故障排除
### 1. **检查日志级别**
```go
// 检查日志级别是否启用
if logger.Core().Enabled(zapcore.DebugLevel) {
logger.Debug("调试信息")
}
```
### 2. **同步日志**
```go
// 确保日志写入完成
defer logger.Sync()
// 或者在应用关闭时
func cleanup() {
logger.Sync()
}
```
### 3. **验证配置**
```go
// 验证日志器配置
config := logger.Config{
Development: true,
Output: "stdout",
Format: "console",
}
logger, err := logger.NewLogger(config)
if err != nil {
log.Fatalf("创建日志器失败: %v", err)
}
```
## 🎯 总结
本日志系统完全基于 Zap 官方最佳实践设计,具有以下优势:
1. **高性能**: 基于 Zap 的高性能实现
2. **官方推荐**: 使用 `zap.NewProduction()``zap.NewDevelopment()` 预设
3. **强类型**: 支持强类型字段,避免运行时错误
4. **结构化**: 支持结构化日志记录
5. **上下文**: 自动提取上下文信息
6. **灵活配置**: 支持文件输出、级别分离、按日分包等
7. **全局访问**: 支持全局日志器访问
通过合理使用,您将获得高性能、结构化的日志系统,满足生产环境的各种需求!
## 📚 参考资源
- [Zap 官方文档](https://pkg.go.dev/go.uber.org/zap)
- [Zap 最佳实践指南](https://betterstack.com/community/guides/logging/go/zap/)
- [Zap GitHub 仓库](https://github.com/uber-go/zap)

View File

@@ -0,0 +1,603 @@
# 统计功能API文档
## 概述
统计功能API提供了完整的统计数据分析和管理功能包括指标管理、实时统计、历史统计、仪表板管理、报告生成、数据导出等功能。
## 基础信息
- **基础URL**: `/api/v1/statistics`
- **认证方式**: Bearer Token
- **内容类型**: `application/json`
- **字符编码**: `UTF-8`
## 认证和权限
### 认证方式
所有API请求都需要在请求头中包含有效的JWT令牌
```
Authorization: Bearer <your-jwt-token>
```
### 权限级别
- **公开访问**: 无需认证的接口
- **用户权限**: 需要用户或管理员权限
- **管理员权限**: 仅管理员可访问
## API接口
### 1. 指标管理
#### 1.1 创建统计指标
- **URL**: `POST /api/v1/statistics/metrics`
- **权限**: 管理员
- **描述**: 创建新的统计指标
**请求体**:
```json
{
"metric_type": "api_calls",
"metric_name": "total_count",
"dimension": "realtime",
"value": 100.0,
"metadata": "{\"source\": \"api_gateway\"}",
"date": "2024-01-01T00:00:00Z"
}
```
**响应**:
```json
{
"success": true,
"message": "指标创建成功",
"data": {
"id": "uuid",
"metric_type": "api_calls",
"metric_name": "total_count",
"dimension": "realtime",
"value": 100.0,
"metadata": "{\"source\": \"api_gateway\"}",
"date": "2024-01-01T00:00:00Z",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
}
```
#### 1.2 更新统计指标
- **URL**: `PUT /api/v1/statistics/metrics`
- **权限**: 管理员
- **描述**: 更新现有统计指标的值
**请求体**:
```json
{
"id": "uuid",
"value": 150.0
}
```
#### 1.3 删除统计指标
- **URL**: `DELETE /api/v1/statistics/metrics`
- **权限**: 管理员
- **描述**: 删除指定的统计指标
**请求体**:
```json
{
"id": "uuid"
}
```
#### 1.4 获取单个指标
- **URL**: `GET /api/v1/statistics/metrics/{id}`
- **权限**: 用户
- **描述**: 根据ID获取指定的统计指标
#### 1.5 获取指标列表
- **URL**: `GET /api/v1/statistics/metrics`
- **权限**: 用户
- **描述**: 根据条件获取统计指标列表
**查询参数**:
- `metric_type` (string): 指标类型
- `metric_name` (string): 指标名称
- `dimension` (string): 统计维度
- `start_date` (string): 开始日期 (YYYY-MM-DD)
- `end_date` (string): 结束日期 (YYYY-MM-DD)
- `limit` (int): 限制数量 (默认20, 最大1000)
- `offset` (int): 偏移量 (默认0)
- `sort_by` (string): 排序字段 (默认created_at)
- `sort_order` (string): 排序顺序 (默认desc)
### 2. 实时统计
#### 2.1 获取实时指标
- **URL**: `GET /api/v1/statistics/realtime`
- **权限**: 公开
- **描述**: 获取指定类型的实时统计指标
**查询参数**:
- `metric_type` (string, 必需): 指标类型
- `time_range` (string): 时间范围 (last_hour, last_day, last_week)
- `dimension` (string): 统计维度
**响应**:
```json
{
"success": true,
"message": "获取实时指标成功",
"data": {
"metric_type": "api_calls",
"metrics": {
"total_count": 1000,
"success_count": 950,
"failed_count": 50,
"success_rate": 95.0
},
"timestamp": "2024-01-01T12:00:00Z",
"metadata": {
"time_range": "last_hour",
"dimension": "realtime"
}
}
}
```
### 3. 历史统计
#### 3.1 获取历史指标
- **URL**: `GET /api/v1/statistics/historical`
- **权限**: 公开
- **描述**: 获取指定时间范围的历史统计指标
**查询参数**:
- `metric_type` (string, 必需): 指标类型
- `metric_name` (string): 指标名称
- `dimension` (string): 统计维度
- `start_date` (string, 必需): 开始日期 (YYYY-MM-DD)
- `end_date` (string, 必需): 结束日期 (YYYY-MM-DD)
- `period` (string): 统计周期
- `limit` (int): 限制数量 (默认20)
- `offset` (int): 偏移量 (默认0)
- `aggregate_by` (string): 聚合维度
- `group_by` (string): 分组维度
**响应**:
```json
{
"success": true,
"message": "获取历史指标成功",
"data": {
"metric_type": "api_calls",
"metric_name": "total_count",
"dimension": "daily",
"data_points": [
{
"date": "2024-01-01T00:00:00Z",
"value": 1000,
"label": "total_count"
}
],
"summary": {
"total": 30000,
"average": 1000,
"max": 1500,
"min": 500,
"count": 30,
"growth_rate": 5.2,
"trend": "increasing"
},
"metadata": {
"period": "daily",
"aggregate_by": "day",
"group_by": "metric_name"
}
}
}
```
### 4. 仪表板管理
#### 4.1 创建仪表板
- **URL**: `POST /api/v1/statistics/dashboards`
- **权限**: 管理员
- **描述**: 创建新的统计仪表板
**请求体**:
```json
{
"name": "用户仪表板",
"description": "普通用户专用仪表板",
"user_role": "user",
"layout": "{\"columns\": 2, \"rows\": 3}",
"widgets": "[{\"type\": \"api_calls\", \"position\": {\"x\": 0, \"y\": 0}}]",
"settings": "{\"theme\": \"light\", \"auto_refresh\": false}",
"refresh_interval": 600,
"access_level": "private",
"created_by": "user_id"
}
```
#### 4.2 获取仪表板列表
- **URL**: `GET /api/v1/statistics/dashboards`
- **权限**: 用户
- **描述**: 根据条件获取统计仪表板列表
**查询参数**:
- `user_role` (string): 用户角色
- `is_default` (bool): 是否默认
- `is_active` (bool): 是否激活
- `access_level` (string): 访问级别
- `created_by` (string): 创建者ID
- `name` (string): 仪表板名称
- `limit` (int): 限制数量 (默认20)
- `offset` (int): 偏移量 (默认0)
- `sort_by` (string): 排序字段 (默认created_at)
- `sort_order` (string): 排序顺序 (默认desc)
#### 4.3 获取单个仪表板
- **URL**: `GET /api/v1/statistics/dashboards/{id}`
- **权限**: 用户
- **描述**: 根据ID获取指定的统计仪表板
#### 4.4 获取仪表板数据
- **URL**: `GET /api/v1/statistics/dashboards/data`
- **权限**: 公开
- **描述**: 获取指定角色的仪表板数据
**查询参数**:
- `user_role` (string, 必需): 用户角色
- `period` (string): 统计周期 (today, week, month)
- `start_date` (string): 开始日期 (YYYY-MM-DD)
- `end_date` (string): 结束日期 (YYYY-MM-DD)
- `metric_types` (string): 指标类型列表
- `dimensions` (string): 统计维度列表
**响应**:
```json
{
"success": true,
"message": "获取仪表板数据成功",
"data": {
"api_calls": {
"total_count": 10000,
"success_count": 9500,
"failed_count": 500,
"success_rate": 95.0,
"avg_response_time": 150.5
},
"users": {
"total_count": 1000,
"certified_count": 800,
"active_count": 750,
"certification_rate": 80.0,
"retention_rate": 75.0
},
"finance": {
"total_amount": 50000.0,
"recharge_amount": 60000.0,
"deduct_amount": 10000.0,
"net_amount": 50000.0
},
"period": {
"start_date": "2024-01-01",
"end_date": "2024-01-01",
"period": "today"
},
"metadata": {
"generated_at": "2024-01-01 12:00:00",
"user_role": "user",
"data_version": "1.0"
}
}
}
```
### 5. 报告管理
#### 5.1 生成报告
- **URL**: `POST /api/v1/statistics/reports`
- **权限**: 管理员
- **描述**: 生成指定类型的统计报告
**请求体**:
```json
{
"report_type": "summary",
"title": "月度汇总报告",
"period": "month",
"user_role": "admin",
"start_date": "2024-01-01T00:00:00Z",
"end_date": "2024-01-31T23:59:59Z",
"filters": {
"metric_types": ["api_calls", "users"],
"dimensions": ["daily", "weekly"]
},
"generated_by": "admin_id"
}
```
#### 5.2 获取报告列表
- **URL**: `GET /api/v1/statistics/reports`
- **权限**: 用户
- **描述**: 根据条件获取统计报告列表
**查询参数**:
- `report_type` (string): 报告类型
- `user_role` (string): 用户角色
- `status` (string): 报告状态
- `period` (string): 统计周期
- `start_date` (string): 开始日期 (YYYY-MM-DD)
- `end_date` (string): 结束日期 (YYYY-MM-DD)
- `limit` (int): 限制数量 (默认20)
- `offset` (int): 偏移量 (默认0)
- `sort_by` (string): 排序字段 (默认created_at)
- `sort_order` (string): 排序顺序 (默认desc)
- `generated_by` (string): 生成者ID
#### 5.3 获取单个报告
- **URL**: `GET /api/v1/statistics/reports/{id}`
- **权限**: 用户
- **描述**: 根据ID获取指定的统计报告
### 6. 统计分析
#### 6.1 计算增长率
- **URL**: `GET /api/v1/statistics/analysis/growth-rate`
- **权限**: 用户
- **描述**: 计算指定指标的增长率
**查询参数**:
- `metric_type` (string, 必需): 指标类型
- `metric_name` (string, 必需): 指标名称
- `current_period` (string, 必需): 当前周期 (YYYY-MM-DD)
- `previous_period` (string, 必需): 上一周期 (YYYY-MM-DD)
**响应**:
```json
{
"success": true,
"message": "计算增长率成功",
"data": {
"growth_rate": 15.5,
"current_value": 1150,
"previous_value": 1000,
"period": "daily"
}
}
```
#### 6.2 计算趋势
- **URL**: `GET /api/v1/statistics/analysis/trend`
- **权限**: 用户
- **描述**: 计算指定指标的趋势
**查询参数**:
- `metric_type` (string, 必需): 指标类型
- `metric_name` (string, 必需): 指标名称
- `start_date` (string, 必需): 开始日期 (YYYY-MM-DD)
- `end_date` (string, 必需): 结束日期 (YYYY-MM-DD)
**响应**:
```json
{
"success": true,
"message": "计算趋势成功",
"data": {
"trend": "increasing",
"trend_strength": 0.8,
"data_points": 30,
"correlation": 0.75
}
}
```
### 7. 数据导出
#### 7.1 导出数据
- **URL**: `POST /api/v1/statistics/export`
- **权限**: 管理员
- **描述**: 导出指定格式的统计数据
**请求体**:
```json
{
"format": "excel",
"metric_type": "api_calls",
"start_date": "2024-01-01T00:00:00Z",
"end_date": "2024-01-31T23:59:59Z",
"dimension": "daily",
"group_by": "metric_name",
"filters": {
"status": "success"
},
"columns": ["date", "metric_name", "value"],
"include_charts": true,
"exported_by": "admin_id"
}
```
**响应**:
```json
{
"success": true,
"message": "数据导出成功",
"data": {
"download_url": "https://api.example.com/downloads/export_123.xlsx",
"file_name": "api_calls_export_20240101_20240131.xlsx",
"file_size": 1024000,
"expires_at": "2024-01-02T12:00:00Z"
}
}
```
### 8. 定时任务管理
#### 8.1 手动触发小时聚合
- **URL**: `POST /api/v1/statistics/cron/hourly-aggregation`
- **权限**: 管理员
- **描述**: 手动触发指定时间的小时级数据聚合
**查询参数**:
- `target_hour` (string, 必需): 目标小时 (YYYY-MM-DDTHH:MM:SSZ)
#### 8.2 手动触发日聚合
- **URL**: `POST /api/v1/statistics/cron/daily-aggregation`
- **权限**: 管理员
- **描述**: 手动触发指定时间的日级数据聚合
**查询参数**:
- `target_date` (string, 必需): 目标日期 (YYYY-MM-DD)
#### 8.3 手动触发数据清理
- **URL**: `POST /api/v1/statistics/cron/data-cleanup`
- **权限**: 管理员
- **描述**: 手动触发过期数据清理任务
## 错误码
| 错误码 | HTTP状态码 | 描述 |
|--------|------------|------|
| 400 | 400 Bad Request | 请求参数错误 |
| 401 | 401 Unauthorized | 未认证或认证失败 |
| 403 | 403 Forbidden | 权限不足 |
| 404 | 404 Not Found | 资源不存在 |
| 422 | 422 Unprocessable Entity | 参数验证失败 |
| 429 | 429 Too Many Requests | 请求频率过高 |
| 500 | 500 Internal Server Error | 服务器内部错误 |
## 响应格式
### 成功响应
```json
{
"success": true,
"message": "操作成功",
"data": {
// 响应数据
}
}
```
### 错误响应
```json
{
"success": false,
"message": "错误描述",
"error": "详细错误信息"
}
```
### 列表响应
```json
{
"success": true,
"message": "查询成功",
"data": [
// 数据列表
],
"pagination": {
"page": 1,
"page_size": 20,
"total": 100,
"pages": 5,
"has_next": true,
"has_prev": false
}
}
```
## 数据模型
### 统计指标 (StatisticsMetric)
```json
{
"id": "string",
"metric_type": "string",
"metric_name": "string",
"dimension": "string",
"value": "number",
"metadata": "string",
"date": "string",
"created_at": "string",
"updated_at": "string"
}
```
### 统计报告 (StatisticsReport)
```json
{
"id": "string",
"report_type": "string",
"title": "string",
"content": "string",
"period": "string",
"user_role": "string",
"status": "string",
"generated_by": "string",
"generated_at": "string",
"expires_at": "string",
"created_at": "string",
"updated_at": "string"
}
```
### 统计仪表板 (StatisticsDashboard)
```json
{
"id": "string",
"name": "string",
"description": "string",
"user_role": "string",
"is_default": "boolean",
"is_active": "boolean",
"layout": "string",
"widgets": "string",
"settings": "string",
"refresh_interval": "number",
"created_by": "string",
"access_level": "string",
"created_at": "string",
"updated_at": "string"
}
```
## 使用示例
### 获取今日API调用统计
```bash
curl -X GET "https://api.example.com/api/v1/statistics/realtime?metric_type=api_calls&time_range=last_hour" \
-H "Authorization: Bearer your-jwt-token"
```
### 获取历史用户数据
```bash
curl -X GET "https://api.example.com/api/v1/statistics/historical?metric_type=users&start_date=2024-01-01&end_date=2024-01-31" \
-H "Authorization: Bearer your-jwt-token"
```
### 生成月度报告
```bash
curl -X POST "https://api.example.com/api/v1/statistics/reports" \
-H "Authorization: Bearer your-jwt-token" \
-H "Content-Type: application/json" \
-d '{
"report_type": "summary",
"title": "月度汇总报告",
"period": "month",
"user_role": "admin",
"generated_by": "admin_id"
}'
```
## 注意事项
1. **日期格式**: 所有日期参数都使用 `YYYY-MM-DD` 格式
2. **时间戳**: 所有时间戳都使用 ISO 8601 格式
3. **分页**: 默认每页20条记录最大1000条
4. **限流**: API有请求频率限制超出限制会返回429错误
5. **缓存**: 部分接口支持缓存,响应头会包含缓存信息
6. **权限**: 不同接口需要不同的权限级别,请确保有相应权限
7. **数据量**: 查询大量数据时建议使用分页和日期范围限制

File diff suppressed because it is too large Load Diff

9233
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

6209
docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,999 @@
# 产品示例报告下载功能实现方案
## 一、功能概述
在产品详情页面添加"下载示例报告"功能,允许用户下载与产品对应的前端组件报告。报告文件位于 `resources/Pure_Component/src/ui` 目录下通过产品编号product_code匹配对应的文件夹或文件。
## 二、核心需求
### 2.1 基本功能
1. **报告匹配**:根据子产品的 `product_code` 模糊匹配 `resources/Pure_Component/src/ui` 下的文件夹或文件
- 支持前缀匹配(如产品编号为 `DWBG6A2C`,文件夹可能是 `DWBG6A2C``多cDWBG6A2C`
- 匹配规则:文件夹名称包含产品编号,或产品编号包含文件夹名称的核心部分
2. **文件打包**:将匹配到的文件夹或文件压缩成 ZIP 格式供用户下载
- ZIP 文件结构:
```text
component-report.zip
├── src/
│ └── ui/
│ ├── DWBG6A2C/ # 子产品1的UI组件
│ │ ├── index.vue
│ │ ├── components/
│ │ └── ...
│ ├── CFLXG0V4B/ # 子产品2的UI组件
│ └── ...
└── public/
└── example.json # 响应示例数据文件
```
- 同时需要生成 `public/example.json` 文件,包含子产品的响应示例数据
- `example.json` 文件格式:
```json
[
{
"feature": {
"featureName": "产品名称",
"sort": 1
},
"data": {
"apiID": "产品编号",
"data": {
// 子产品的响应示例数据JSON对象
"code": 0,
"message": "success",
"data": { ... }
}
}
},
{
"feature": {
"featureName": "另一个产品名称",
"sort": 2
},
"data": {
"apiID": "另一个产品编号",
"data": { ... }
}
}
]
```
3. **支付流程**
- 下载前检查用户已下载过的组件报告
- 根据子产品价格计算报告总价(已下载过的子产品价格需减免)
- 用户扫码支付(微信/支付宝)成功后允许下载
### 2.2 组合包产品特殊处理
1. **子产品筛选**:组合包产品只下载包含的子产品对应的报告
2. **价格减免**:如果用户已下载过某个子产品的报告,该子产品的价格不计入总价
3. **批量下载**:将多个子产品的报告打包成一个 ZIP 文件
### 2.3 性能优化
1. **缓存机制**
- 缓存已生成的 ZIP 文件基于产品ID和子产品列表的哈希值
- 缓存文件匹配结果(产品编号到文件夹路径的映射)
- 缓存有效期24小时
2. **二次下载**
- 用户已支付过的报告支持免费重新下载(在有效期内)
- 记录下载历史,避免重复支付
## 三、数据库设计
### 3.1 报告下载记录表 (component_report_downloads)
```sql
CREATE TABLE component_report_downloads (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
product_id VARCHAR(36) NOT NULL,
product_code VARCHAR(50) NOT NULL,
sub_product_ids TEXT, -- JSON数组存储子产品ID列表组合包使用
sub_product_codes TEXT, -- JSON数组存储子产品编号列表
download_price DECIMAL(10,2) NOT NULL, -- 实际支付价格
payment_order_id VARCHAR(64), -- 支付订单号(关联充值记录)
payment_type VARCHAR(20), -- 支付类型alipay, wechat
payment_status VARCHAR(20) DEFAULT 'pending', -- pending, success, failed
file_path VARCHAR(500), -- 生成的ZIP文件路径用于二次下载
file_hash VARCHAR(64), -- 文件哈希值(用于缓存验证)
download_count INT DEFAULT 0, -- 下载次数
last_download_at TIMESTAMP, -- 最后下载时间
expires_at TIMESTAMP, -- 下载有效期支付成功后30天
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL,
INDEX idx_user_id (user_id),
INDEX idx_product_id (product_id),
INDEX idx_product_code (product_code),
INDEX idx_payment_status (payment_status),
INDEX idx_expires_at (expires_at)
);
```
### 3.2 报告文件匹配缓存表 (component_report_cache)
```sql
CREATE TABLE component_report_cache (
id VARCHAR(36) PRIMARY KEY,
product_code VARCHAR(50) NOT NULL UNIQUE,
matched_path VARCHAR(500) NOT NULL, -- 匹配到的文件夹/文件路径
file_type VARCHAR(20) NOT NULL, -- folder, file
cache_key VARCHAR(64) NOT NULL UNIQUE, -- 缓存键用于ZIP文件缓存
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_product_code (product_code),
INDEX idx_cache_key (cache_key)
);
```
## 四、后端实现方案
### 4.1 领域层 (Domain Layer)
#### 4.1.1 实体定义
**文件位置**: `internal/domains/product/entities/component_report_download.go`
```go
package entities
import (
"time"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
// ComponentReportDownload 组件报告下载记录
type ComponentReportDownload struct {
ID string `gorm:"primaryKey;type:varchar(36)"`
UserID string `gorm:"type:varchar(36);not null;index"`
ProductID string `gorm:"type:varchar(36);not null;index"`
ProductCode string `gorm:"type:varchar(50);not null;index"`
SubProductIDs string `gorm:"type:text"` // JSON数组
SubProductCodes string `gorm:"type:text"` // JSON数组
DownloadPrice decimal.Decimal `gorm:"type:decimal(10,2);not null"`
OriginalPrice decimal.Decimal `gorm:"type:decimal(10,2);not null"`
DiscountAmount decimal.Decimal `gorm:"type:decimal(10,2);default:0"`
PaymentOrderID *string `gorm:"type:varchar(64)"`
PaymentType *string `gorm:"type:varchar(20)"`
PaymentStatus string `gorm:"type:varchar(20);default:'pending';index"`
FilePath *string `gorm:"type:varchar(500)"`
FileHash *string `gorm:"type:varchar(64)"`
DownloadCount int `gorm:"default:0"`
LastDownloadAt *time.Time
ExpiresAt *time.Time `gorm:"index"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// ComponentReportCache 报告文件匹配缓存
type ComponentReportCache struct {
ID string `gorm:"primaryKey;type:varchar(36)"`
ProductCode string `gorm:"type:varchar(50);not null;uniqueIndex"`
MatchedPath string `gorm:"type:varchar(500);not null"`
FileType string `gorm:"type:varchar(20);not null"`
CacheKey string `gorm:"type:varchar(64);not null;uniqueIndex"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
```
#### 4.1.2 仓储接口
**文件位置**: `internal/domains/product/repositories/component_report_repository_interface.go`
```go
package repositories
import (
"context"
"tyapi-server/internal/domains/product/entities"
)
// ComponentReportRepository 组件报告仓储接口
type ComponentReportRepository interface {
// 创建下载记录
CreateDownload(ctx context.Context, download *entities.ComponentReportDownload) (*entities.ComponentReportDownload, error)
// 更新下载记录
UpdateDownload(ctx context.Context, download *entities.ComponentReportDownload) error
// 根据ID获取下载记录
GetDownloadByID(ctx context.Context, id string) (*entities.ComponentReportDownload, error)
// 获取用户的下载记录列表
GetUserDownloads(ctx context.Context, userID string, productID *string) ([]*entities.ComponentReportDownload, error)
// 检查用户是否已下载过指定产品
HasUserDownloaded(ctx context.Context, userID string, productCode string) (bool, error)
// 获取用户已下载的产品编号列表
GetUserDownloadedProductCodes(ctx context.Context, userID string) ([]string, error)
// 根据支付订单号获取下载记录
GetDownloadByPaymentOrderID(ctx context.Context, orderID string) (*entities.ComponentReportDownload, error)
// 缓存相关
GetCacheByProductCode(ctx context.Context, productCode string) (*entities.ComponentReportCache, error)
CreateCache(ctx context.Context, cache *entities.ComponentReportCache) error
UpdateCache(ctx context.Context, cache *entities.ComponentReportCache) error
}
```
#### 4.1.3 领域服务
**文件位置**: `internal/domains/product/services/component_report_service.go`
```go
package services
import (
"context"
"path/filepath"
"strings"
"os"
"tyapi-server/internal/domains/product/repositories"
)
// ComponentReportService 组件报告服务
type ComponentReportService interface {
// 匹配产品编号到文件路径
MatchProductCodeToPath(ctx context.Context, productCode string) (string, string, error) // 返回路径和类型
// 计算报告价格(考虑已下载的产品)
CalculateReportPrice(ctx context.Context, userID string, productID string, subProductCodes []string) (decimal.Decimal, decimal.Decimal, []string, error) // 返回总价、减免金额、已下载的产品编号列表
// 生成 example.json 文件内容
GenerateExampleJSON(ctx context.Context, productID string, subProductCodes []string) ([]byte, error)
// 生成ZIP文件
GenerateZipFile(ctx context.Context, productID string, subProductCodes []string, cacheKey string) (string, string, error) // 返回文件路径和哈希值
// 验证下载权限
ValidateDownloadPermission(ctx context.Context, userID string, downloadID string) (bool, error)
}
```
### 4.2 应用层 (Application Layer)
**文件位置**: `internal/application/product/component_report_application_service.go`
```go
package application
import (
"context"
"tyapi-server/internal/domains/product/services"
"tyapi-server/internal/domains/finance/services"
)
// ComponentReportApplicationService 组件报告应用服务
type ComponentReportApplicationService interface {
// 1. 获取报告下载信息(价格、已下载列表等)
GetReportDownloadInfo(ctx context.Context, userID string, productID string) (*ReportDownloadInfoResponse, error)
// 2. 创建支付订单
CreateReportPaymentOrder(ctx context.Context, userID string, productID string) (*PaymentOrderResponse, error)
// 3. 处理支付成功回调
HandlePaymentSuccess(ctx context.Context, orderID string) error
// 4. 下载报告文件
DownloadReport(ctx context.Context, userID string, downloadID string) (*DownloadResponse, error)
// 5. 获取用户下载历史
GetUserDownloadHistory(ctx context.Context, userID string, params *PaginationParams) (*DownloadHistoryResponse, error)
}
```
### 4.3 接口层 (Interface Layer)
**文件位置**: `internal/infrastructure/http/handlers/component_report_handler.go`
#### API 接口设计
1. **GET `/api/v1/products/{productId}/component-report/info`**
- 获取报告下载信息(价格、已下载列表等)
- 响应示例:
```json
{
"code": 200,
"data": {
"product_id": "xxx",
"product_code": "DWBG6A2C",
"is_package": true,
"sub_products": [
{
"product_id": "xxx",
"product_code": "DWBG6A2C",
"product_name": "产品名称",
"price": 100.00,
"is_downloaded": false,
"matched": true,
"has_response_example": true
}
],
"original_total_price": 300.00,
"discount_amount": 100.00,
"final_price": 200.00,
"downloaded_product_codes": ["DWBG6A2C"],
"includes_example_json": true
}
}
```
2. **POST `/api/v1/products/{productId}/component-report/create-order`**
- 创建支付订单
- 请求体:
```json
{
"payment_type": "wechat" // 或 "alipay"
}
```
- 响应返回支付二维码URL或支付链接
3. **POST `/api/v1/products/{productId}/component-report/payment-callback`**
- 支付成功回调(内部调用,由支付服务触发)
4. **GET `/api/v1/products/{productId}/component-report/download/{downloadId}`**
- 下载报告文件
- 响应ZIP文件流
5. **GET `/api/v1/my/component-reports`**
- 获取用户下载历史
- 查询参数page, page_size, product_id
## 五、前端实现方案
### 5.1 API 接口定义
**文件位置**: `tyapi-frontend/src/api/index.js`
```javascript
// 组件报告相关接口
export const componentReportApi = {
// 获取报告下载信息
getReportDownloadInfo: (productId) => request.get(`/products/${productId}/component-report/info`),
// 创建支付订单
createReportPaymentOrder: (productId, data) => request.post(`/products/${productId}/component-report/create-order`, data),
// 下载报告
downloadReport: (productId, downloadId) => request.get(`/products/${productId}/component-report/download/${downloadId}`, {
responseType: 'blob'
}),
// 获取用户下载历史
getUserDownloadHistory: (params) => request.get('/my/component-reports', { params })
}
```
### 5.2 页面组件修改
**文件位置**: `tyapi-frontend/src/pages/products/detail.vue`
#### 5.2.1 添加下载按钮
在页面头部操作区域添加"下载示例报告"按钮:
```vue
<el-button
v-if="isSubscribed"
:size="isMobile ? 'small' : 'default'"
type="success"
@click="handleDownloadReport"
:loading="reportDownloading"
>
<el-icon><Download /></el-icon>
下载示例报告
</el-button>
```
#### 5.2.2 下载流程实现
```javascript
// 响应式数据
const reportDownloading = ref(false)
const reportDownloadInfo = ref(null)
const showReportPaymentDialog = ref(false)
const reportPaymentType = ref('wechat') // 或 'alipay'
// 获取报告下载信息
const loadReportDownloadInfo = async () => {
if (!product.value) return
try {
const response = await componentReportApi.getReportDownloadInfo(product.value.id)
reportDownloadInfo.value = response.data
} catch (error) {
console.error('获取报告下载信息失败:', error)
}
}
// 处理下载报告
const handleDownloadReport = async () => {
if (!product.value) return
try {
// 1. 获取下载信息
await loadReportDownloadInfo()
// 2. 如果价格大于0需要支付
if (reportDownloadInfo.value.final_price > 0) {
showReportPaymentDialog.value = true
return
}
// 3. 如果已支付过,直接下载
await downloadReportFile()
} catch (error) {
console.error('下载报告失败:', error)
ElMessage.error('下载报告失败')
}
}
// 创建支付订单并支付
const createReportPayment = async () => {
if (!product.value) return
try {
const response = await componentReportApi.createReportPaymentOrder(
product.value.id,
{ payment_type: reportPaymentType.value }
)
// 显示支付二维码(参考现有的支付流程)
if (reportPaymentType.value === 'wechat') {
await showWechatQrCode(response.data.code_url)
} else {
// 支付宝支付
window.location.href = response.data.pay_url
}
// 开始轮询支付状态
startPaymentPolling(response.data.order_id)
} catch (error) {
console.error('创建支付订单失败:', error)
ElMessage.error('创建支付订单失败')
}
}
// 下载报告文件
const downloadReportFile = async (downloadId) => {
if (!product.value) return
reportDownloading.value = true
try {
// 如果没有downloadId从下载信息中获取
const id = downloadId || reportDownloadInfo.value?.download_id
if (!id) {
ElMessage.warning('请先完成支付')
return
}
const response = await componentReportApi.downloadReport(product.value.id, id)
// 创建下载链接
const blob = new Blob([response.data], { type: 'application/zip' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${product.value.name || '产品'}_示例报告.zip`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('报告下载成功')
// 关闭支付对话框
showReportPaymentDialog.value = false
} catch (error) {
console.error('下载报告文件失败:', error)
ElMessage.error('下载报告文件失败')
} finally {
reportDownloading.value = false
}
}
```
## 六、核心算法实现
### 6.1 响应示例数据提取
在生成 `example.json` 时,需要从产品文档中提取响应示例数据。数据来源优先级:
1. **产品文档的 `response_example` 字段**JSON格式
2. **产品文档的 `response_example` 字段**Markdown代码块中的JSON
3. **产品API配置的 `response_example` 字段**(如果产品文档中没有)
4. **默认空对象** `{}`(如果都没有)
### 6.2 产品编号匹配算法
```go
// MatchProductCodeToPath 匹配产品编号到文件路径
func (s *ComponentReportServiceImpl) MatchProductCodeToPath(ctx context.Context, productCode string) (string, string, error) {
// 1. 先查缓存
cache, err := s.repo.GetCacheByProductCode(ctx, productCode)
if err == nil && cache != nil {
return cache.MatchedPath, cache.FileType, nil
}
// 2. 扫描目录
basePath := "resources/Pure_Component/src/ui"
entries, err := os.ReadDir(basePath)
if err != nil {
return "", "", err
}
// 3. 模糊匹配
for _, entry := range entries {
name := entry.Name()
// 精确匹配
if name == productCode {
path := filepath.Join(basePath, name)
fileType := "folder"
if !entry.IsDir() {
fileType = "file"
}
// 保存到缓存
cache := &entities.ComponentReportCache{
ID: uuid.New().String(),
ProductCode: productCode,
MatchedPath: path,
FileType: fileType,
CacheKey: generateCacheKey(productCode),
}
s.repo.CreateCache(ctx, cache)
return path, fileType, nil
}
// 模糊匹配:文件夹名称包含产品编号,或产品编号包含文件夹名称的核心部分
if strings.Contains(name, productCode) || strings.Contains(productCode, extractCoreCode(name)) {
path := filepath.Join(basePath, name)
fileType := "folder"
if !entry.IsDir() {
fileType = "file"
}
// 保存到缓存
cache := &entities.ComponentReportCache{
ID: uuid.New().String(),
ProductCode: productCode,
MatchedPath: path,
FileType: fileType,
CacheKey: generateCacheKey(productCode),
}
s.repo.CreateCache(ctx, cache)
return path, fileType, nil
}
}
return "", "", fmt.Errorf("未找到匹配的报告文件: %s", productCode)
}
// extractCoreCode 提取文件夹名称的核心代码(去除前缀)
func extractCoreCode(name string) string {
// 如果名称以字母开头,提取后面的代码部分
// 例如多cDWBG6A2C -> DWBG6A2C
for i, r := range name {
if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
return name[i:]
}
}
return name
}
```
### 6.3 价格计算算法
```go
// CalculateReportPrice 计算报告价格
func (s *ComponentReportServiceImpl) CalculateReportPrice(ctx context.Context, userID string, productID string, subProductCodes []string) (decimal.Decimal, decimal.Decimal, []string, error) {
// 1. 获取用户已下载的产品编号列表
downloadedCodes, err := s.repo.GetUserDownloadedProductCodes(ctx, userID)
if err != nil {
return decimal.Zero, decimal.Zero, nil, err
}
// 2. 获取产品信息(包括子产品价格)
product, err := s.productRepo.GetByID(ctx, productID)
if err != nil {
return decimal.Zero, decimal.Zero, nil, err
}
// 3. 计算总价和减免金额
originalTotal := decimal.Zero
discountAmount := decimal.Zero
downloadedList := []string{}
if product.IsPackage {
// 组合包:遍历子产品
for _, item := range product.PackageItems {
if contains(subProductCodes, item.ProductCode) {
originalTotal = originalTotal.Add(item.Price)
// 检查是否已下载
if contains(downloadedCodes, item.ProductCode) {
discountAmount = discountAmount.Add(item.Price)
downloadedList = append(downloadedList, item.ProductCode)
}
}
}
} else {
// 单品
originalTotal = product.Price
if contains(downloadedCodes, product.Code) {
discountAmount = product.Price
downloadedList = append(downloadedList, product.Code)
}
}
finalPrice := originalTotal.Sub(discountAmount)
return finalPrice, discountAmount, downloadedList, nil
}
```
### 6.4 example.json 文件生成算法
```go
// GenerateExampleJSON 生成 example.json 文件内容
func (s *ComponentReportServiceImpl) GenerateExampleJSON(ctx context.Context, productID string, subProductCodes []string) ([]byte, error) {
// 1. 获取产品信息(包括子产品)
product, err := s.productRepo.GetByID(ctx, productID)
if err != nil {
return nil, err
}
// 2. 构建 example.json 数组
var examples []map[string]interface{}
if product.IsPackage {
// 组合包:遍历子产品
for sort, item := range product.PackageItems {
// 只处理在 subProductCodes 列表中的子产品
if !contains(subProductCodes, item.ProductCode) {
continue
}
// 获取子产品的文档信息(包含响应示例)
subProduct, err := s.productRepo.GetByID(ctx, item.ProductID)
if err != nil {
s.logger.Warn("获取子产品信息失败", zap.String("product_id", item.ProductID), zap.Error(err))
continue
}
// 获取响应示例数据(优先级:文档 > API配置 > 默认值)
var responseData interface{}
responseData = s.extractResponseExample(ctx, subProduct)
// 构建示例项
example := map[string]interface{}{
"feature": map[string]interface{}{
"featureName": item.ProductName,
"sort": sort + 1,
},
"data": map[string]interface{}{
"apiID": item.ProductCode,
"data": responseData,
},
}
examples = append(examples, example)
}
} else {
// 单品
responseData := s.extractResponseExample(ctx, product)
example := map[string]interface{}{
"feature": map[string]interface{}{
"featureName": product.Name,
"sort": 1,
},
"data": map[string]interface{}{
"apiID": product.Code,
"data": responseData,
},
}
examples = append(examples, example)
}
// 3. 序列化为JSON
jsonData, err := json.MarshalIndent(examples, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化example.json失败: %w", err)
}
return jsonData, nil
}
// extractResponseExample 提取产品响应示例数据(优先级:文档 > API配置 > 默认值)
func (s *ComponentReportServiceImpl) extractResponseExample(ctx context.Context, product *entities.Product) interface{} {
var responseData interface{}
// 1. 优先从产品文档中获取
if product.Documentation != nil && product.Documentation.ResponseExample != "" {
// 尝试直接解析为JSON
err := json.Unmarshal([]byte(product.Documentation.ResponseExample), &responseData)
if err == nil {
return responseData
}
// 如果解析失败尝试从Markdown代码块中提取JSON
responseData = extractJSONFromMarkdown(product.Documentation.ResponseExample)
if responseData != nil {
return responseData
}
}
// 2. 如果文档中没有尝试从产品API配置中获取
apiConfig, err := s.productApiConfigRepo.GetByProductID(ctx, product.ID)
if err == nil && apiConfig != nil && apiConfig.ResponseExample != nil {
// API配置的响应示例通常是 map[string]interface{} 类型
return apiConfig.ResponseExample
}
// 3. 如果都没有,返回默认空对象
return map[string]interface{}{}
}
// extractJSONFromMarkdown 从Markdown代码块中提取JSON
func extractJSONFromMarkdown(markdown string) interface{} {
// 查找 ```json 代码块
re := regexp.MustCompile("(?s)```json\\s*(.*?)\\s*```")
matches := re.FindStringSubmatch(markdown)
if len(matches) > 1 {
var jsonData interface{}
err := json.Unmarshal([]byte(matches[1]), &jsonData)
if err == nil {
return jsonData
}
}
// 如果提取失败,返回 nil由调用者决定默认值
return nil
}
```
### 6.5 ZIP文件生成算法
```go
// GenerateZipFile 生成ZIP文件
func (s *ComponentReportServiceImpl) GenerateZipFile(ctx context.Context, productID string, subProductCodes []string, cacheKey string) (string, string, error) {
// 1. 检查缓存文件是否存在
cacheDir := "storage/component-reports"
zipPath := filepath.Join(cacheDir, cacheKey+".zip")
if _, err := os.Stat(zipPath); err == nil {
// 文件已存在,计算哈希值
hash, err := calculateFileHash(zipPath)
if err == nil {
return zipPath, hash, nil
}
}
// 2. 创建ZIP文件
zipFile, err := os.Create(zipPath)
if err != nil {
return "", "", err
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
// 3. 遍历子产品添加UI组件文件到ZIP
basePath := "resources/Pure_Component/src/ui"
for _, productCode := range subProductCodes {
path, fileType, err := s.MatchProductCodeToPath(ctx, productCode)
if err != nil {
continue // 跳过未找到的文件
}
if fileType == "folder" {
// 递归添加文件夹
err = addFolderToZip(zipWriter, path, basePath)
} else {
// 添加单个文件
err = addFileToZip(zipWriter, path, basePath)
}
if err != nil {
s.logger.Warn("添加文件到ZIP失败", zap.String("path", path), zap.Error(err))
}
}
// 4. 生成并添加 example.json 文件
exampleJSON, err := s.GenerateExampleJSON(ctx, productID, subProductCodes)
if err != nil {
s.logger.Warn("生成example.json失败", zap.Error(err))
// 不阻断流程继续生成ZIP
} else {
// 添加 example.json 到 ZIP 的 public 目录
exampleWriter, err := zipWriter.Create("public/example.json")
if err != nil {
s.logger.Warn("创建example.json文件失败", zap.Error(err))
} else {
_, err = exampleWriter.Write(exampleJSON)
if err != nil {
s.logger.Warn("写入example.json失败", zap.Error(err))
}
}
}
// 5. 添加其他必要的文件(如果需要)
// 例如:复制 public 目录下的其他文件(如果有)
publicBasePath := "resources/Pure_Component/public"
publicFiles, err := os.ReadDir(publicBasePath)
if err == nil {
for _, file := range publicFiles {
// 跳过 example.json已经生成
if file.Name() == "example.json" {
continue
}
filePath := filepath.Join(publicBasePath, file.Name())
if !file.IsDir() {
err = addFileToZip(zipWriter, filePath, publicBasePath)
if err != nil {
s.logger.Warn("添加public文件失败", zap.String("file", file.Name()), zap.Error(err))
}
}
}
}
// 6. 计算文件哈希值
hash, err := calculateFileHash(zipPath)
if err != nil {
return "", "", err
}
return zipPath, hash, nil
}
```
## 七、支付流程集成
### 7.1 支付订单创建
在创建报告支付订单时,需要:
1. 创建充值记录RechargeRecord类型为 `component_report`
2. 创建支付订单(微信/支付宝)
3. 创建下载记录ComponentReportDownload状态为 `pending`
### 7.2 支付成功回调
支付成功后:
1. 更新充值记录状态
2. 更新下载记录状态为 `success`
3. 生成ZIP文件包括UI组件文件和 `public/example.json`
4. 更新下载记录的 `file_path` 和 `file_hash`
5. 设置 `expires_at`30天后
### 7.3 二次下载
用户已支付过的报告:
1. 检查下载记录是否存在且未过期
2. 如果ZIP文件存在直接返回
3. 如果ZIP文件不存在重新生成使用相同的cache_key
## 八、性能优化策略
### 8.1 缓存策略
1. **文件匹配缓存**:产品编号到文件路径的映射缓存到数据库
2. **ZIP文件缓存**生成的ZIP文件保存到 `storage/component-reports` 目录
3. **缓存键生成**基于产品ID和子产品编号列表生成唯一缓存键
### 8.2 异步处理
对于大文件或包含多个子产品的组合包:
1. 支付成功后异步生成ZIP文件
2. 生成完成后通知用户(可选:邮件/站内消息)
3. 用户下载时检查文件是否已生成完成
### 8.3 文件清理
定期清理过期的ZIP文件
1. 下载记录过期后expires_at + 7天删除ZIP文件
2. 使用定时任务cron定期执行清理
## 九、错误处理
### 9.1 文件未找到
- 如果某个子产品的报告文件未找到,记录警告日志,但不影响其他文件的打包
- 在响应中返回未找到的文件列表
### 9.4 example.json 生成失败
- 如果某个子产品没有响应示例数据,使用空对象 `{}` 作为默认值
- 如果响应示例解析失败尝试从Markdown代码块中提取JSON
- 如果所有子产品都没有响应示例,生成包含空数据的 `example.json` 文件
- 记录警告日志但不阻断ZIP文件生成流程
### 9.2 支付失败
- 支付失败时,下载记录状态保持为 `pending`
- 用户可以重新发起支付
### 9.3 文件生成失败
- 记录错误日志
- 通知管理员
- 用户可以联系客服处理
## 十、测试要点
1. **匹配算法测试**
- 精确匹配
- 模糊匹配(前缀、后缀)
- 未找到文件的情况
2. **价格计算测试**
- 单品价格计算
- 组合包价格计算
- 已下载产品价格减免
3. **example.json 生成测试**
- 单品 example.json 生成
- 组合包 example.json 生成(多个子产品)
- 响应示例数据提取JSON格式、Markdown代码块格式
- 缺少响应示例时的默认值处理
4. **支付流程测试**
- 创建支付订单
- 支付成功回调
- 支付失败处理
5. **下载功能测试**
- 首次下载
- 二次下载
- 文件过期处理
6. **性能测试**
- 大文件生成时间
- 并发下载处理
- 缓存命中率
## 十一、部署注意事项
1. **目录权限**:确保 `storage/component-reports` 目录有写权限
2. **存储空间**ZIP文件可能较大需要足够的磁盘空间
3. **备份策略**:定期备份下载记录和缓存表
4. **监控告警**:监控文件生成失败、支付回调异常等情况
## 十二、后续优化方向
1. **CDN加速**将ZIP文件上传到CDN加速下载
2. **分片下载**:对于超大文件,支持分片下载
3. **预览功能**:在下载前提供报告预览
4. **版本管理**:支持报告版本更新,用户可选择下载历史版本
5. **批量下载**:支持用户选择多个产品批量下载

View File

@@ -0,0 +1,340 @@
# 🚀 新日志系统使用指南
## 概述
本项目已重新设计日志系统,完全基于 **Zap 官方最佳实践**,提供高性能、结构化的日志记录功能。
## ✨ 核心特性
### 🎯 **基于 Zap 官方推荐**
- 使用 `zap.Config` 和官方配置结构
- 支持 `zap.NewProductionConfig()``zap.NewDevelopmentConfig()`
- 完整的编码器配置和输出配置
### 📁 **灵活的日志输出**
- **控制台输出**: `stdout`, `stderr`
- **文件输出**: 支持日志轮转和压缩
- **按日分包**: 自动按日期创建目录
- **按级别分文件**: 不同级别写入不同文件
### 🔧 **智能配置**
- 根据环境自动选择最佳配置
- 支持选项模式配置
- 完整的默认值设置
## 🏗️ 架构设计
```
应用代码 → Logger接口 → LoggerFactory → Zap核心 → 输出目标
```
### **核心组件**
1. **`Logger` 接口**: 统一的日志接口
2. **`ZapLogger`**: 基于 Zap 的日志实现
3. **`LevelLogger`**: 级别分文件日志器
4. **`LoggerFactory`**: 日志器工厂,支持多种创建方式
## 📖 使用方法
### **1. 基础使用**
```go
import "tyapi-server/internal/shared/logger"
// 创建日志器
log, err := logger.NewLogger(logger.Config{
Level: "info",
Format: "json",
Output: "file",
LogDir: "logs",
UseDaily: true,
})
if err != nil {
panic(err)
}
// 记录日志
log.Info("应用启动成功",
logger.String("version", "1.0.0"),
logger.String("environment", "production"),
)
```
### **2. 使用日志工厂**
```go
// 创建工厂
factory := logger.NewLoggerFactory(logger.Config{
Level: "info",
Format: "json",
Output: "file",
Development: false, // 生产环境
})
// 创建生产环境日志器
prodLogger, err := factory.CreateProductionLogger()
if err != nil {
panic(err)
}
// 创建开发环境日志器
devLogger, err := factory.CreateDevelopmentLogger()
if err != nil {
panic(err)
}
// 根据环境自动选择
autoLogger, err := factory.CreateLoggerByEnvironment()
if err != nil {
panic(err)
}
```
### **3. 选项模式配置**
```go
// 使用选项模式
logger, err := factory.CreateLoggerWithOptions(
logger.WithLevel("debug"),
logger.WithFormat("console"),
logger.WithOutput("stdout"),
logger.WithDevelopment(true),
logger.WithColor(true),
)
```
### **4. 级别分文件日志器**
```go
// 创建级别分文件日志器
levelConfig := logger.LevelLoggerConfig{
BaseConfig: logger.Config{
Level: "info",
Format: "json",
Output: "file",
LogDir: "logs",
UseDaily: true,
},
EnableLevelSeparation: true,
LevelConfigs: map[zapcore.Level]logger.LevelFileConfig{
zapcore.InfoLevel: {
MaxSize: 100,
MaxBackups: 5,
MaxAge: 30,
Compress: true,
},
zapcore.ErrorLevel: {
MaxSize: 200,
MaxBackups: 10,
MaxAge: 90,
Compress: true,
},
},
}
levelLogger, err := logger.NewLevelLogger(levelConfig)
if err != nil {
panic(err)
}
// 不同级别的日志会写入不同文件
levelLogger.Info("这是一条信息日志") // 写入 logs/2024-01-01/info.log
levelLogger.Error("这是一条错误日志") // 写入 logs/2024-01-01/error.log
```
### **5. 结构化日志**
```go
// 使用 With 添加字段
userLogger := log.With(
logger.String("user_id", "12345"),
logger.String("action", "login"),
)
userLogger.Info("用户登录成功",
logger.String("ip", "192.168.1.1"),
logger.String("user_agent", "Mozilla/5.0..."),
)
// 使用 WithContext 从上下文提取字段
ctx := context.WithValue(context.Background(), "request_id", "req_123")
ctx = context.WithValue(ctx, "user_id", "user_456")
ctx = context.WithValue(ctx, "trace_id", "trace_789")
contextLogger := log.WithContext(ctx)
contextLogger.Info("处理请求",
logger.String("endpoint", "/api/users"),
logger.Int("status_code", 200),
)
```
### **6. 命名日志器**
```go
// 创建命名日志器
dbLogger := log.Named("database")
dbLogger.Info("数据库连接成功",
logger.String("host", "localhost"),
logger.String("database", "tyapi"),
)
apiLogger := log.Named("api")
apiLogger.Info("API 请求处理",
logger.String("method", "GET"),
logger.String("path", "/api/v1/users"),
)
```
## ⚙️ 配置说明
### **基础配置**
```yaml
logger:
# 基础配置
level: "info" # 日志级别
format: "json" # 输出格式
output: "file" # 输出方式
log_dir: "logs" # 日志目录
use_daily: true # 是否按日分包
use_color: false # 是否使用彩色输出
# 文件配置
max_size: 100 # 单个文件最大大小(MB)
max_backups: 5 # 最大备份文件数
max_age: 30 # 最大保留天数
compress: true # 是否压缩
# 高级功能
enable_level_separation: true # 是否启用按级别分文件
development: true # 是否为开发环境
```
### **级别配置**
```yaml
logger:
level_configs:
debug:
max_size: 50 # 50MB
max_backups: 3
max_age: 7 # 7天
compress: true
info:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
error:
max_size: 200 # 200MB
max_backups: 10
max_age: 90 # 90天
compress: true
```
## 🔍 日志级别
### **级别说明**
- **`debug`**: 调试信息,开发环境使用
- **`info`**: 一般信息,记录应用状态
- **`warn`**: 警告信息,需要注意但不影响运行
- **`error`**: 错误信息,操作失败但可恢复
- **`fatal`**: 致命错误,应用无法继续运行
- **`panic`**: 恐慌错误,程序崩溃
### **级别选择建议**
- **开发环境**: `debug``info`
- **测试环境**: `info``warn`
- **生产环境**: `warn``error`
## 📊 日志格式
### **JSON 格式(推荐)**
```json
{
"level": "info",
"timestamp": "2024-01-01T12:00:00.000Z",
"logger": "main",
"caller": "main.go:25",
"message": "应用启动成功",
"version": "1.0.0",
"environment": "production"
}
```
### **Console 格式(开发环境)**
```
2024-01-01T12:00:00.000Z INFO main/main.go:25 应用启动成功 {"version": "1.0.0", "environment": "production"}
```
## 🚀 性能优化
### **Zap 官方推荐**
- 使用结构化字段而不是字符串拼接
- 避免在日志记录时进行复杂计算
- 合理设置日志级别,避免过度记录
### **最佳实践**
```go
// ✅ 推荐:使用结构化字段
log.Info("用户操作",
logger.String("user_id", userID),
logger.String("action", action),
logger.String("resource", resource),
)
// ❌ 不推荐:字符串拼接
log.Info(fmt.Sprintf("用户 %s 执行了 %s 操作,资源: %s", userID, action, resource))
```
## 🔧 故障排除
### **常见问题**
1. **日志文件未创建**
- 检查目录权限
- 确认配置中的 `log_dir` 路径
2. **日志级别不生效**
- 检查配置中的 `level`
- 确认日志器创建时使用了正确的配置
3. **按级别分文件不工作**
- 确认 `enable_level_separation: true`
- 检查 `level_configs` 配置
4. **按日分包不工作**
- 确认 `use_daily: true`
- 检查日期格式是否正确
### **调试技巧**
```go
// 启用调试模式
log.SetLevel(zapcore.DebugLevel)
// 检查日志器配置
if zapLogger, ok := log.(*logger.ZapLogger); ok {
config := zapLogger.GetConfig()
fmt.Printf("日志器配置: %+v\n", config)
}
```
## 📚 更多资源
- [Zap 官方文档](https://pkg.go.dev/go.uber.org/zap)
- [Zap 最佳实践](https://github.com/uber-go/zap/blob/master/FAQ.md)
- [结构化日志指南](https://github.com/uber-go/zap/blob/master/FAQ.md#q-how-do-i-choose-between-the-json-and-console-encoders)
## 🎉 总结
新的日志系统完全基于 Zap 官方最佳实践,提供了:
1. **高性能**: Zap 是 Go 生态中性能最好的日志库
2. **结构化**: 支持结构化字段,便于日志分析
3. **灵活性**: 支持多种输出方式和配置选项
4. **生产就绪**: 支持日志轮转、压缩、清理等生产环境需求
5. **官方推荐**: 完全按照 Zap 官方文档实现,确保最佳实践
使用新的日志系统,您将获得更好的性能、更清晰的日志结构和更强大的功能!

View File

@@ -0,0 +1,321 @@
# 📝 日志系统配置示例
## 概述
新的日志系统完全基于配置文件,所有配置都在 `config.yaml` 中设置,容器代码保持简洁。
## 🎯 完整配置示例
```yaml
# 🚀 日志系统配置 - 基于 Zap 官方推荐
logger:
# 基础配置
level: "info" # 日志级别: debug, info, warn, error, fatal, panic
format: "json" # 输出格式: json, console
output: "file" # 输出方式: stdout, stderr, file
log_dir: "logs" # 日志目录
use_daily: true # 是否按日分包
use_color: false # 是否使用彩色输出仅console格式有效
# 文件配置
max_size: 100 # 单个文件最大大小(MB)
max_backups: 5 # 最大备份文件数
max_age: 30 # 最大保留天数
compress: true # 是否压缩
# 高级功能
enable_level_separation: true # 是否启用按级别分文件
enable_request_logging: true # 是否启用请求日志
enable_performance_log: true # 是否启用性能日志
# 各级别配置(按级别分文件时使用)
level_configs:
debug:
max_size: 50 # 50MB
max_backups: 3
max_age: 7 # 7天
compress: true
info:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
warn:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
error:
max_size: 200 # 200MB
max_backups: 10
max_age: 90 # 90天
compress: true
fatal:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
panic:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
```
## 🔧 环境特定配置
### 开发环境配置
```yaml
# 开发环境 - 详细日志,控制台输出
logger:
level: "debug"
format: "console"
output: "stdout"
use_daily: false
use_color: true
enable_level_separation: false
development: true
```
### 生产环境配置
```yaml
# 生产环境 - 精简日志,文件输出
logger:
level: "warn"
format: "json"
output: "file"
log_dir: "/app/logs"
use_daily: true
use_color: false
enable_level_separation: true
development: false
# 生产环境文件配置
max_size: 200
max_backups: 10
max_age: 90
compress: true
# 生产环境级别配置
level_configs:
warn:
max_size: 200
max_backups: 10
max_age: 90
compress: true
error:
max_size: 500
max_backups: 20
max_age: 180
compress: true
fatal:
max_size: 100
max_backups: 10
max_age: 365
compress: true
```
## 📊 配置字段说明
### 基础配置字段
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `level` | string | 日志级别 | "info" |
| `format` | string | 输出格式 | "json" |
| `output` | string | 输出方式 | "stdout" |
| `log_dir` | string | 日志目录 | "logs" |
| `use_daily` | bool | 是否按日分包 | false |
| `use_color` | bool | 是否使用彩色输出 | false |
### 文件配置字段
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `max_size` | int | 单个文件最大大小(MB) | 100 |
| `max_backups` | int | 最大备份文件数 | 5 |
| `max_age` | int | 最大保留天数 | 30 |
| `compress` | bool | 是否压缩 | true |
### 高级功能字段
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `enable_level_separation` | bool | 是否启用按级别分文件 | false |
| `enable_request_logging` | bool | 是否启用请求日志 | false |
| `enable_performance_log` | bool | 是否启用性能日志 | false |
### 级别配置字段
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `max_size` | int | 单个文件最大大小(MB) | 无 |
| `max_backups` | int | 最大备份文件数 | 无 |
| `max_age` | int | 最大保留天数 | 无 |
| `compress` | bool | 是否压缩 | 无 |
## 🚀 配置最佳实践
### 1. 按环境配置
```yaml
# 开发环境
logger:
level: "debug"
format: "console"
output: "stdout"
use_color: true
enable_level_separation: false
# 测试环境
logger:
level: "info"
format: "json"
output: "file"
use_daily: true
enable_level_separation: true
# 生产环境
logger:
level: "warn"
format: "json"
output: "file"
use_daily: true
enable_level_separation: true
max_size: 200
max_backups: 10
max_age: 90
```
### 2. 按级别配置
```yaml
logger:
enable_level_separation: true
level_configs:
# 调试日志 - 小文件,短期保留
debug:
max_size: 50
max_backups: 3
max_age: 7
compress: true
# 信息日志 - 中等文件,中期保留
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# 警告日志 - 中等文件,中期保留
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# 错误日志 - 大文件,长期保留
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
# 致命错误 - 中等文件,长期保留
fatal:
max_size: 100
max_backups: 10
max_age: 365
compress: true
```
### 3. 性能优化配置
```yaml
logger:
# 生产环境性能优化
level: "warn" # 只记录警告及以上级别
format: "json" # JSON格式便于日志分析
output: "file" # 文件输出避免控制台性能影响
use_daily: true # 按日分包便于管理
enable_level_separation: true # 按级别分文件便于分析
# 文件轮转优化
max_size: 200 # 较大的文件大小减少轮转频率
max_backups: 10 # 适中的备份数量
max_age: 90 # 90天保留期平衡存储和分析需求
compress: true # 启用压缩节省存储空间
```
## 🔍 配置验证
### 1. 必需字段
以下字段是必需的,如果未设置将使用默认值:
- `level`: 日志级别
- `format`: 输出格式
- `output`: 输出方式
### 2. 可选字段
以下字段是可选的,如果未设置将使用系统默认值:
- `log_dir`: 日志目录
- `use_daily`: 是否按日分包
- `use_color`: 是否使用彩色输出
- `max_size`: 文件大小限制
- `max_backups`: 备份文件数量
- `max_age`: 文件保留天数
- `compress`: 是否压缩
### 3. 条件字段
以下字段在特定条件下是必需的:
-`output: "file"` 时,建议设置 `log_dir`
-`enable_level_separation: true` 时,建议设置 `level_configs`
-`format: "console"` 时,`use_color` 才有效
## 📝 配置迁移指南
### 从旧配置迁移
如果您之前使用的是旧版本的日志配置,可以按照以下方式迁移:
#### 旧配置
```yaml
# 旧版本配置
logging:
level: "info"
file: "logs/app.log"
max_size: 100
max_backups: 3
```
#### 新配置
```yaml
# 新版本配置
logger:
level: "info"
output: "file"
log_dir: "logs"
max_size: 100
max_backups: 3
format: "json"
use_daily: false
```
## 🎉 总结
新的日志系统配置完全基于 `config.yaml` 文件,具有以下特点:
1. **配置集中**: 所有日志配置都在一个地方管理
2. **环境友好**: 支持不同环境的配置
3. **灵活性强**: 支持按级别分文件、按日分包等高级功能
4. **性能优化**: 基于 Zap 官方最佳实践
5. **易于维护**: 配置结构清晰,易于理解和修改
通过合理的配置,您可以获得高性能、结构化的日志系统,满足开发、测试和生产环境的各种需求!

View File

@@ -0,0 +1,347 @@
# 短信接口签名验证使用指南
## 概述
为了防止短信发送接口被恶意刷取系统实现了基于HMAC-SHA256的签名验证机制。所有发送短信的请求必须包含有效的签名否则请求将被拒绝。
## 工作原理
1. **前端生成签名**使用密钥对请求参数进行HMAC-SHA256签名
2. **后端验证签名**:后端使用相同密钥重新计算签名并比对
3. **时间戳验证**防止重放攻击时间戳必须在5分钟内有效
4. **随机数验证**每次请求必须包含唯一的随机字符串nonce
5. **参数编码传输**推荐将所有参数包括签名编码成Base64字符串后传输隐藏参数结构增加安全性
## 配置说明
### 后端配置
`config.yaml` 中配置签名相关参数:
```yaml
sms:
# ... 其他配置 ...
# 签名验证配置
signature_enabled: true # 是否启用签名验证
signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥)
```
**重要提示**
- 生产环境必须修改 `signature_secret` 为复杂的随机字符串
- 密钥长度建议至少32个字符
- 密钥应包含大小写字母、数字和特殊字符
## 签名算法
### 1. 构建待签名字符串
将请求参数(排除`signature`字段按key排序拼接成以下格式
```
key1=value1&key2=value2&timestamp=1234567890&nonce=random_string
```
### 2. 计算HMAC-SHA256签名
使用配置的密钥对待签名字符串进行HMAC-SHA256计算结果转换为hex编码。
### 3. 请求参数
系统支持两种请求方式:
#### 方式1直接传递参数
发送请求时直接传递所有字段:
```json
{
"phone": "13800138000",
"scene": "register",
"timestamp": 1704067200,
"nonce": "a1b2c3d4e5f6g7h8",
"signature": "abc123def456..."
}
```
#### 方式2编码后传输推荐更安全
将所有参数包括签名编码成Base64字符串后传输只传递一个`data`字段:
```json
{
"data": "eyJwaG9uZSI6IjEzODAwMTM4MDAwIiwic2NlbmUiOiJyZWdpc3RlciIsInRpbWVzdGFtcCI6MTcwNDA2NzIwMCwibm9uY2UiOiJhMWIyYzNkNGE1ZjYiLCJzaWduYXR1cmUiOiJhYmMxMjNkZWY0NTYifQ=="
}
```
**编码传输的优势**
- 隐藏参数结构,增加破解难度
- 参数不可见,防止参数被直接修改
- 增加一层编码保护
## 前端实现
### Node.js 示例
参考文件:`tyapi-frontend/public/examples/nodejs/sms_signature_demo.js`
#### 方式1直接传递参数
```javascript
const crypto = require('crypto');
function generateSignature(params, secretKey, timestamp, nonce) {
// 1. 构建待签名字符串
const keys = Object.keys(params)
.filter(k => k !== 'signature')
.sort();
const parts = keys.map(k => `${k}=${params[k]}`);
parts.push(`timestamp=${timestamp}`);
parts.push(`nonce=${nonce}`);
const signString = parts.join('&');
// 2. 计算HMAC-SHA256签名
const signature = crypto
.createHmac('sha256', secretKey)
.update(signString)
.digest('hex');
return signature;
}
// 使用示例
const params = { phone: '13800138000', scene: 'register' };
const timestamp = Math.floor(Date.now() / 1000);
const nonce = generateRandomString(16);
const secretKey = 'your_secret_key';
const signature = generateSignature(params, secretKey, timestamp, nonce);
// 发送请求
const requestBody = {
phone: '13800138000',
scene: 'register',
timestamp: timestamp,
nonce: nonce,
signature: signature,
};
```
#### 方式2编码后传输推荐
```javascript
// 1. 生成签名(同上)
const params = { phone: '13800138000', scene: 'register' };
const timestamp = Math.floor(Date.now() / 1000);
const nonce = generateRandomString(16);
const secretKey = 'your_secret_key';
const signature = generateSignature(params, secretKey, timestamp, nonce);
// 2. 构建包含所有参数的JSON对象
const allParams = {
phone: '13800138000',
scene: 'register',
timestamp: timestamp,
nonce: nonce,
signature: signature,
};
// 3. 编码为Base64
const jsonString = JSON.stringify(allParams);
const encodedData = Buffer.from(jsonString).toString('base64');
// 4. 发送请求只传递data字段
const requestBody = {
data: encodedData,
};
```
### 浏览器 JavaScript 示例
参考文件:`tyapi-frontend/public/examples/javascript/sms_signature_demo.js`
#### 方式1直接传递参数
```javascript
async function generateSignature(params, secretKey, timestamp, nonce) {
// 1. 构建待签名字符串
const keys = Object.keys(params)
.filter(k => k !== 'signature')
.sort();
const parts = keys.map(k => `${k}=${params[k]}`);
parts.push(`timestamp=${timestamp}`);
parts.push(`nonce=${nonce}`);
const signString = parts.join('&');
// 2. 使用Web Crypto API计算HMAC-SHA256签名
const encoder = new TextEncoder();
const keyData = encoder.encode(secretKey);
const messageData = encoder.encode(signString);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
const hashArray = Array.from(new Uint8Array(signature));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
// 使用示例
const params = { phone: '13800138000', scene: 'register' };
const timestamp = Math.floor(Date.now() / 1000);
const nonce = generateNonce(16);
const secretKey = 'your_secret_key';
const signature = await generateSignature(params, secretKey, timestamp, nonce);
// 发送请求
const requestBody = {
phone: '13800138000',
scene: 'register',
timestamp: timestamp,
nonce: nonce,
signature: signature,
};
```
#### 方式2编码后传输推荐
```javascript
// 1. 生成签名(同上)
const params = { phone: '13800138000', scene: 'register' };
const timestamp = Math.floor(Date.now() / 1000);
const nonce = generateNonce(16);
const secretKey = 'your_secret_key';
const signature = await generateSignature(params, secretKey, timestamp, nonce);
// 2. 构建包含所有参数的JSON对象
const allParams = {
phone: '13800138000',
scene: 'register',
timestamp: timestamp,
nonce: nonce,
signature: signature,
};
// 3. 编码为Base64浏览器环境
const jsonString = JSON.stringify(allParams);
const encodedData = btoa(unescape(encodeURIComponent(jsonString)));
// 4. 发送请求只传递data字段
const requestBody = {
data: encodedData,
};
```
## 密钥隐藏策略
由于前端代码可以被查看,完全隐藏密钥是不可能的。但可以通过以下方式增加破解难度:
### 1. 字符串拆分和拼接
```javascript
function getSecretKey() {
const part1 = 'TyApi2024';
const part2 = 'SMSSecret';
const part3 = 'Key!@#$%^';
return part1 + part2 + part3;
}
```
### 2. 字符数组拼接
```javascript
function getSecretKey() {
const chars = ['T', 'y', 'A', 'p', 'i', ...];
return chars.join('');
}
```
### 3. Base64编码混淆
```javascript
function getSecretKey() {
const encoded = 'base64_encoded_string';
return atob(encoded);
}
```
### 4. 代码混淆
使用构建工具如webpack、rollup等进行代码混淆和压缩使密钥更难被发现。
### 5. 后端代理(推荐)
将签名逻辑放在后端代理接口中,前端只调用代理接口,不直接包含密钥。
## 安全建议
1. **定期更换密钥**建议每3-6个月更换一次签名密钥
2. **监控异常请求**:监控签名验证失败的请求,及时发现攻击行为
3. **结合其他防护措施**
- IP限流
- 设备指纹识别
- 验证码(图形验证码)
- 行为分析
4. **日志记录**记录所有签名验证失败的请求包括IP、User-Agent等信息
## 错误处理
### 常见错误
1. **签名字段缺失**:返回 `"签名字段缺失"`
2. **时间戳无效**:返回 `"时间戳无效"`
3. **请求已过期**:返回 `"请求已过期,时间戳超出容差范围"`
4. **签名验证失败**:返回 `"签名验证失败"`
### 时间戳容差
系统允许的时间戳容差为 **5分钟**300秒。如果请求时间戳与服务器时间差超过5分钟请求将被拒绝。
## 测试
### 测试签名生成
```bash
# 使用Node.js示例
node tyapi-frontend/public/examples/nodejs/sms_signature_demo.js
```
### 测试API调用
```bash
curl -X POST http://localhost:8080/api/v1/users/send-code \
-H "Content-Type: application/json" \
-d '{
"phone": "13800138000",
"scene": "register",
"timestamp": 1704067200,
"nonce": "a1b2c3d4e5f6g7h8",
"signature": "计算得到的签名"
}'
```
## 注意事项
1. **时间同步**:确保客户端和服务器时间同步,避免时间戳验证失败
2. **随机数唯一性**每次请求的nonce应该是唯一的可以使用UUID或时间戳+随机数
3. **密钥安全**:生产环境密钥不要提交到代码仓库,应使用环境变量或密钥管理服务
4. **向后兼容**:如果需要在开发环境禁用签名验证,可以设置 `signature_enabled: false`
## 相关文件
- 后端签名工具:`internal/shared/crypto/signature.go`
- 后端Handler`internal/infrastructure/http/handlers/user_handler.go`
- 配置结构:`internal/config/config.go`
- Node.js示例`tyapi-frontend/public/examples/nodejs/sms_signature_demo.js`
- 浏览器示例:`tyapi-frontend/public/examples/javascript/sms_signature_demo.js`

View File

@@ -0,0 +1,176 @@
# 组合包动态处理机制说明
## 🎉 重大更新
组合包系统现在支持**动态处理机制**!这意味着:
-**零编码**大部分组合包无需编写任何Go代码
-**配置驱动**:只需在数据库配置即可立即使用
-**灵活扩展**:特殊需求仍可通过自定义处理器实现
## 🔧 工作原理
### 处理优先级
1. **自定义处理器优先**:如果注册了专门的处理器,优先使用
2. **通用处理器兜底**COMB开头的API自动使用通用组合包处理器
3. **数据库驱动**:根据数据库配置自动调用子产品处理器
### 系统架构
```
API请求 (COMBXXXX)
优先查找自定义处理器
↓ (未找到)
检查是否COMB开头
↓ (是)
通用组合包处理器
查询数据库获取子产品配置
并发调用子产品处理器
聚合结果并返回
```
## 📋 使用方法
### 方案1纯配置组合包推荐
**步骤1创建组合包产品**
```sql
INSERT INTO products (
id, code, name, description,
is_package, is_enabled, is_visible,
price, category_id
) VALUES (
'uuid1', 'COMB1234', '身份验证组合包', '包含身份证二要素和手机三要素验证',
true, true, true,
5.00, 'category_id'
);
```
**步骤2配置子产品**
```sql
INSERT INTO product_package_items (package_id, product_id, sort_order) VALUES
('uuid1', 'product_id_1', 1), -- FLXG162A 身份证二要素
('uuid1', 'product_id_2', 2); -- FLXG54F5 手机三要素
```
**步骤3直接使用**
```bash
# 立即可用,无需任何代码编写!
POST /api/v1/COMB1234
{
"id_card": "123456789012345678",
"name": "张三",
"mobile_no": "13800138000"
}
```
### 方案2自定义逻辑组合包
如果需要对结果进行后处理,才需要编写代码:
**步骤1创建处理器文件**
```go
// internal/domains/api/services/processors/comb/comb1234_processor.go
func ProcessCOMB1234Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
// 参数验证
var paramsDto dto.COMB1234Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 调用组合包服务
combinedResult, err := deps.CombService.ProcessCombRequest(ctx, params, deps, "COMB1234")
if err != nil {
return nil, err
}
// 自定义后处理逻辑
for _, resp := range combinedResult.Responses {
if resp.ApiCode == "FLXG162A" && resp.Success {
// 添加自定义字段
if data, ok := resp.Data.(map[string]interface{}); ok {
data["processed_by"] = "COMB1234"
}
}
}
return json.Marshal(combinedResult)
}
```
**步骤2注册处理器**
```go
// api_request_service.go 中添加
"COMB1234": comb.ProcessCOMB1234Request, // 有自定义逻辑
```
**步骤3添加DTO如需要**
```go
// dto/api_request_dto.go 中添加
type COMB1234Req struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
}
```
## 📊 现有组合包状态
### ✅ 已迁移到动态处理
- **COMB298Y**:删除了专门的处理器文件,现在使用通用处理器
### 🔧 保留自定义处理器
- **COMB86PM**有特殊逻辑重命名ApiCode保留自定义处理器
## 🎯 响应格式
所有组合包都返回统一的响应格式:
```json
{
"responses": [
{
"api_code": "FLXG162A",
"success": true,
"data": {
// 子产品的响应数据
}
},
{
"api_code": "FLXG54F5",
"success": true,
"data": {
// 子产品的响应数据
}
},
{
"api_code": "YYSY4B37",
"success": false,
"error": "数据源异常"
}
]
}
```
## 🚀 优势总结
1. **开发效率**90%的组合包无需编写代码
2. **维护成本**:减少重复代码,统一处理逻辑
3. **业务灵活**:数据库配置即时生效
4. **向后兼容**:现有自定义处理器继续工作
5. **扩展性强**:特殊需求仍可通过自定义处理器实现
## ⚡ 性能特性
- **并发处理**:所有子产品并发调用
- **独立失败**:单个子产品失败不影响其他
- **智能排序**通过sort_order控制响应顺序
- **错误隔离**:每个子产品的错误独立处理
现在,创建一个新的组合包就像配置数据库一样简单!🎉

View File

@@ -0,0 +1,383 @@
# 🚀 西部数据日志系统使用指南
## 概述
西部数据服务现在集成了完整的日志记录系统,支持请求、响应、错误和性能四种类型的日志记录,每种类型都有独立的日志文件。
## 📝 日志记录范围
### ✅ **会记录日志的操作**
- `CallAPI()` - 调用西部数据API
- `G05HZ01CallAPI()` - 调用G05HZ01 API
### ❌ **不会记录日志的操作**
- `Encrypt()` - 内部加密方法
- `Md5Encrypt()` - 内部MD5加密方法
**说明**: 加密解密是内部工具方法调用频率高且不涉及外部API因此不记录日志以提高性能。日志系统专注于记录外部API调用的完整生命周期。
## ✨ 核心特性
### 1. **四种日志类型**
- **请求日志**: 记录所有API请求的详细信息
- **响应日志**: 记录所有API响应的详细信息
- **错误日志**: 记录所有错误和异常情况
- **性能日志**: 记录请求耗时和性能指标
### 2. **日志文件分离**
```
logs/
├── westdex/
│ ├── 2024-01-01/
│ │ ├── request.log # 请求日志
│ │ ├── response.log # 响应日志
│ │ ├── error.log # 错误日志
│ │ └── performance.log # 性能日志
│ └── 2024-01-02/
│ ├── request.log
│ ├── response.log
│ ├── error.log
│ └── performance.log
```
### 3. **请求追踪**
- 每个请求都有唯一的请求ID
- 请求和响应日志通过请求ID关联
- 支持完整的请求链路追踪
### 4. **性能监控**
- 记录每个API调用的耗时
- 区分成功和失败的请求
- 提供性能分析数据
## 🏗️ 配置说明
### 配置文件设置
`config.yaml` 中添加西部数据日志配置:
```yaml
westdex:
url: "https://apimaster.westdex.com.cn/api/invoke"
key: "your-key"
secret_id: "your-secret-id"
secret_second_id: "your-secret-second-id"
# 西部数据日志配置
logging:
enabled: true # 启用日志记录
log_dir: "logs/westdex" # 日志目录
use_daily: true # 按日分包
enable_level_separation: true # 启用级别分离
# 各级别配置
level_configs:
request:
max_size: 100 # 100MB
max_backups: 5 # 5个备份
max_age: 30 # 30天
compress: true # 启用压缩
response:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200 # 错误日志文件更大
max_backups: 10 # 更多备份
max_age: 90 # 保留更久
compress: true
performance:
max_size: 100
max_backups: 5
max_age: 30
compress: true
```
## 🚀 使用方法
### 1. 使用配置创建服务
```go
package main
import (
"tyapi-server/internal/config"
"tyapi-server/internal/infrastructure/external/westdex"
)
func main() {
// 加载配置
cfg := &config.Config{
// ... 配置内容
}
// 使用配置创建西部数据服务
service, err := westdex.NewWestDexServiceWithConfig(cfg)
if err != nil {
panic(err)
}
// 使用服务
resp, err := service.CallAPI("G05HZ01", map[string]interface{}{
"param1": "value1",
"param2": "value2",
})
if err != nil {
// 错误会自动记录到错误日志
log.Printf("API调用失败: %v", err)
return
}
// 成功响应会自动记录到响应日志
log.Printf("API调用成功: %s", string(resp))
}
```
### 2. 使用自定义日志配置
```go
// 创建自定义日志配置
loggingConfig := westdex.WestDexLoggingConfig{
Enabled: true,
LogDir: "logs/custom_westdex",
UseDaily: true,
EnableLevelSeparation: true,
LevelConfigs: map[string]westdex.WestDexLevelFileConfig{
"request": {
MaxSize: 50,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
"error": {
MaxSize: 100,
MaxBackups: 5,
MaxAge: 30,
Compress: true,
},
},
}
// 使用自定义配置创建服务
service, err := westdex.NewWestDexServiceWithLogging(
"https://api.example.com",
"your-key",
"your-secret-id",
"your-secret-second-id",
loggingConfig,
)
```
## 📊 日志示例
### 请求日志示例
```json
{
"level": "info",
"msg": "西部数据API请求",
"request_id": "westdex_a1b2c3d4",
"api_code": "G05HZ01",
"url": "https://apimaster.westdex.com.cn/api/invoke/449159/G05HZ01?timestamp=1704096000000",
"request_data": {
"param1": "value1",
"param2": "value2"
},
"timestamp": "2024-01-01T12:00:00Z"
}
```
### 响应日志示例
```json
{
"level": "info",
"msg": "西部数据API响应",
"request_id": "westdex_a1b2c3d4",
"api_code": "G05HZ01",
"status_code": 200,
"response_data": "{\"code\":\"0000\",\"data\":\"...\",\"message\":\"success\"}",
"duration": "150ms",
"timestamp": "2024-01-01T12:00:01Z"
}
```
### 错误日志示例
```json
{
"level": "error",
"msg": "西部数据API错误",
"request_id": "westdex_a1b2c3d4",
"api_code": "G05HZ01",
"error": "数据源异常: 查询失败",
"request_data": {
"param1": "value1",
"param2": "value2"
},
"timestamp": "2024-01-01T12:00:01Z"
}
```
### 性能日志示例
```json
{
"level": "info",
"msg": "西部数据API性能",
"request_id": "westdex_a1b2c3d4",
"api_code": "G05HZ01",
"duration": "150ms",
"success": true,
"timestamp": "2024-01-01T12:00:01Z"
}
```
## 🔍 日志字段说明
### 通用字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `request_id` | string | 请求唯一标识符 |
| `api_code` | string | API代码 |
| `timestamp` | string | 时间戳ISO8601格式 |
### 请求日志字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `url` | string | 完整的请求URL |
| `request_data` | object | 请求数据 |
### 响应日志字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `status_code` | int | HTTP状态码 |
| `response_data` | string | 响应数据JSON字符串 |
| `duration` | duration | 请求耗时 |
### 错误日志字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `error` | error | 错误信息 |
| `request_data` | object | 原始请求数据 |
### 性能日志字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `duration` | duration | 请求耗时 |
| `success` | bool | 是否成功 |
## 🎯 最佳实践
### 1. **日志配置优化**
```yaml
# 生产环境配置
logging:
enabled: true
log_dir: "/var/log/westdex"
use_daily: true
enable_level_separation: true
level_configs:
request:
max_size: 200 # 更大的文件大小
max_backups: 10 # 更多备份
max_age: 90 # 保留更久
error:
max_size: 500 # 错误日志文件更大
max_backups: 20 # 更多备份
max_age: 180 # 保留更久
```
### 2. **日志分析**
```bash
# 查看请求日志
tail -f logs/westdex/2024-01-01/request.log
# 查看错误日志
tail -f logs/westdex/2024-01-01/error.log
# 查看性能日志
tail -f logs/westdex/2024-01-01/performance.log
# 统计API调用次数
grep -c "G05HZ01" logs/westdex/2024-01-01/request.log
# 统计错误率
grep -c "error" logs/westdex/2024-01-01/error.log
```
### 3. **性能监控**
```bash
# 查看平均响应时间
grep "duration" logs/westdex/2024-01-01/performance.log | \
awk -F'"duration":"' '{print $2}' | \
awk -F'"' '{print $1}' | \
awk -F'ms' '{sum+=$1; count++} END {print "平均响应时间: " sum/count "ms"}'
# 查看成功率
total=$(grep -c "success" logs/westdex/2024-01-01/performance.log)
success=$(grep -c '"success":true' logs/westdex/2024-01-01/performance.log)
echo "成功率: $((success * 100 / total))%"
```
## 🚨 注意事项
### 1. **日志文件管理**
- 定期清理旧日志文件
- 监控磁盘空间使用
- 配置合适的日志轮转策略
### 2. **敏感信息处理**
- 日志中可能包含敏感数据
- 确保日志文件访问权限
- 考虑日志脱敏需求
### 3. **性能影响**
- 日志记录会增加少量性能开销
- 异步日志记录可减少性能影响
- 合理配置日志级别
## 🔧 故障排除
### 1. **日志文件未创建**
- 检查日志目录权限
- 确认日志配置已启用
- 验证文件路径配置
### 2. **日志记录不完整**
- 检查日志器是否正确初始化
- 确认请求ID生成逻辑
- 验证错误处理流程
### 3. **性能问题**
- 检查日志文件大小和数量
- 确认日志轮转配置
- 监控磁盘I/O性能
## 🎉 总结
西部数据日志系统提供了完整的API调用追踪能力
1. **请求追踪**: 通过唯一请求ID关联请求和响应
2. **错误监控**: 记录所有错误和异常情况
3. **性能分析**: 提供详细的性能指标
4. **文件管理**: 支持按日期分包和级别分离
5. **配置灵活**: 支持自定义日志配置
通过合理使用这个日志系统,您可以:
- 快速定位API调用问题
- 监控系统性能和稳定性
- 分析API使用模式和趋势
- 提高问题排查效率
现在您的西部数据服务已经具备了完整的日志记录能力!🚀

28
go.mod
View File

@@ -3,7 +3,12 @@ module tyapi-server
go 1.23.4 go 1.23.4
require ( require (
github.com/alibabacloud-go/captcha-20230305 v1.1.3
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
github.com/alibabacloud-go/tea v1.3.13
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8
github.com/chromedp/chromedp v0.13.2
github.com/gin-contrib/cors v1.7.6 github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1 github.com/gin-gonic/gin v1.10.1
github.com/go-playground/locales v0.14.1 github.com/go-playground/locales v0.14.1
@@ -11,10 +16,15 @@ require (
github.com/go-playground/validator/v10 v10.26.0 github.com/go-playground/validator/v10 v10.26.0
github.com/golang-jwt/jwt/v5 v5.2.2 github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hibiken/asynq v0.25.1
github.com/jung-kurt/gofpdf/v2 v2.17.3
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220
github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_golang v1.22.0
github.com/qiniu/go-sdk/v7 v7.25.4 github.com/qiniu/go-sdk/v7 v7.25.4
github.com/redis/go-redis/v9 v9.11.0 github.com/redis/go-redis/v9 v9.11.0
github.com/robfig/cron/v3 v3.0.1
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/smartwalle/alipay/v3 v3.2.25 github.com/smartwalle/alipay/v3 v3.2.25
github.com/spf13/viper v1.20.1 github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
@@ -22,6 +32,8 @@ require (
github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4 github.com/swaggo/swag v1.16.4
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
github.com/xuri/excelize/v2 v2.9.1
go.opentelemetry.io/otel v1.37.0 go.opentelemetry.io/otel v1.37.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
go.opentelemetry.io/otel/sdk v1.37.0 go.opentelemetry.io/otel/sdk v1.37.0
@@ -40,11 +52,17 @@ require (
github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/aliyun/credentials-go v1.4.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@@ -52,6 +70,7 @@ require (
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gammazero/toposort v0.1.1 // indirect github.com/gammazero/toposort v0.1.1 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect
@@ -59,6 +78,9 @@ require (
github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/gofrs/flock v0.8.1 // indirect github.com/gofrs/flock v0.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
@@ -84,6 +106,8 @@ require (
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/smartwalle/ncrypto v1.0.4 // indirect github.com/smartwalle/ncrypto v1.0.4 // indirect
github.com/smartwalle/ngx v1.0.9 // indirect github.com/smartwalle/ngx v1.0.9 // indirect
@@ -95,8 +119,12 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/tiendc/go-deepcopy v1.6.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.0 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect

232
go.sum
View File

@@ -1,4 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -9,11 +11,59 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4=
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
github.com/alibabacloud-go/captcha-20230305 v1.1.3 h1:0Aobw12m3x28aeDMPjwjXsfF8MuLvRjlQ4Hhoy5hFOY=
github.com/alibabacloud-go/captcha-20230305 v1.1.3/go.mod h1:ydzBIN2OiM7eeQPpAFyBrv1H5TY1MtUP2rQig44C4UQ=
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU= github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
@@ -27,11 +77,22 @@ github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCN
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs=
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.13.2 h1:f6sZFFzCzPLvWSzeuXQBgONKG7zPq54YfEyEj0EplOY=
github.com/chromedp/chromedp v0.13.2/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -39,6 +100,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
@@ -57,6 +121,8 @@ github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fq
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -88,6 +154,12 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
@@ -96,18 +168,38 @@ github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptG
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
@@ -128,9 +220,13 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf/v2 v2.17.3 h1:otZXZby2gXJ7uU6pzprXHq/R57lsHLi0WtH79VabWxY=
github.com/jung-kurt/gofpdf/v2 v2.17.3/go.mod h1:Qx8ZNg4cNsO5i6uLDiBngnm+ii/FjtAqjRNO6drsoYU=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -148,10 +244,13 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220 h1:FLQyP/6tTsTEtAhcIq/kS/zkDEMdOMon0I70pXVehOU=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220/go.mod h1:+mNMTBuDMdEGhWzoQgc6kBdqeaQpWh5ba8zqmp2MxCU=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
@@ -161,6 +260,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -168,6 +269,7 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -177,6 +279,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
@@ -189,6 +292,13 @@ github.com/qiniu/go-sdk/v7 v7.25.4/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peq
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@@ -197,6 +307,8 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q= github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q=
github.com/smartwalle/alipay/v3 v3.2.25/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE= github.com/smartwalle/alipay/v3 v3.2.25/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE=
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8= github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
@@ -205,6 +317,9 @@ github.com/smartwalle/ngx v1.0.9 h1:pUXDvWRZJIHVrCKA1uZ15YwNti+5P4GuJGbpJ4WvpMw=
github.com/smartwalle/ngx v1.0.9/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= github.com/smartwalle/ngx v1.0.9/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E= github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E=
github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc= github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
@@ -216,10 +331,12 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -241,6 +358,11 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
@@ -249,6 +371,16 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
@@ -284,38 +416,89 @@ golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -323,42 +506,88 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -367,6 +596,7 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
@@ -385,6 +615,8 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w= modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w=
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -22,8 +22,17 @@ import (
// 产品域实体 // 产品域实体
productEntities "tyapi-server/internal/domains/product/entities" productEntities "tyapi-server/internal/domains/product/entities"
// 文章域实体
articleEntities "tyapi-server/internal/domains/article/entities"
// 统计域实体
securityEntities "tyapi-server/internal/domains/security/entities"
statisticsEntities "tyapi-server/internal/domains/statistics/entities"
apiEntities "tyapi-server/internal/domains/api/entities" apiEntities "tyapi-server/internal/domains/api/entities"
subordinateEntities "tyapi-server/internal/domains/subordinate/entities"
"tyapi-server/internal/infrastructure/database" "tyapi-server/internal/infrastructure/database"
taskEntities "tyapi-server/internal/infrastructure/task/entities"
) )
// Application 应用程序结构 // Application 应用程序结构
@@ -224,6 +233,8 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
&financeEntities.AlipayOrder{}, &financeEntities.AlipayOrder{},
&financeEntities.InvoiceApplication{}, &financeEntities.InvoiceApplication{},
&financeEntities.UserInvoiceInfo{}, &financeEntities.UserInvoiceInfo{},
&financeEntities.PurchaseOrder{}, //购买组件订单表
// 产品域 // 产品域
&productEntities.Product{}, &productEntities.Product{},
&productEntities.ProductPackageItem{}, &productEntities.ProductPackageItem{},
@@ -231,9 +242,39 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
&productEntities.Subscription{}, &productEntities.Subscription{},
&productEntities.ProductDocumentation{}, &productEntities.ProductDocumentation{},
&productEntities.ProductApiConfig{}, &productEntities.ProductApiConfig{},
&productEntities.ComponentReportDownload{},
&productEntities.UIComponent{},
&productEntities.ProductUIComponent{},
// 文章域
&articleEntities.Article{},
&articleEntities.Category{},
&articleEntities.Tag{},
&articleEntities.ScheduledTask{},
// 公告
&articleEntities.Announcement{},
// 统计域
&statisticsEntities.StatisticsMetric{},
&statisticsEntities.StatisticsDashboard{},
&statisticsEntities.StatisticsReport{},
&securityEntities.SuspiciousIPRecord{},
// api // api
&apiEntities.ApiUser{}, &apiEntities.ApiUser{},
&apiEntities.ApiCall{}, &apiEntities.ApiCall{},
&apiEntities.Report{},
// 下属账号域
&subordinateEntities.SubordinateInvitation{},
&subordinateEntities.UserSubordinateLink{},
&subordinateEntities.SubordinateWalletAllocation{},
&subordinateEntities.SubordinateQuotaPurchase{},
&subordinateEntities.UserProductQuotaAccount{},
&subordinateEntities.UserProductQuotaLedger{},
// 任务域
&taskEntities.AsyncTask{},
) )
} }
@@ -256,6 +297,7 @@ func createLogger(cfg *config.Config) (*zap.Logger, error) {
if cfg.Logger.Format == "" { if cfg.Logger.Format == "" {
config.Encoding = "json" config.Encoding = "json"
} }
if cfg.Logger.Output == "" { if cfg.Logger.Output == "" {
config.OutputPaths = []string{"stdout"} config.OutputPaths = []string{"stdout"}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -24,3 +24,48 @@ type DecryptCommand struct {
EncryptedData string `json:"encrypted_data" binding:"required"` EncryptedData string `json:"encrypted_data" binding:"required"`
SecretKey string `json:"secret_key" binding:"required"` SecretKey string `json:"secret_key" binding:"required"`
} }
// SaveApiCallCommand 保存API调用命令
type SaveApiCallCommand struct {
ApiCallID string `json:"api_call_id"`
UserID string `json:"user_id"`
ProductID string `json:"product_id"`
TransactionID string `json:"transaction_id"`
Status string `json:"status"`
Cost float64 `json:"cost"`
ErrorType string `json:"error_type"`
ErrorMsg string `json:"error_msg"`
ClientIP string `json:"client_ip"`
}
// ProcessDeductionCommand 处理扣款命令
type ProcessDeductionCommand struct {
UserID string `json:"user_id"`
Amount string `json:"amount"`
ApiCallID string `json:"api_call_id"`
TransactionID string `json:"transaction_id"`
ProductID string `json:"product_id"`
}
// UpdateUsageStatsCommand 更新使用统计命令
type UpdateUsageStatsCommand struct {
SubscriptionID string `json:"subscription_id"`
UserID string `json:"user_id"`
ProductID string `json:"product_id"`
Increment int `json:"increment"`
}
// RecordApiLogCommand 记录API日志命令
type RecordApiLogCommand struct {
TransactionID string `json:"transaction_id"`
UserID string `json:"user_id"`
ApiName string `json:"api_name"`
ClientIP string `json:"client_ip"`
ResponseSize int64 `json:"response_size"`
}
// ProcessCompensationCommand 处理补偿命令
type ProcessCompensationCommand struct {
TransactionID string `json:"transaction_id"`
Type string `json:"type"`
}

View File

@@ -0,0 +1,104 @@
package dto
import (
api_entities "tyapi-server/internal/domains/api/entities"
product_entities "tyapi-server/internal/domains/product/entities"
"github.com/shopspring/decimal"
)
// ApiCallValidationResult API调用验证结果
type ApiCallValidationResult struct {
UserID string `json:"user_id"`
ProductID string `json:"product_id"`
SubscriptionID string `json:"subscription_id"`
Amount decimal.Decimal `json:"amount"`
SecretKey string `json:"secret_key"`
IsValid bool `json:"is_valid"`
ErrorMessage string `json:"error_message"`
// 新增字段
ContractCode string `json:"contract_code"`
ApiCall *api_entities.ApiCall `json:"api_call"`
RequestParams map[string]interface{} `json:"request_params"`
Product *product_entities.Product `json:"product"`
Subscription *product_entities.Subscription `json:"subscription"`
}
// GetUserID 获取用户ID
func (r *ApiCallValidationResult) GetUserID() string {
return r.UserID
}
// GetProductID 获取产品ID
func (r *ApiCallValidationResult) GetProductID() string {
return r.ProductID
}
// GetSubscriptionID 获取订阅ID
func (r *ApiCallValidationResult) GetSubscriptionID() string {
return r.SubscriptionID
}
// GetAmount 获取金额
func (r *ApiCallValidationResult) GetAmount() decimal.Decimal {
return r.Amount
}
// GetSecretKey 获取密钥
func (r *ApiCallValidationResult) GetSecretKey() string {
return r.SecretKey
}
// IsValidResult 检查是否有效
func (r *ApiCallValidationResult) IsValidResult() bool {
return r.IsValid
}
// GetErrorMessage 获取错误消息
func (r *ApiCallValidationResult) GetErrorMessage() string {
return r.ErrorMessage
}
// NewApiCallValidationResult 创建新的API调用验证结果
func NewApiCallValidationResult() *ApiCallValidationResult {
return &ApiCallValidationResult{
IsValid: true,
RequestParams: make(map[string]interface{}),
}
}
// SetApiUser 设置API用户
func (r *ApiCallValidationResult) SetApiUser(apiUser *api_entities.ApiUser) {
r.UserID = apiUser.UserId
r.SecretKey = apiUser.SecretKey
}
// SetProduct 设置产品
func (r *ApiCallValidationResult) SetProduct(product *product_entities.Product) {
r.ProductID = product.ID
r.Product = product
// 注意这里不设置Amount应该通过SetSubscription来设置实际的扣费金额
}
// SetApiCall 设置API调用
func (r *ApiCallValidationResult) SetApiCall(apiCall *api_entities.ApiCall) {
r.ApiCall = apiCall
}
// SetRequestParams 设置请求参数
func (r *ApiCallValidationResult) SetRequestParams(params map[string]interface{}) {
r.RequestParams = params
}
// SetContractCode 设置合同代码
func (r *ApiCallValidationResult) SetContractCode(code string) {
r.ContractCode = code
}
// SetSubscription 设置订阅信息(包含实际扣费金额)
func (r *ApiCallValidationResult) SetSubscription(subscription *product_entities.Subscription) {
r.SubscriptionID = subscription.ID
r.Amount = subscription.Price // 使用订阅价格作为扣费金额
r.Subscription = subscription
}

View File

@@ -26,11 +26,13 @@ type WhiteListResponse struct {
ID string `json:"id"` ID string `json:"id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
IPAddress string `json:"ip_address"` IPAddress string `json:"ip_address"`
Remark string `json:"remark"` // 备注
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }
type WhiteListRequest struct { type WhiteListRequest struct {
IPAddress string `json:"ip_address" binding:"required,ip"` IPAddress string `json:"ip_address" binding:"required,ip"`
Remark string `json:"remark"` // 备注(可选)
} }
type WhiteListListResponse struct { type WhiteListListResponse struct {
@@ -47,6 +49,7 @@ type ApiCallRecordResponse struct {
ProductName *string `json:"product_name,omitempty"` ProductName *string `json:"product_name,omitempty"`
TransactionId string `json:"transaction_id"` TransactionId string `json:"transaction_id"`
ClientIp string `json:"client_ip"` ClientIp string `json:"client_ip"`
RequestParams string `json:"request_params"`
Status string `json:"status"` Status string `json:"status"`
StartAt string `json:"start_at"` StartAt string `json:"start_at"`
EndAt *string `json:"end_at,omitempty"` EndAt *string `json:"end_at,omitempty"`

View File

@@ -0,0 +1,19 @@
package dto
// FormField 表单字段配置
type FormField struct {
Name string `json:"name"`
Label string `json:"label"`
Type string `json:"type"`
Required bool `json:"required"`
Validation string `json:"validation"`
Description string `json:"description"`
Example string `json:"example"`
Placeholder string `json:"placeholder"`
}
// FormConfigResponse 表单配置响应
type FormConfigResponse struct {
ApiCode string `json:"api_code"`
Fields []FormField `json:"fields"`
}

View File

@@ -5,6 +5,7 @@ import "errors"
// API调用相关错误类型 // API调用相关错误类型
var ( var (
ErrQueryEmpty = errors.New("查询为空") ErrQueryEmpty = errors.New("查询为空")
ErrQueryFailed = errors.New("查询失败")
ErrSystem = errors.New("接口异常") ErrSystem = errors.New("接口异常")
ErrDecryptFail = errors.New("解密失败") ErrDecryptFail = errors.New("解密失败")
ErrRequestParam = errors.New("请求参数结构不正确") ErrRequestParam = errors.New("请求参数结构不正确")
@@ -14,15 +15,22 @@ var (
ErrInvalidAccessId = errors.New("未经授权的AccessId") ErrInvalidAccessId = errors.New("未经授权的AccessId")
ErrFrozenAccount = errors.New("账户已冻结") ErrFrozenAccount = errors.New("账户已冻结")
ErrArrears = errors.New("账户余额不足,无法请求") ErrArrears = errors.New("账户余额不足,无法请求")
ErrInsufficientBalance = errors.New("钱包余额不足")
ErrProductNotFound = errors.New("产品不存在") ErrProductNotFound = errors.New("产品不存在")
ErrProductDisabled = errors.New("产品已停用") ErrProductDisabled = errors.New("产品已停用")
ErrNotSubscribed = errors.New("未订阅此产品") ErrNotSubscribed = errors.New("未订阅此产品")
ErrProductNotSubscribed = errors.New("未订阅此产品")
ErrSubscriptionExpired = errors.New("订阅已过期")
ErrSubscriptionSuspended = errors.New("订阅已暂停")
ErrBusiness = errors.New("业务失败") ErrBusiness = errors.New("业务失败")
ErrSubordinateLinkNotFound = errors.New("非子账号无法使用master_accessid")
ErrSubordinateParentMismatch = errors.New("master_accessid与主账号不匹配")
) )
// 错误码映射 - 严格按照用户要求 // 错误码映射 - 严格按照用户要求
var ErrorCodeMap = map[error]int{ var ErrorCodeMap = map[error]int{
ErrQueryEmpty: 1000, ErrQueryEmpty: 1000,
ErrQueryFailed: 1000,
ErrSystem: 1001, ErrSystem: 1001,
ErrDecryptFail: 1002, ErrDecryptFail: 1002,
ErrRequestParam: 1003, ErrRequestParam: 1003,
@@ -32,10 +40,16 @@ var ErrorCodeMap = map[error]int{
ErrInvalidAccessId: 1006, ErrInvalidAccessId: 1006,
ErrFrozenAccount: 1007, ErrFrozenAccount: 1007,
ErrArrears: 1007, ErrArrears: 1007,
ErrInsufficientBalance: 1007,
ErrProductNotFound: 1008, ErrProductNotFound: 1008,
ErrProductDisabled: 1008, ErrProductDisabled: 1008,
ErrNotSubscribed: 1008, ErrNotSubscribed: 1008,
ErrProductNotSubscribed: 1008,
ErrSubscriptionExpired: 1008,
ErrSubscriptionSuspended: 1008,
ErrBusiness: 2001, ErrBusiness: 2001,
ErrSubordinateLinkNotFound: 1301,
ErrSubordinateParentMismatch: 1302,
} }
// GetErrorCode 获取错误对应的错误码 // GetErrorCode 获取错误对应的错误码

View File

@@ -0,0 +1,30 @@
package article
import (
"context"
"tyapi-server/internal/application/article/dto/commands"
appQueries "tyapi-server/internal/application/article/dto/queries"
"tyapi-server/internal/application/article/dto/responses"
)
// AnnouncementApplicationService 公告应用服务接口
type AnnouncementApplicationService interface {
// 公告管理
CreateAnnouncement(ctx context.Context, cmd *commands.CreateAnnouncementCommand) error
UpdateAnnouncement(ctx context.Context, cmd *commands.UpdateAnnouncementCommand) error
DeleteAnnouncement(ctx context.Context, cmd *commands.DeleteAnnouncementCommand) error
GetAnnouncementByID(ctx context.Context, query *appQueries.GetAnnouncementQuery) (*responses.AnnouncementInfoResponse, error)
ListAnnouncements(ctx context.Context, query *appQueries.ListAnnouncementQuery) (*responses.AnnouncementListResponse, error)
// 公告状态管理
PublishAnnouncement(ctx context.Context, cmd *commands.PublishAnnouncementCommand) error
PublishAnnouncementByID(ctx context.Context, announcementID string) error // 通过ID发布公告 (用于定时任务)
WithdrawAnnouncement(ctx context.Context, cmd *commands.WithdrawAnnouncementCommand) error
ArchiveAnnouncement(ctx context.Context, cmd *commands.ArchiveAnnouncementCommand) error
SchedulePublishAnnouncement(ctx context.Context, cmd *commands.SchedulePublishAnnouncementCommand) error
UpdateSchedulePublishAnnouncement(ctx context.Context, cmd *commands.UpdateSchedulePublishAnnouncementCommand) error
CancelSchedulePublishAnnouncement(ctx context.Context, cmd *commands.CancelSchedulePublishAnnouncementCommand) error
// 统计信息
GetAnnouncementStats(ctx context.Context) (*responses.AnnouncementStatsResponse, error)
}

View File

@@ -0,0 +1,484 @@
package article
import (
"context"
"fmt"
"tyapi-server/internal/application/article/dto/commands"
appQueries "tyapi-server/internal/application/article/dto/queries"
"tyapi-server/internal/application/article/dto/responses"
"tyapi-server/internal/domains/article/entities"
"tyapi-server/internal/domains/article/repositories"
repoQueries "tyapi-server/internal/domains/article/repositories/queries"
"tyapi-server/internal/domains/article/services"
task_entities "tyapi-server/internal/infrastructure/task/entities"
task_interfaces "tyapi-server/internal/infrastructure/task/interfaces"
"go.uber.org/zap"
)
// AnnouncementApplicationServiceImpl 公告应用服务实现
type AnnouncementApplicationServiceImpl struct {
announcementRepo repositories.AnnouncementRepository
announcementService *services.AnnouncementService
taskManager task_interfaces.TaskManager
logger *zap.Logger
}
// NewAnnouncementApplicationService 创建公告应用服务
func NewAnnouncementApplicationService(
announcementRepo repositories.AnnouncementRepository,
announcementService *services.AnnouncementService,
taskManager task_interfaces.TaskManager,
logger *zap.Logger,
) AnnouncementApplicationService {
return &AnnouncementApplicationServiceImpl{
announcementRepo: announcementRepo,
announcementService: announcementService,
taskManager: taskManager,
logger: logger,
}
}
// CreateAnnouncement 创建公告
func (s *AnnouncementApplicationServiceImpl) CreateAnnouncement(ctx context.Context, cmd *commands.CreateAnnouncementCommand) error {
// 1. 创建公告实体
announcement := &entities.Announcement{
Title: cmd.Title,
Content: cmd.Content,
Status: entities.AnnouncementStatusDraft,
}
// 2. 调用领域服务验证
if err := s.announcementService.ValidateAnnouncement(announcement); err != nil {
return fmt.Errorf("业务验证失败: %w", err)
}
// 3. 保存公告
_, err := s.announcementRepo.Create(ctx, *announcement)
if err != nil {
s.logger.Error("创建公告失败", zap.Error(err))
return fmt.Errorf("创建公告失败: %w", err)
}
s.logger.Info("创建公告成功", zap.String("id", announcement.ID), zap.String("title", announcement.Title))
return nil
}
// UpdateAnnouncement 更新公告
func (s *AnnouncementApplicationServiceImpl) UpdateAnnouncement(ctx context.Context, cmd *commands.UpdateAnnouncementCommand) error {
// 1. 获取原公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否可以编辑
if err := s.announcementService.CanEdit(&announcement); err != nil {
return fmt.Errorf("公告状态不允许编辑: %w", err)
}
// 3. 更新字段
if cmd.Title != "" {
announcement.Title = cmd.Title
}
if cmd.Content != "" {
announcement.Content = cmd.Content
}
// 4. 验证更新后的公告
if err := s.announcementService.ValidateAnnouncement(&announcement); err != nil {
return fmt.Errorf("业务验证失败: %w", err)
}
// 5. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("更新公告失败: %w", err)
}
s.logger.Info("更新公告成功", zap.String("id", announcement.ID))
return nil
}
// DeleteAnnouncement 删除公告
func (s *AnnouncementApplicationServiceImpl) DeleteAnnouncement(ctx context.Context, cmd *commands.DeleteAnnouncementCommand) error {
// 1. 检查公告是否存在
_, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 删除公告
if err := s.announcementRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("删除公告失败: %w", err)
}
s.logger.Info("删除公告成功", zap.String("id", cmd.ID))
return nil
}
// GetAnnouncementByID 获取公告详情
func (s *AnnouncementApplicationServiceImpl) GetAnnouncementByID(ctx context.Context, query *appQueries.GetAnnouncementQuery) (*responses.AnnouncementInfoResponse, error) {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, query.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", query.ID), zap.Error(err))
return nil, fmt.Errorf("公告不存在: %w", err)
}
// 2. 转换为响应对象
response := responses.FromAnnouncementEntity(&announcement)
return response, nil
}
// ListAnnouncements 获取公告列表
func (s *AnnouncementApplicationServiceImpl) ListAnnouncements(ctx context.Context, query *appQueries.ListAnnouncementQuery) (*responses.AnnouncementListResponse, error) {
// 1. 构建仓储查询
repoQuery := &repoQueries.ListAnnouncementQuery{
Page: query.Page,
PageSize: query.PageSize,
Status: query.Status,
Title: query.Title,
OrderBy: query.OrderBy,
OrderDir: query.OrderDir,
}
// 2. 调用仓储
announcements, total, err := s.announcementRepo.ListAnnouncements(ctx, repoQuery)
if err != nil {
s.logger.Error("获取公告列表失败", zap.Error(err))
return nil, fmt.Errorf("获取公告列表失败: %w", err)
}
// 3. 转换为响应对象
items := responses.FromAnnouncementEntityList(announcements)
response := &responses.AnnouncementListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}
s.logger.Info("获取公告列表成功", zap.Int64("total", total))
return response, nil
}
// PublishAnnouncement 发布公告
func (s *AnnouncementApplicationServiceImpl) PublishAnnouncement(ctx context.Context, cmd *commands.PublishAnnouncementCommand) error {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否可以发布
if err := s.announcementService.CanPublish(&announcement); err != nil {
return fmt.Errorf("无法发布公告: %w", err)
}
// 3. 发布公告
if err := announcement.Publish(); err != nil {
return fmt.Errorf("发布公告失败: %w", err)
}
// 4. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("发布公告失败: %w", err)
}
s.logger.Info("发布公告成功", zap.String("id", announcement.ID))
return nil
}
// PublishAnnouncementByID 通过ID发布公告 (用于定时任务)
func (s *AnnouncementApplicationServiceImpl) PublishAnnouncementByID(ctx context.Context, announcementID string) error {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, announcementID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", announcementID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否已取消定时发布
if !announcement.IsScheduled() {
s.logger.Info("公告定时发布已取消,跳过执行",
zap.String("id", announcementID),
zap.String("status", string(announcement.Status)))
return nil // 静默返回,不报错
}
// 3. 检查定时发布时间是否匹配
if announcement.ScheduledAt == nil {
s.logger.Info("公告没有定时发布时间,跳过执行",
zap.String("id", announcementID))
return nil
}
// 4. 发布公告
if err := announcement.Publish(); err != nil {
return fmt.Errorf("发布公告失败: %w", err)
}
// 5. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("发布公告失败: %w", err)
}
s.logger.Info("定时发布公告成功", zap.String("id", announcement.ID))
return nil
}
// WithdrawAnnouncement 撤回公告
func (s *AnnouncementApplicationServiceImpl) WithdrawAnnouncement(ctx context.Context, cmd *commands.WithdrawAnnouncementCommand) error {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否可以撤回
if err := s.announcementService.CanWithdraw(&announcement); err != nil {
return fmt.Errorf("无法撤回公告: %w", err)
}
// 3. 撤回公告
if err := announcement.Withdraw(); err != nil {
return fmt.Errorf("撤回公告失败: %w", err)
}
// 4. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("撤回公告失败: %w", err)
}
s.logger.Info("撤回公告成功", zap.String("id", announcement.ID))
return nil
}
// ArchiveAnnouncement 归档公告
func (s *AnnouncementApplicationServiceImpl) ArchiveAnnouncement(ctx context.Context, cmd *commands.ArchiveAnnouncementCommand) error {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否可以归档
if err := s.announcementService.CanArchive(&announcement); err != nil {
return fmt.Errorf("无法归档公告: %w", err)
}
// 3. 归档公告
announcement.Status = entities.AnnouncementStatusArchived
// 4. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("归档公告失败: %w", err)
}
s.logger.Info("归档公告成功", zap.String("id", announcement.ID))
return nil
}
// SchedulePublishAnnouncement 定时发布公告
func (s *AnnouncementApplicationServiceImpl) SchedulePublishAnnouncement(ctx context.Context, cmd *commands.SchedulePublishAnnouncementCommand) error {
// 1. 解析定时发布时间
scheduledTime, err := cmd.GetScheduledTime()
if err != nil {
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
return fmt.Errorf("定时发布时间格式错误: %w", err)
}
// 2. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 3. 检查是否可以定时发布
if err := s.announcementService.CanSchedulePublish(&announcement, scheduledTime); err != nil {
return fmt.Errorf("无法设置定时发布: %w", err)
}
// 4. 取消旧任务(如果存在)
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
}
// 5. 创建任务工厂
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
// 6. 创建并异步入队公告发布任务
if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask(
ctx,
cmd.ID,
scheduledTime,
"system", // 暂时使用系统用户ID
); err != nil {
s.logger.Error("创建并入队公告发布任务失败", zap.Error(err))
return fmt.Errorf("创建定时发布任务失败: %w", err)
}
// 7. 设置定时发布
if err := announcement.SchedulePublish(scheduledTime); err != nil {
return fmt.Errorf("设置定时发布失败: %w", err)
}
// 8. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("设置定时发布失败: %w", err)
}
s.logger.Info("设置定时发布成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime))
return nil
}
// UpdateSchedulePublishAnnouncement 更新定时发布公告
func (s *AnnouncementApplicationServiceImpl) UpdateSchedulePublishAnnouncement(ctx context.Context, cmd *commands.UpdateSchedulePublishAnnouncementCommand) error {
// 1. 解析定时发布时间
scheduledTime, err := cmd.GetScheduledTime()
if err != nil {
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
return fmt.Errorf("定时发布时间格式错误: %w", err)
}
// 2. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 3. 检查是否已设置定时发布
if !announcement.IsScheduled() {
return fmt.Errorf("公告未设置定时发布,无法修改时间")
}
// 4. 取消旧任务
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
}
// 5. 创建任务工厂
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
// 6. 创建并异步入队新的公告发布任务
if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask(
ctx,
cmd.ID,
scheduledTime,
"system", // 暂时使用系统用户ID
); err != nil {
s.logger.Error("创建并入队公告发布任务失败", zap.Error(err))
return fmt.Errorf("创建定时发布任务失败: %w", err)
}
// 7. 更新定时发布时间
if err := announcement.UpdateSchedulePublish(scheduledTime); err != nil {
return fmt.Errorf("更新定时发布时间失败: %w", err)
}
// 8. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("修改定时发布时间失败: %w", err)
}
s.logger.Info("修改定时发布时间成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime))
return nil
}
// CancelSchedulePublishAnnouncement 取消定时发布公告
func (s *AnnouncementApplicationServiceImpl) CancelSchedulePublishAnnouncement(ctx context.Context, cmd *commands.CancelSchedulePublishAnnouncementCommand) error {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否已设置定时发布
if !announcement.IsScheduled() {
return fmt.Errorf("公告未设置定时发布,无需取消")
}
// 3. 取消任务
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
s.logger.Warn("取消任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
// 继续执行,即使取消任务失败也尝试取消定时发布状态
}
// 4. 取消定时发布
if err := announcement.CancelSchedulePublish(); err != nil {
return fmt.Errorf("取消定时发布失败: %w", err)
}
// 5. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("取消定时发布失败: %w", err)
}
s.logger.Info("取消定时发布成功", zap.String("id", announcement.ID))
return nil
}
// GetAnnouncementStats 获取公告统计信息
func (s *AnnouncementApplicationServiceImpl) GetAnnouncementStats(ctx context.Context) (*responses.AnnouncementStatsResponse, error) {
// 1. 统计总数
total, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft)
if err != nil {
s.logger.Error("统计公告总数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
// 2. 统计各状态数量
published, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusPublished)
if err != nil {
s.logger.Error("统计已发布公告数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
draft, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft)
if err != nil {
s.logger.Error("统计草稿公告数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
archived, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusArchived)
if err != nil {
s.logger.Error("统计归档公告数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
// 3. 统计定时发布数量需要查询有scheduled_at的草稿
scheduled, err := s.announcementRepo.FindScheduled(ctx)
if err != nil {
s.logger.Error("统计定时发布公告数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
response := &responses.AnnouncementStatsResponse{
TotalAnnouncements: total + published + archived,
PublishedAnnouncements: published,
DraftAnnouncements: draft,
ArchivedAnnouncements: archived,
ScheduledAnnouncements: int64(len(scheduled)),
}
return response, nil
}

View File

@@ -0,0 +1,48 @@
package article
import (
"context"
"tyapi-server/internal/application/article/dto/commands"
appQueries "tyapi-server/internal/application/article/dto/queries"
"tyapi-server/internal/application/article/dto/responses"
)
// ArticleApplicationService 文章应用服务接口
type ArticleApplicationService interface {
// 文章管理
CreateArticle(ctx context.Context, cmd *commands.CreateArticleCommand) error
UpdateArticle(ctx context.Context, cmd *commands.UpdateArticleCommand) error
DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error
GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error)
ListArticles(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error)
ListArticlesForAdmin(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error)
// 文章状态管理
PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error
PublishArticleByID(ctx context.Context, articleID string) error
SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error
UpdateSchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error
CancelSchedulePublishArticle(ctx context.Context, cmd *commands.CancelScheduleCommand) error
ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error
SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error
// 文章交互
RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error
// 统计信息
GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error)
// 分类管理
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
GetCategoryByID(ctx context.Context, query *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
ListCategories(ctx context.Context) (*responses.CategoryListResponse, error)
// 标签管理
CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error
UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error
DeleteTag(ctx context.Context, cmd *commands.DeleteTagCommand) error
GetTagByID(ctx context.Context, query *appQueries.GetTagQuery) (*responses.TagInfoResponse, error)
ListTags(ctx context.Context) (*responses.TagListResponse, error)
}

View File

@@ -0,0 +1,836 @@
package article
import (
"context"
"fmt"
"tyapi-server/internal/application/article/dto/commands"
appQueries "tyapi-server/internal/application/article/dto/queries"
"tyapi-server/internal/application/article/dto/responses"
"tyapi-server/internal/domains/article/entities"
"tyapi-server/internal/domains/article/repositories"
repoQueries "tyapi-server/internal/domains/article/repositories/queries"
"tyapi-server/internal/domains/article/services"
task_entities "tyapi-server/internal/infrastructure/task/entities"
task_interfaces "tyapi-server/internal/infrastructure/task/interfaces"
shared_interfaces "tyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
)
// ArticleApplicationServiceImpl 文章应用服务实现
type ArticleApplicationServiceImpl struct {
articleRepo repositories.ArticleRepository
categoryRepo repositories.CategoryRepository
tagRepo repositories.TagRepository
articleService *services.ArticleService
taskManager task_interfaces.TaskManager
logger *zap.Logger
}
// NewArticleApplicationService 创建文章应用服务
func NewArticleApplicationService(
articleRepo repositories.ArticleRepository,
categoryRepo repositories.CategoryRepository,
tagRepo repositories.TagRepository,
articleService *services.ArticleService,
taskManager task_interfaces.TaskManager,
logger *zap.Logger,
) ArticleApplicationService {
return &ArticleApplicationServiceImpl{
articleRepo: articleRepo,
categoryRepo: categoryRepo,
tagRepo: tagRepo,
articleService: articleService,
taskManager: taskManager,
logger: logger,
}
}
// CreateArticle 创建文章
func (s *ArticleApplicationServiceImpl) CreateArticle(ctx context.Context, cmd *commands.CreateArticleCommand) error {
// 1. 参数验证
if err := s.validateCreateArticle(cmd); err != nil {
return fmt.Errorf("参数验证失败: %w", err)
}
// 2. 创建文章实体
article := &entities.Article{
Title: cmd.Title,
Content: cmd.Content,
Summary: cmd.Summary,
CoverImage: cmd.CoverImage,
CategoryID: cmd.CategoryID,
IsFeatured: cmd.IsFeatured,
Status: entities.ArticleStatusDraft,
}
// 3. 调用领域服务验证
if err := s.articleService.ValidateArticle(article); err != nil {
return fmt.Errorf("业务验证失败: %w", err)
}
// 4. 保存文章
_, err := s.articleRepo.Create(ctx, *article)
if err != nil {
s.logger.Error("创建文章失败", zap.Error(err))
return fmt.Errorf("创建文章失败: %w", err)
}
// 5. 处理标签关联
if len(cmd.TagIDs) > 0 {
for _, tagID := range cmd.TagIDs {
if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil {
s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err))
}
}
}
s.logger.Info("创建文章成功", zap.String("id", article.ID), zap.String("title", article.Title))
return nil
}
// UpdateArticle 更新文章
func (s *ArticleApplicationServiceImpl) UpdateArticle(ctx context.Context, cmd *commands.UpdateArticleCommand) error {
// 1. 获取原文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 检查是否可以编辑
if !article.CanEdit() {
return fmt.Errorf("文章状态不允许编辑")
}
// 3. 更新字段
if cmd.Title != "" {
article.Title = cmd.Title
}
if cmd.Content != "" {
article.Content = cmd.Content
}
if cmd.Summary != "" {
article.Summary = cmd.Summary
}
if cmd.CoverImage != "" {
article.CoverImage = cmd.CoverImage
}
if cmd.CategoryID != "" {
article.CategoryID = cmd.CategoryID
}
article.IsFeatured = cmd.IsFeatured
// 4. 验证更新后的文章
if err := s.articleService.ValidateArticle(&article); err != nil {
return fmt.Errorf("业务验证失败: %w", err)
}
// 5. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("更新文章失败: %w", err)
}
// 6. 处理标签关联
// 先清除现有标签
existingTags, _ := s.tagRepo.GetArticleTags(ctx, article.ID)
for _, tag := range existingTags {
s.tagRepo.RemoveTagFromArticle(ctx, article.ID, tag.ID)
}
// 添加新标签
for _, tagID := range cmd.TagIDs {
if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil {
s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err))
}
}
s.logger.Info("更新文章成功", zap.String("id", article.ID))
return nil
}
// DeleteArticle 删除文章
func (s *ArticleApplicationServiceImpl) DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error {
// 1. 检查文章是否存在
_, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 删除文章
if err := s.articleRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("删除文章失败: %w", err)
}
s.logger.Info("删除文章成功", zap.String("id", cmd.ID))
return nil
}
// GetArticleByID 根据ID获取文章
func (s *ArticleApplicationServiceImpl) GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error) {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, query.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", query.ID), zap.Error(err))
return nil, fmt.Errorf("文章不存在: %w", err)
}
// 2. 转换为响应对象
response := responses.FromArticleEntity(&article)
s.logger.Info("获取文章成功", zap.String("id", article.ID))
return response, nil
}
// ListArticles 获取文章列表
func (s *ArticleApplicationServiceImpl) ListArticles(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) {
// 1. 构建仓储查询
repoQuery := &repoQueries.ListArticleQuery{
Page: query.Page,
PageSize: query.PageSize,
Status: query.Status,
CategoryID: query.CategoryID,
TagID: query.TagID,
Title: query.Title,
Summary: query.Summary,
IsFeatured: query.IsFeatured,
OrderBy: query.OrderBy,
OrderDir: query.OrderDir,
}
// 2. 调用仓储
articles, total, err := s.articleRepo.ListArticles(ctx, repoQuery)
if err != nil {
s.logger.Error("获取文章列表失败", zap.Error(err))
return nil, fmt.Errorf("获取文章列表失败: %w", err)
}
// 3. 转换为响应对象
items := responses.FromArticleEntitiesToListItemList(articles)
response := &responses.ArticleListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}
s.logger.Info("获取文章列表成功", zap.Int64("total", total))
return response, nil
}
// ListArticlesForAdmin 获取文章列表(管理员端)
func (s *ArticleApplicationServiceImpl) ListArticlesForAdmin(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) {
// 1. 构建仓储查询
repoQuery := &repoQueries.ListArticleQuery{
Page: query.Page,
PageSize: query.PageSize,
Status: query.Status,
CategoryID: query.CategoryID,
TagID: query.TagID,
Title: query.Title,
Summary: query.Summary,
IsFeatured: query.IsFeatured,
OrderBy: query.OrderBy,
OrderDir: query.OrderDir,
}
// 2. 调用仓储
articles, total, err := s.articleRepo.ListArticlesForAdmin(ctx, repoQuery)
if err != nil {
s.logger.Error("获取文章列表失败", zap.Error(err))
return nil, fmt.Errorf("获取文章列表失败: %w", err)
}
// 3. 转换为响应对象
items := responses.FromArticleEntitiesToListItemList(articles)
response := &responses.ArticleListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}
s.logger.Info("获取文章列表成功", zap.Int64("total", total))
return response, nil
}
// PublishArticle 发布文章
func (s *ArticleApplicationServiceImpl) PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 发布文章
if err := article.Publish(); err != nil {
return fmt.Errorf("发布文章失败: %w", err)
}
// 3. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("发布文章失败: %w", err)
}
s.logger.Info("发布文章成功", zap.String("id", article.ID))
return nil
}
// PublishArticleByID 通过ID发布文章 (用于定时任务)
func (s *ArticleApplicationServiceImpl) PublishArticleByID(ctx context.Context, articleID string) error {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, articleID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", articleID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 检查是否已取消定时发布
if !article.IsScheduled() {
s.logger.Info("文章定时发布已取消,跳过执行",
zap.String("id", articleID),
zap.String("status", string(article.Status)))
return nil // 静默返回,不报错
}
// 3. 检查定时发布时间是否匹配
if article.ScheduledAt == nil {
s.logger.Info("文章没有定时发布时间,跳过执行",
zap.String("id", articleID))
return nil
}
// 4. 发布文章
if err := article.Publish(); err != nil {
return fmt.Errorf("发布文章失败: %w", err)
}
// 5. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("发布文章失败: %w", err)
}
s.logger.Info("定时发布文章成功", zap.String("id", article.ID))
return nil
}
// SchedulePublishArticle 定时发布文章
func (s *ArticleApplicationServiceImpl) SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error {
// 1. 解析定时发布时间
scheduledTime, err := cmd.GetScheduledTime()
if err != nil {
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
return fmt.Errorf("定时发布时间格式错误: %w", err)
}
// 2. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 3. 取消旧任务
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
s.logger.Warn("取消旧任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
}
// 4. 创建任务工厂
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
// 5. 创建并异步入队文章发布任务
if err := taskFactory.CreateAndEnqueueArticlePublishTask(
ctx,
cmd.ID,
scheduledTime,
"system", // 暂时使用系统用户ID
); err != nil {
s.logger.Error("创建并入队文章发布任务失败", zap.Error(err))
return err
}
// 6. 设置定时发布
if err := article.SchedulePublish(scheduledTime); err != nil {
return fmt.Errorf("设置定时发布失败: %w", err)
}
// 7. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("设置定时发布失败: %w", err)
}
s.logger.Info("设置定时发布成功", zap.String("id", article.ID), zap.Time("scheduled_time", scheduledTime))
return nil
}
// CancelSchedulePublishArticle 取消定时发布文章
func (s *ArticleApplicationServiceImpl) CancelSchedulePublishArticle(ctx context.Context, cmd *commands.CancelScheduleCommand) error {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 检查是否已设置定时发布
if !article.IsScheduled() {
return fmt.Errorf("文章未设置定时发布")
}
// 3. 取消定时任务
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
s.logger.Warn("取消定时任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
// 不返回错误,继续执行取消定时发布
}
// 4. 取消定时发布
if err := article.CancelSchedulePublish(); err != nil {
return fmt.Errorf("取消定时发布失败: %w", err)
}
// 5. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("取消定时发布失败: %w", err)
}
s.logger.Info("取消定时发布成功", zap.String("id", article.ID))
return nil
}
// ArchiveArticle 归档文章
func (s *ArticleApplicationServiceImpl) ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 归档文章
if err := article.Archive(); err != nil {
return fmt.Errorf("归档文章失败: %w", err)
}
// 3. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("归档文章失败: %w", err)
}
s.logger.Info("归档文章成功", zap.String("id", article.ID))
return nil
}
// SetFeatured 设置推荐状态
func (s *ArticleApplicationServiceImpl) SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 设置推荐状态
article.SetFeatured(cmd.IsFeatured)
// 3. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("设置推荐状态失败: %w", err)
}
s.logger.Info("设置推荐状态成功", zap.String("id", article.ID), zap.Bool("is_featured", cmd.IsFeatured))
return nil
}
// RecordView 记录阅读
func (s *ArticleApplicationServiceImpl) RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error {
// 1. 增加阅读量
if err := s.articleRepo.IncrementViewCount(ctx, articleID); err != nil {
s.logger.Error("增加阅读量失败", zap.String("id", articleID), zap.Error(err))
return fmt.Errorf("记录阅读失败: %w", err)
}
s.logger.Info("记录阅读成功", zap.String("id", articleID))
return nil
}
// GetArticleStats 获取文章统计
func (s *ArticleApplicationServiceImpl) GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error) {
// 1. 获取各种统计
totalArticles, err := s.articleRepo.CountByStatus(ctx, "")
if err != nil {
s.logger.Error("获取文章总数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
publishedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusPublished)
if err != nil {
s.logger.Error("获取已发布文章数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
draftArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusDraft)
if err != nil {
s.logger.Error("获取草稿文章数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
archivedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusArchived)
if err != nil {
s.logger.Error("获取归档文章数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
response := &responses.ArticleStatsResponse{
TotalArticles: totalArticles,
PublishedArticles: publishedArticles,
DraftArticles: draftArticles,
ArchivedArticles: archivedArticles,
TotalViews: 0, // TODO: 实现总阅读量统计
}
s.logger.Info("获取文章统计成功")
return response, nil
}
// validateCreateArticle 验证创建文章参数
func (s *ArticleApplicationServiceImpl) validateCreateArticle(cmd *commands.CreateArticleCommand) error {
if cmd.Title == "" {
return fmt.Errorf("文章标题不能为空")
}
if cmd.Content == "" {
return fmt.Errorf("文章内容不能为空")
}
return nil
}
// ==================== 分类相关方法 ====================
// CreateCategory 创建分类
func (s *ArticleApplicationServiceImpl) CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error {
// 1. 参数验证
if err := s.validateCreateCategory(cmd); err != nil {
return fmt.Errorf("参数验证失败: %w", err)
}
// 2. 创建分类实体
category := &entities.Category{
Name: cmd.Name,
Description: cmd.Description,
}
// 3. 保存分类
_, err := s.categoryRepo.Create(ctx, *category)
if err != nil {
s.logger.Error("创建分类失败", zap.Error(err))
return fmt.Errorf("创建分类失败: %w", err)
}
s.logger.Info("创建分类成功", zap.String("id", category.ID), zap.String("name", category.Name))
return nil
}
// UpdateCategory 更新分类
func (s *ArticleApplicationServiceImpl) UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error {
// 1. 获取原分类
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("分类不存在: %w", err)
}
// 2. 更新字段
category.Name = cmd.Name
category.Description = cmd.Description
// 3. 保存更新
if err := s.categoryRepo.Update(ctx, category); err != nil {
s.logger.Error("更新分类失败", zap.String("id", category.ID), zap.Error(err))
return fmt.Errorf("更新分类失败: %w", err)
}
s.logger.Info("更新分类成功", zap.String("id", category.ID))
return nil
}
// DeleteCategory 删除分类
func (s *ArticleApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error {
// 1. 检查分类是否存在
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("分类不存在: %w", err)
}
// 2. 检查是否有文章使用此分类
count, err := s.articleRepo.CountByCategoryID(ctx, cmd.ID)
if err != nil {
s.logger.Error("检查分类使用情况失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("删除分类失败: %w", err)
}
if count > 0 {
return fmt.Errorf("该分类下还有 %d 篇文章,无法删除", count)
}
// 3. 删除分类
if err := s.categoryRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除分类失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("删除分类失败: %w", err)
}
s.logger.Info("删除分类成功", zap.String("id", cmd.ID), zap.String("name", category.Name))
return nil
}
// GetCategoryByID 获取分类详情
func (s *ArticleApplicationServiceImpl) GetCategoryByID(ctx context.Context, query *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error) {
// 1. 获取分类
category, err := s.categoryRepo.GetByID(ctx, query.ID)
if err != nil {
s.logger.Error("获取分类失败", zap.String("id", query.ID), zap.Error(err))
return nil, fmt.Errorf("分类不存在: %w", err)
}
// 2. 转换为响应对象
response := &responses.CategoryInfoResponse{
ID: category.ID,
Name: category.Name,
Description: category.Description,
SortOrder: category.SortOrder,
CreatedAt: category.CreatedAt,
}
return response, nil
}
// ListCategories 获取分类列表
func (s *ArticleApplicationServiceImpl) ListCategories(ctx context.Context) (*responses.CategoryListResponse, error) {
// 1. 获取分类列表
categories, err := s.categoryRepo.List(ctx, shared_interfaces.ListOptions{})
if err != nil {
s.logger.Error("获取分类列表失败", zap.Error(err))
return nil, fmt.Errorf("获取分类列表失败: %w", err)
}
// 2. 转换为响应对象
items := make([]responses.CategoryInfoResponse, len(categories))
for i, category := range categories {
items[i] = responses.CategoryInfoResponse{
ID: category.ID,
Name: category.Name,
Description: category.Description,
SortOrder: category.SortOrder,
CreatedAt: category.CreatedAt,
}
}
response := &responses.CategoryListResponse{
Items: items,
Total: len(items),
}
return response, nil
}
// ==================== 标签相关方法 ====================
// CreateTag 创建标签
func (s *ArticleApplicationServiceImpl) CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error {
// 1. 参数验证
if err := s.validateCreateTag(cmd); err != nil {
return fmt.Errorf("参数验证失败: %w", err)
}
// 2. 创建标签实体
tag := &entities.Tag{
Name: cmd.Name,
Color: cmd.Color,
}
// 3. 保存标签
_, err := s.tagRepo.Create(ctx, *tag)
if err != nil {
s.logger.Error("创建标签失败", zap.Error(err))
return fmt.Errorf("创建标签失败: %w", err)
}
s.logger.Info("创建标签成功", zap.String("id", tag.ID), zap.String("name", tag.Name))
return nil
}
// UpdateTag 更新标签
func (s *ArticleApplicationServiceImpl) UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error {
// 1. 获取原标签
tag, err := s.tagRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("标签不存在: %w", err)
}
// 2. 更新字段
tag.Name = cmd.Name
tag.Color = cmd.Color
// 3. 保存更新
if err := s.tagRepo.Update(ctx, tag); err != nil {
s.logger.Error("更新标签失败", zap.String("id", tag.ID), zap.Error(err))
return fmt.Errorf("更新标签失败: %w", err)
}
s.logger.Info("更新标签成功", zap.String("id", tag.ID))
return nil
}
// DeleteTag 删除标签
func (s *ArticleApplicationServiceImpl) DeleteTag(ctx context.Context, cmd *commands.DeleteTagCommand) error {
// 1. 检查标签是否存在
tag, err := s.tagRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("标签不存在: %w", err)
}
// 2. 删除标签
if err := s.tagRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除标签失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("删除标签失败: %w", err)
}
s.logger.Info("删除标签成功", zap.String("id", cmd.ID), zap.String("name", tag.Name))
return nil
}
// GetTagByID 获取标签详情
func (s *ArticleApplicationServiceImpl) GetTagByID(ctx context.Context, query *appQueries.GetTagQuery) (*responses.TagInfoResponse, error) {
// 1. 获取标签
tag, err := s.tagRepo.GetByID(ctx, query.ID)
if err != nil {
s.logger.Error("获取标签失败", zap.String("id", query.ID), zap.Error(err))
return nil, fmt.Errorf("标签不存在: %w", err)
}
// 2. 转换为响应对象
response := &responses.TagInfoResponse{
ID: tag.ID,
Name: tag.Name,
Color: tag.Color,
CreatedAt: tag.CreatedAt,
}
return response, nil
}
// ListTags 获取标签列表
func (s *ArticleApplicationServiceImpl) ListTags(ctx context.Context) (*responses.TagListResponse, error) {
// 1. 获取标签列表
tags, err := s.tagRepo.List(ctx, shared_interfaces.ListOptions{})
if err != nil {
s.logger.Error("获取标签列表失败", zap.Error(err))
return nil, fmt.Errorf("获取标签列表失败: %w", err)
}
// 2. 转换为响应对象
items := make([]responses.TagInfoResponse, len(tags))
for i, tag := range tags {
items[i] = responses.TagInfoResponse{
ID: tag.ID,
Name: tag.Name,
Color: tag.Color,
CreatedAt: tag.CreatedAt,
}
}
response := &responses.TagListResponse{
Items: items,
Total: len(items),
}
return response, nil
}
// UpdateSchedulePublishArticle 修改定时发布时间
func (s *ArticleApplicationServiceImpl) UpdateSchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error {
// 1. 解析定时发布时间
scheduledTime, err := cmd.GetScheduledTime()
if err != nil {
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
return fmt.Errorf("定时发布时间格式错误: %w", err)
}
// 2. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 3. 检查是否已设置定时发布
if !article.IsScheduled() {
return fmt.Errorf("文章未设置定时发布,无法修改时间")
}
// 4. 更新数据库中的任务调度时间
if err := s.taskManager.UpdateTaskSchedule(ctx, cmd.ID, scheduledTime); err != nil {
s.logger.Error("更新任务调度时间失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("修改定时发布时间失败: %w", err)
}
// 5. 更新定时发布
if err := article.UpdateSchedulePublish(scheduledTime); err != nil {
return fmt.Errorf("更新定时发布失败: %w", err)
}
// 6. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("修改定时发布时间失败: %w", err)
}
s.logger.Info("修改定时发布时间成功",
zap.String("id", article.ID),
zap.Time("new_scheduled_time", scheduledTime))
return nil
}
// ==================== 验证方法 ====================
// validateCreateCategory 验证创建分类参数
func (s *ArticleApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error {
if cmd.Name == "" {
return fmt.Errorf("分类名称不能为空")
}
if len(cmd.Name) > 50 {
return fmt.Errorf("分类名称长度不能超过50个字符")
}
if len(cmd.Description) > 200 {
return fmt.Errorf("分类描述长度不能超过200个字符")
}
return nil
}
// validateCreateTag 验证创建标签参数
func (s *ArticleApplicationServiceImpl) validateCreateTag(cmd *commands.CreateTagCommand) error {
if cmd.Name == "" {
return fmt.Errorf("标签名称不能为空")
}
if len(cmd.Name) > 30 {
return fmt.Errorf("标签名称长度不能超过30个字符")
}
if cmd.Color == "" {
return fmt.Errorf("标签颜色不能为空")
}
// TODO: 添加十六进制颜色格式验证
return nil
}

View File

@@ -0,0 +1,104 @@
package commands
import (
"fmt"
"time"
)
// CreateAnnouncementCommand 创建公告命令
type CreateAnnouncementCommand struct {
Title string `json:"title" binding:"required" comment:"公告标题"`
Content string `json:"content" binding:"required" comment:"公告内容"`
}
// UpdateAnnouncementCommand 更新公告命令
type UpdateAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
Title string `json:"title" comment:"公告标题"`
Content string `json:"content" comment:"公告内容"`
}
// DeleteAnnouncementCommand 删除公告命令
type DeleteAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
}
// PublishAnnouncementCommand 发布公告命令
type PublishAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
}
// WithdrawAnnouncementCommand 撤回公告命令
type WithdrawAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
}
// ArchiveAnnouncementCommand 归档公告命令
type ArchiveAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
}
// SchedulePublishAnnouncementCommand 定时发布公告命令
type SchedulePublishAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
}
// GetScheduledTime 获取解析后的定时发布时间
func (cmd *SchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
// 定义中国东八区时区
cst := time.FixedZone("CST", 8*3600)
// 支持多种时间格式
formats := []string{
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
"2006-01-02 15:04", // "2025-09-02 14:12"
time.RFC3339, // "2025-09-02T14:12:01+08:00"
}
for _, format := range formats {
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
// 确保返回的时间是东八区时区
return t.In(cst), nil
}
}
return time.Time{}, fmt.Errorf("不支持的时间格式: %s请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
}
// UpdateSchedulePublishAnnouncementCommand 更新定时发布公告命令
type UpdateSchedulePublishAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
}
// GetScheduledTime 获取解析后的定时发布时间
func (cmd *UpdateSchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
// 定义中国东八区时区
cst := time.FixedZone("CST", 8*3600)
// 支持多种时间格式
formats := []string{
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
"2006-01-02 15:04", // "2025-09-02 14:12"
time.RFC3339, // "2025-09-02T14:12:01+08:00"
}
for _, format := range formats {
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
// 确保返回的时间是东八区时区
return t.In(cst), nil
}
}
return time.Time{}, fmt.Errorf("不支持的时间格式: %s请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
}
// CancelSchedulePublishAnnouncementCommand 取消定时发布公告命令
type CancelSchedulePublishAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
}

View File

@@ -0,0 +1,47 @@
package commands
// CreateArticleCommand 创建文章命令
type CreateArticleCommand struct {
Title string `json:"title" binding:"required" comment:"文章标题"`
Content string `json:"content" binding:"required" comment:"文章内容"`
Summary string `json:"summary" comment:"文章摘要"`
CoverImage string `json:"cover_image" comment:"封面图片"`
CategoryID string `json:"category_id" comment:"分类ID"`
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
}
// UpdateArticleCommand 更新文章命令
type UpdateArticleCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
Title string `json:"title" comment:"文章标题"`
Content string `json:"content" comment:"文章内容"`
Summary string `json:"summary" comment:"文章摘要"`
CoverImage string `json:"cover_image" comment:"封面图片"`
CategoryID string `json:"category_id" comment:"分类ID"`
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
}
// DeleteArticleCommand 删除文章命令
type DeleteArticleCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
}
// PublishArticleCommand 发布文章命令
type PublishArticleCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
}
// ArchiveArticleCommand 归档文章命令
type ArchiveArticleCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
}
// SetFeaturedCommand 设置推荐状态命令
type SetFeaturedCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
IsFeatured bool `json:"is_featured" binding:"required" comment:"是否推荐"`
}

View File

@@ -0,0 +1,6 @@
package commands
// CancelScheduleCommand 取消定时发布命令
type CancelScheduleCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
}

View File

@@ -0,0 +1,19 @@
package commands
// CreateCategoryCommand 创建分类命令
type CreateCategoryCommand struct {
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
}
// UpdateCategoryCommand 更新分类命令
type UpdateCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
}
// DeleteCategoryCommand 删除分类命令
type DeleteCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
}

View File

@@ -0,0 +1,36 @@
package commands
import (
"fmt"
"time"
)
// SchedulePublishCommand 定时发布文章命令
type SchedulePublishCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
}
// GetScheduledTime 获取解析后的定时发布时间
func (cmd *SchedulePublishCommand) GetScheduledTime() (time.Time, error) {
// 定义中国东八区时区
cst := time.FixedZone("CST", 8*3600)
// 支持多种时间格式
formats := []string{
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
"2006-01-02 15:04", // "2025-09-02 14:12"
time.RFC3339, // "2025-09-02T14:12:01+08:00"
}
for _, format := range formats {
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
// 确保返回的时间是东八区时区
return t.In(cst), nil
}
}
return time.Time{}, fmt.Errorf("不支持的时间格式: %s请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
}

View File

@@ -0,0 +1,19 @@
package commands
// CreateTagCommand 创建标签命令
type CreateTagCommand struct {
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
}
// UpdateTagCommand 更新标签命令
type UpdateTagCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
}
// DeleteTagCommand 删除标签命令
type DeleteTagCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
}

View File

@@ -0,0 +1,18 @@
package queries
import "tyapi-server/internal/domains/article/entities"
// ListAnnouncementQuery 公告列表查询
type ListAnnouncementQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
Status entities.AnnouncementStatus `form:"status" comment:"公告状态"`
Title string `form:"title" comment:"标题关键词"`
OrderBy string `form:"order_by" comment:"排序字段"`
OrderDir string `form:"order_dir" comment:"排序方向"`
}
// GetAnnouncementQuery 获取公告详情查询
type GetAnnouncementQuery struct {
ID string `uri:"id" binding:"required" comment:"公告ID"`
}

View File

@@ -0,0 +1,54 @@
package queries
import "tyapi-server/internal/domains/article/entities"
// ListArticleQuery 文章列表查询
type ListArticleQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
Status entities.ArticleStatus `form:"status" comment:"文章状态"`
CategoryID string `form:"category_id" comment:"分类ID"`
TagID string `form:"tag_id" comment:"标签ID"`
Title string `form:"title" comment:"标题关键词"`
Summary string `form:"summary" comment:"摘要关键词"`
IsFeatured *bool `form:"is_featured" comment:"是否推荐"`
OrderBy string `form:"order_by" comment:"排序字段"`
OrderDir string `form:"order_dir" comment:"排序方向"`
}
// SearchArticleQuery 文章搜索查询
type SearchArticleQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
Keyword string `form:"keyword" comment:"搜索关键词"`
CategoryID string `form:"category_id" comment:"分类ID"`
AuthorID string `form:"author_id" comment:"作者ID"`
Status entities.ArticleStatus `form:"status" comment:"文章状态"`
OrderBy string `form:"order_by" comment:"排序字段"`
OrderDir string `form:"order_dir" comment:"排序方向"`
}
// GetArticleQuery 获取文章详情查询
type GetArticleQuery struct {
ID string `uri:"id" binding:"required" comment:"文章ID"`
}
// GetArticlesByAuthorQuery 获取作者文章查询
type GetArticlesByAuthorQuery struct {
AuthorID string `uri:"author_id" binding:"required" comment:"作者ID"`
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
}
// GetArticlesByCategoryQuery 获取分类文章查询
type GetArticlesByCategoryQuery struct {
CategoryID string `uri:"category_id" binding:"required" comment:"分类ID"`
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
}
// GetFeaturedArticlesQuery 获取推荐文章查询
type GetFeaturedArticlesQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
}

View File

@@ -0,0 +1,6 @@
package queries
// GetCategoryQuery 获取分类详情查询
type GetCategoryQuery struct {
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
}

View File

@@ -0,0 +1,6 @@
package queries
// GetTagQuery 获取标签详情查询
type GetTagQuery struct {
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
}

View File

@@ -0,0 +1,79 @@
package responses
import (
"time"
"tyapi-server/internal/domains/article/entities"
)
// AnnouncementInfoResponse 公告详情响应
type AnnouncementInfoResponse struct {
ID string `json:"id" comment:"公告ID"`
Title string `json:"title" comment:"公告标题"`
Content string `json:"content" comment:"公告内容"`
Status string `json:"status" comment:"公告状态"`
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// AnnouncementListItemResponse 公告列表项响应
type AnnouncementListItemResponse struct {
ID string `json:"id" comment:"公告ID"`
Title string `json:"title" comment:"公告标题"`
Content string `json:"content" comment:"公告内容"`
Status string `json:"status" comment:"公告状态"`
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// AnnouncementListResponse 公告列表响应
type AnnouncementListResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []AnnouncementListItemResponse `json:"items" comment:"公告列表"`
}
// AnnouncementStatsResponse 公告统计响应
type AnnouncementStatsResponse struct {
TotalAnnouncements int64 `json:"total_announcements" comment:"公告总数"`
PublishedAnnouncements int64 `json:"published_announcements" comment:"已发布公告数"`
DraftAnnouncements int64 `json:"draft_announcements" comment:"草稿公告数"`
ArchivedAnnouncements int64 `json:"archived_announcements" comment:"归档公告数"`
ScheduledAnnouncements int64 `json:"scheduled_announcements" comment:"定时发布公告数"`
}
// FromAnnouncementEntity 从公告实体转换为响应对象
func FromAnnouncementEntity(announcement *entities.Announcement) *AnnouncementInfoResponse {
if announcement == nil {
return nil
}
return &AnnouncementInfoResponse{
ID: announcement.ID,
Title: announcement.Title,
Content: announcement.Content,
Status: string(announcement.Status),
ScheduledAt: announcement.ScheduledAt,
CreatedAt: announcement.CreatedAt,
UpdatedAt: announcement.UpdatedAt,
}
}
// FromAnnouncementEntityList 从公告实体列表转换为列表项响应
func FromAnnouncementEntityList(announcements []*entities.Announcement) []AnnouncementListItemResponse {
items := make([]AnnouncementListItemResponse, 0, len(announcements))
for _, announcement := range announcements {
items = append(items, AnnouncementListItemResponse{
ID: announcement.ID,
Title: announcement.Title,
Content: announcement.Content,
Status: string(announcement.Status),
ScheduledAt: announcement.ScheduledAt,
CreatedAt: announcement.CreatedAt,
UpdatedAt: announcement.UpdatedAt,
})
}
return items
}

View File

@@ -0,0 +1,219 @@
package responses
import (
"time"
"tyapi-server/internal/domains/article/entities"
)
// ArticleInfoResponse 文章详情响应
type ArticleInfoResponse struct {
ID string `json:"id" comment:"文章ID"`
Title string `json:"title" comment:"文章标题"`
Content string `json:"content" comment:"文章内容"`
Summary string `json:"summary" comment:"文章摘要"`
CoverImage string `json:"cover_image" comment:"封面图片"`
CategoryID string `json:"category_id" comment:"分类ID"`
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"`
Status string `json:"status" comment:"文章状态"`
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
ViewCount int `json:"view_count" comment:"阅读量"`
Tags []TagInfoResponse `json:"tags" comment:"标签列表"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// ArticleListItemResponse 文章列表项响应不包含content
type ArticleListItemResponse struct {
ID string `json:"id" comment:"文章ID"`
Title string `json:"title" comment:"文章标题"`
Summary string `json:"summary" comment:"文章摘要"`
CoverImage string `json:"cover_image" comment:"封面图片"`
CategoryID string `json:"category_id" comment:"分类ID"`
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"`
Status string `json:"status" comment:"文章状态"`
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
ViewCount int `json:"view_count" comment:"阅读量"`
Tags []TagInfoResponse `json:"tags" comment:"标签列表"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// ArticleListResponse 文章列表响应
type ArticleListResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []ArticleListItemResponse `json:"items" comment:"文章列表"`
}
// CategoryInfoResponse 分类信息响应
type CategoryInfoResponse struct {
ID string `json:"id" comment:"分类ID"`
Name string `json:"name" comment:"分类名称"`
Description string `json:"description" comment:"分类描述"`
SortOrder int `json:"sort_order" comment:"排序"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
}
// TagInfoResponse 标签信息响应
type TagInfoResponse struct {
ID string `json:"id" comment:"标签ID"`
Name string `json:"name" comment:"标签名称"`
Color string `json:"color" comment:"标签颜色"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
}
// CategoryListResponse 分类列表响应
type CategoryListResponse struct {
Items []CategoryInfoResponse `json:"items" comment:"分类列表"`
Total int `json:"total" comment:"总数"`
}
// TagListResponse 标签列表响应
type TagListResponse struct {
Items []TagInfoResponse `json:"items" comment:"标签列表"`
Total int `json:"total" comment:"总数"`
}
// ArticleStatsResponse 文章统计响应
type ArticleStatsResponse struct {
TotalArticles int64 `json:"total_articles" comment:"文章总数"`
PublishedArticles int64 `json:"published_articles" comment:"已发布文章数"`
DraftArticles int64 `json:"draft_articles" comment:"草稿文章数"`
ArchivedArticles int64 `json:"archived_articles" comment:"归档文章数"`
TotalViews int64 `json:"total_views" comment:"总阅读量"`
}
// FromArticleEntity 从文章实体转换为响应对象
func FromArticleEntity(article *entities.Article) *ArticleInfoResponse {
if article == nil {
return nil
}
response := &ArticleInfoResponse{
ID: article.ID,
Title: article.Title,
Content: article.Content,
Summary: article.Summary,
CoverImage: article.CoverImage,
CategoryID: article.CategoryID,
Status: string(article.Status),
IsFeatured: article.IsFeatured,
PublishedAt: article.PublishedAt,
ScheduledAt: article.ScheduledAt,
ViewCount: article.ViewCount,
CreatedAt: article.CreatedAt,
UpdatedAt: article.UpdatedAt,
}
// 转换分类信息
if article.Category != nil {
response.Category = &CategoryInfoResponse{
ID: article.Category.ID,
Name: article.Category.Name,
Description: article.Category.Description,
SortOrder: article.Category.SortOrder,
CreatedAt: article.Category.CreatedAt,
}
}
// 转换标签信息
if len(article.Tags) > 0 {
response.Tags = make([]TagInfoResponse, len(article.Tags))
for i, tag := range article.Tags {
response.Tags[i] = TagInfoResponse{
ID: tag.ID,
Name: tag.Name,
Color: tag.Color,
CreatedAt: tag.CreatedAt,
}
}
}
return response
}
// FromArticleEntityToListItem 从文章实体转换为列表项响应对象不包含content
func FromArticleEntityToListItem(article *entities.Article) *ArticleListItemResponse {
if article == nil {
return nil
}
response := &ArticleListItemResponse{
ID: article.ID,
Title: article.Title,
Summary: article.Summary,
CoverImage: article.CoverImage,
CategoryID: article.CategoryID,
Status: string(article.Status),
IsFeatured: article.IsFeatured,
PublishedAt: article.PublishedAt,
ScheduledAt: article.ScheduledAt,
ViewCount: article.ViewCount,
CreatedAt: article.CreatedAt,
UpdatedAt: article.UpdatedAt,
}
// 转换分类信息
if article.Category != nil {
response.Category = &CategoryInfoResponse{
ID: article.Category.ID,
Name: article.Category.Name,
Description: article.Category.Description,
SortOrder: article.Category.SortOrder,
CreatedAt: article.Category.CreatedAt,
}
}
// 转换标签信息
if len(article.Tags) > 0 {
response.Tags = make([]TagInfoResponse, len(article.Tags))
for i, tag := range article.Tags {
response.Tags[i] = TagInfoResponse{
ID: tag.ID,
Name: tag.Name,
Color: tag.Color,
CreatedAt: tag.CreatedAt,
}
}
}
return response
}
// FromArticleEntities 从文章实体列表转换为响应对象列表
func FromArticleEntities(articles []*entities.Article) []ArticleInfoResponse {
if len(articles) == 0 {
return []ArticleInfoResponse{}
}
responses := make([]ArticleInfoResponse, len(articles))
for i, article := range articles {
if response := FromArticleEntity(article); response != nil {
responses[i] = *response
}
}
return responses
}
// FromArticleEntitiesToListItemList 从文章实体列表转换为列表项响应对象列表不包含content
func FromArticleEntitiesToListItemList(articles []*entities.Article) []ArticleListItemResponse {
if len(articles) == 0 {
return []ArticleListItemResponse{}
}
responses := make([]ArticleListItemResponse, len(articles))
for i, article := range articles {
if response := FromArticleEntityToListItem(article); response != nil {
responses[i] = *response
}
}
return responses
}

View File

@@ -0,0 +1,126 @@
package article
import (
"context"
"fmt"
"time"
"tyapi-server/internal/domains/article/entities"
"tyapi-server/internal/domains/article/repositories"
"go.uber.org/zap"
)
// TaskManagementService 任务管理服务
type TaskManagementService struct {
scheduledTaskRepo repositories.ScheduledTaskRepository
logger *zap.Logger
}
// NewTaskManagementService 创建任务管理服务
func NewTaskManagementService(
scheduledTaskRepo repositories.ScheduledTaskRepository,
logger *zap.Logger,
) *TaskManagementService {
return &TaskManagementService{
scheduledTaskRepo: scheduledTaskRepo,
logger: logger,
}
}
// GetTaskStatus 获取任务状态
func (s *TaskManagementService) GetTaskStatus(ctx context.Context, taskID string) (*entities.ScheduledTask, error) {
task, err := s.scheduledTaskRepo.GetByTaskID(ctx, taskID)
if err != nil {
return nil, fmt.Errorf("获取任务状态失败: %w", err)
}
return &task, nil
}
// GetArticleTaskStatus 获取文章的定时任务状态
func (s *TaskManagementService) GetArticleTaskStatus(ctx context.Context, articleID string) (*entities.ScheduledTask, error) {
task, err := s.scheduledTaskRepo.GetByArticleID(ctx, articleID)
if err != nil {
return nil, fmt.Errorf("获取文章定时任务状态失败: %w", err)
}
return &task, nil
}
// CancelTask 取消任务
func (s *TaskManagementService) CancelTask(ctx context.Context, taskID string) error {
if err := s.scheduledTaskRepo.MarkAsCancelled(ctx, taskID); err != nil {
return fmt.Errorf("取消任务失败: %w", err)
}
s.logger.Info("任务已取消", zap.String("task_id", taskID))
return nil
}
// GetActiveTasks 获取活动任务列表
func (s *TaskManagementService) GetActiveTasks(ctx context.Context) ([]entities.ScheduledTask, error) {
tasks, err := s.scheduledTaskRepo.GetActiveTasks(ctx)
if err != nil {
return nil, fmt.Errorf("获取活动任务列表失败: %w", err)
}
return tasks, nil
}
// GetExpiredTasks 获取过期任务列表
func (s *TaskManagementService) GetExpiredTasks(ctx context.Context) ([]entities.ScheduledTask, error) {
tasks, err := s.scheduledTaskRepo.GetExpiredTasks(ctx)
if err != nil {
return nil, fmt.Errorf("获取过期任务列表失败: %w", err)
}
return tasks, nil
}
// CleanupExpiredTasks 清理过期任务
func (s *TaskManagementService) CleanupExpiredTasks(ctx context.Context) error {
expiredTasks, err := s.GetExpiredTasks(ctx)
if err != nil {
return err
}
for _, task := range expiredTasks {
if err := s.scheduledTaskRepo.MarkAsCancelled(ctx, task.TaskID); err != nil {
s.logger.Warn("清理过期任务失败", zap.String("task_id", task.TaskID), zap.Error(err))
continue
}
s.logger.Info("已清理过期任务", zap.String("task_id", task.TaskID))
}
return nil
}
// GetTaskStats 获取任务统计信息
func (s *TaskManagementService) GetTaskStats(ctx context.Context) (map[string]interface{}, error) {
activeTasks, err := s.GetActiveTasks(ctx)
if err != nil {
return nil, err
}
expiredTasks, err := s.GetExpiredTasks(ctx)
if err != nil {
return nil, err
}
stats := map[string]interface{}{
"active_tasks_count": len(activeTasks),
"expired_tasks_count": len(expiredTasks),
"total_tasks_count": len(activeTasks) + len(expiredTasks),
"next_task_time": nil,
"last_cleanup_time": time.Now(),
}
// 计算下一个任务时间
if len(activeTasks) > 0 {
nextTask := activeTasks[0]
for _, task := range activeTasks {
if task.ScheduledAt.Before(nextTask.ScheduledAt) {
nextTask = task
}
}
stats["next_task_time"] = nextTask.ScheduledAt
}
return stats, nil
}

View File

@@ -21,6 +21,9 @@ type CertificationApplicationService interface {
// 申请合同签署 // 申请合同签署
ApplyContract(ctx context.Context, cmd *commands.ApplyContractCommand) (*responses.ContractSignUrlResponse, error) ApplyContract(ctx context.Context, cmd *commands.ApplyContractCommand) (*responses.ContractSignUrlResponse, error)
// OCR营业执照识别
RecognizeBusinessLicense(ctx context.Context, imageBytes []byte) (*responses.BusinessLicenseResult, error)
// ================ 查询用例 ================ // ================ 查询用例 ================
// 获取认证详情 // 获取认证详情
@@ -29,6 +32,22 @@ type CertificationApplicationService interface {
// 获取认证列表(管理员) // 获取认证列表(管理员)
ListCertifications(ctx context.Context, query *queries.ListCertificationsQuery) (*responses.CertificationListResponse, error) ListCertifications(ctx context.Context, query *queries.ListCertificationsQuery) (*responses.CertificationListResponse, error)
// ================ 管理员后台操作用例 ================
// AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同)
AdminCompleteCertificationWithoutContract(ctx context.Context, cmd *commands.AdminCompleteCertificationCommand) (*responses.CertificationResponse, error)
// AdminListSubmitRecords 管理端分页查询企业信息提交记录
AdminListSubmitRecords(ctx context.Context, query *queries.AdminListSubmitRecordsQuery) (*responses.AdminSubmitRecordsListResponse, error)
// AdminGetSubmitRecordByID 管理端获取单条提交记录详情
AdminGetSubmitRecordByID(ctx context.Context, recordID string) (*responses.AdminSubmitRecordDetail, error)
// AdminApproveSubmitRecord 管理端审核通过(按提交记录 ID
AdminApproveSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
// AdminRejectSubmitRecord 管理端审核拒绝(按提交记录 ID
AdminRejectSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
// AdminTransitionCertificationStatus 管理端按用户变更认证状态以状态机为准info_submitted=通过 / info_rejected=拒绝)
AdminTransitionCertificationStatus(ctx context.Context, cmd *commands.AdminTransitionCertificationStatusCommand) error
// ================ e签宝回调处理 ================ // ================ e签宝回调处理 ================
// 处理e签宝回调 // 处理e签宝回调

View File

@@ -71,7 +71,20 @@ type EsignOrganization struct {
// 可以根据需要添加更多企业信息字段 // 可以根据需要添加更多企业信息字段
} }
// AdminCompleteCertificationCommand 管理员代用户完成认证命令(可不关联合同)
type AdminCompleteCertificationCommand struct {
// AdminID 从JWT中获取不从请求体传递因此不做必填校验
AdminID string `json:"-"`
UserID string `json:"user_id" validate:"required"`
CompanyName string `json:"company_name" validate:"required,min=2,max=100"`
UnifiedSocialCode string `json:"unified_social_code" validate:"required"`
LegalPersonName string `json:"legal_person_name" validate:"required,min=2,max=20"`
LegalPersonID string `json:"legal_person_id" validate:"required"`
LegalPersonPhone string `json:"legal_person_phone" validate:"required"`
EnterpriseAddress string `json:"enterprise_address" validate:"required"`
// 备注信息,用于记录后台操作原因
Reason string `json:"reason" validate:"required"`
}
// ForceTransitionStatusCommand 强制状态转换命令(管理员) // ForceTransitionStatusCommand 强制状态转换命令(管理员)
type ForceTransitionStatusCommand struct { type ForceTransitionStatusCommand struct {
CertificationID string `json:"certification_id" validate:"required"` CertificationID string `json:"certification_id" validate:"required"`
@@ -81,6 +94,14 @@ type ForceTransitionStatusCommand struct {
Force bool `json:"force,omitempty"` // 是否强制执行,跳过业务规则验证 Force bool `json:"force,omitempty"` // 是否强制执行,跳过业务规则验证
} }
// AdminTransitionCertificationStatusCommand 管理端变更认证状态(以状态机为准,用于审核通过/拒绝等)
type AdminTransitionCertificationStatusCommand struct {
AdminID string `json:"-"`
UserID string `json:"user_id" validate:"required"`
TargetStatus string `json:"target_status" validate:"required,oneof=info_submitted info_rejected"` // 审核通过 -> info_submitted审核拒绝 -> info_rejected
Remark string `json:"remark"`
}
// SubmitEnterpriseInfoCommand 提交企业信息命令 // SubmitEnterpriseInfoCommand 提交企业信息命令
type SubmitEnterpriseInfoCommand struct { type SubmitEnterpriseInfoCommand struct {
UserID string `json:"-" comment:"用户唯一标识从JWT token获取不在JSON中暴露"` UserID string `json:"-" comment:"用户唯一标识从JWT token获取不在JSON中暴露"`
@@ -91,4 +112,19 @@ type SubmitEnterpriseInfoCommand struct {
LegalPersonPhone string `json:"legal_person_phone" binding:"required,phone" comment:"法定代表人手机号11位13800138000"` LegalPersonPhone string `json:"legal_person_phone" binding:"required,phone" comment:"法定代表人手机号11位13800138000"`
EnterpriseAddress string `json:"enterprise_address" binding:"required,enterprise_address" comment:"企业地址,如:北京市海淀区"` EnterpriseAddress string `json:"enterprise_address" binding:"required,enterprise_address" comment:"企业地址,如:北京市海淀区"`
VerificationCode string `json:"verification_code" binding:"required,len=6" comment:"验证码"` VerificationCode string `json:"verification_code" binding:"required,len=6" comment:"验证码"`
// 营业执照图片 URL单张
BusinessLicenseImageURL string `json:"business_license_image_url" binding:"omitempty,url" comment:"营业执照图片URL"`
// 办公场地图片 URL 列表(前端传 string 数组)
OfficePlaceImageURLs []string `json:"office_place_image_urls" binding:"omitempty,dive,url" comment:"办公场地图片URL列表"`
// 授权代表信息(与前端 authorized_rep_* 及表字段一致)
AuthorizedRepName string `json:"authorized_rep_name" binding:"omitempty,min=2,max=20" comment:"授权代表姓名"`
AuthorizedRepID string `json:"authorized_rep_id" binding:"omitempty,id_card" comment:"授权代表身份证号"`
AuthorizedRepPhone string `json:"authorized_rep_phone" binding:"omitempty,phone" comment:"授权代表手机号"`
AuthorizedRepIDImageURLs []string `json:"authorized_rep_id_image_urls" binding:"omitempty,dive,url" comment:"授权代表身份证正反面图片URL"`
// 应用场景
APIUsage string `json:"api_usage" binding:"omitempty,min=5,max=500" comment:"接口用途及业务场景说明"`
ScenarioAttachmentURLs []string `json:"scenario_attachment_urls" binding:"omitempty,dive,url" comment:"场景附件图片URL列表"`
} }

View File

@@ -192,3 +192,13 @@ func (q *GetSystemMonitoringQuery) ShouldIncludeMetric(metric string) bool {
} }
return false return false
} }
// AdminListSubmitRecordsQuery 管理端企业信息提交记录列表查询(以状态机 certification_status 为准,不做审核状态筛选)
type AdminListSubmitRecordsQuery struct {
Page int `json:"page" form:"page"`
PageSize int `json:"page_size" form:"page_size"`
CertificationStatus string `json:"certification_status" form:"certification_status"` // 按认证状态筛选,如 info_pending_review / info_submitted / info_rejected空为全部
CompanyName string `json:"company_name" form:"company_name"` // 企业名称(模糊搜索)
LegalPersonPhone string `json:"legal_person_phone" form:"legal_person_phone"` // 法人手机号
LegalPersonName string `json:"legal_person_name" form:"legal_person_name"` // 法人姓名(模糊搜索)
}

View File

@@ -81,7 +81,6 @@ type ContractSignUrlResponse struct {
Message string `json:"message"` Message string `json:"message"`
} }
// SystemMonitoringResponse 系统监控响应 // SystemMonitoringResponse 系统监控响应
type SystemMonitoringResponse struct { type SystemMonitoringResponse struct {
TimeRange string `json:"time_range"` TimeRange string `json:"time_range"`
@@ -111,6 +110,55 @@ type SystemHealthStatus struct {
Details map[string]interface{} `json:"details,omitempty"` Details map[string]interface{} `json:"details,omitempty"`
} }
// AdminSubmitRecordItem 管理端提交记录列表项
type AdminSubmitRecordItem struct {
ID string `json:"id"`
UserID string `json:"user_id"`
CompanyName string `json:"company_name"`
UnifiedSocialCode string `json:"unified_social_code"`
LegalPersonName string `json:"legal_person_name"`
SubmitAt time.Time `json:"submit_at"`
Status string `json:"status"`
CertificationStatus string `json:"certification_status,omitempty"` // 以状态机为准info_pending_review/info_submitted/info_rejected 等
}
// AdminSubmitRecordDetail 管理端提交记录详情(含完整信息与图片 URL
type AdminSubmitRecordDetail struct {
ID string `json:"id"`
UserID string `json:"user_id"`
CompanyName string `json:"company_name"`
UnifiedSocialCode string `json:"unified_social_code"`
LegalPersonName string `json:"legal_person_name"`
LegalPersonID string `json:"legal_person_id"`
LegalPersonPhone string `json:"legal_person_phone"`
EnterpriseAddress string `json:"enterprise_address"`
AuthorizedRepName string `json:"authorized_rep_name"`
AuthorizedRepID string `json:"authorized_rep_id"`
AuthorizedRepPhone string `json:"authorized_rep_phone"`
AuthorizedRepIDImageURLs string `json:"authorized_rep_id_image_urls"` // JSON 字符串或解析后数组
BusinessLicenseImageURL string `json:"business_license_image_url"`
OfficePlaceImageURLs string `json:"office_place_image_urls"` // JSON 数组字符串
APIUsage string `json:"api_usage"`
ScenarioAttachmentURLs string `json:"scenario_attachment_urls"`
Status string `json:"status"`
SubmitAt time.Time `json:"submit_at"`
VerifiedAt *time.Time `json:"verified_at,omitempty"`
FailedAt *time.Time `json:"failed_at,omitempty"`
FailureReason string `json:"failure_reason,omitempty"`
CertificationStatus string `json:"certification_status,omitempty"` // 以状态机为准
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AdminSubmitRecordsListResponse 管理端提交记录列表响应
type AdminSubmitRecordsListResponse struct {
Items []*AdminSubmitRecordItem `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// ================ 响应构建辅助方法 ================ // ================ 响应构建辅助方法 ================
// NewCertificationListResponse 创建认证列表响应 // NewCertificationListResponse 创建认证列表响应
@@ -146,7 +194,6 @@ func NewContractSignUrlResponse(certificationID, signURL, contractURL, nextActio
return response return response
} }
// NewSystemAlert 创建系统警告 // NewSystemAlert 创建系统警告
func NewSystemAlert(level, alertType, message, metric string, value, threshold interface{}) *SystemAlert { func NewSystemAlert(level, alertType, message, metric string, value, threshold interface{}) *SystemAlert {
return &SystemAlert{ return &SystemAlert{
@@ -161,7 +208,6 @@ func NewSystemAlert(level, alertType, message, metric string, value, threshold i
} }
} }
// IsHealthy 检查系统是否健康 // IsHealthy 检查系统是否健康
func (r *SystemMonitoringResponse) IsHealthy() bool { func (r *SystemMonitoringResponse) IsHealthy() bool {
return r.SystemHealth.Overall == "healthy" return r.SystemHealth.Overall == "healthy"

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ type CreateWalletCommand struct {
UserID string `json:"user_id" binding:"required,uuid"` UserID string `json:"user_id" binding:"required,uuid"`
} }
// TransferRechargeCommand 对公转账充值命令 // TransferRechargeCommand 对公转账充值命令
type TransferRechargeCommand struct { type TransferRechargeCommand struct {
UserID string `json:"user_id" binding:"required,uuid"` UserID string `json:"user_id" binding:"required,uuid"`
@@ -21,7 +20,6 @@ type GiftRechargeCommand struct {
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"` Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
} }
// CreateAlipayRechargeCommand 创建支付宝充值订单命令 // CreateAlipayRechargeCommand 创建支付宝充值订单命令
type CreateAlipayRechargeCommand struct { type CreateAlipayRechargeCommand struct {
UserID string `json:"-"` // 用户ID从token获取 UserID string `json:"-"` // 用户ID从token获取
@@ -29,3 +27,12 @@ type CreateAlipayRechargeCommand struct {
Subject string `json:"-"` // 订单标题 Subject string `json:"-"` // 订单标题
Platform string `json:"platform" binding:"required,oneof=app h5 pc"` // 支付平台app/h5/pc Platform string `json:"platform" binding:"required,oneof=app h5 pc"` // 支付平台app/h5/pc
} }
// CreateWechatRechargeCommand 创建微信充值订单命令
type CreateWechatRechargeCommand struct {
UserID string `json:"-"` // 用户ID从token获取
Amount string `json:"amount" binding:"required"` // 充值金额
Subject string `json:"-"` // 订单标题
Platform string `json:"platform" binding:"required,oneof=wx_native native wx_h5 h5"` // 仅支持微信Native扫码兼容传入native/wx_h5/h5
OpenID string `json:"openid" binding:"omitempty"` // 前端可直接传入的 openid用于小程序/H5
}

View File

@@ -55,7 +55,9 @@ type RechargeRecordResponse struct {
RechargeType string `json:"recharge_type"` RechargeType string `json:"recharge_type"`
Status string `json:"status"` Status string `json:"status"`
AlipayOrderID string `json:"alipay_order_id,omitempty"` AlipayOrderID string `json:"alipay_order_id,omitempty"`
WechatOrderID string `json:"wechat_order_id,omitempty"`
TransferOrderID string `json:"transfer_order_id,omitempty"` TransferOrderID string `json:"transfer_order_id,omitempty"`
Platform string `json:"platform,omitempty"` // 支付平台pc/wx_native等
Notes string `json:"notes,omitempty"` Notes string `json:"notes,omitempty"`
OperatorID string `json:"operator_id,omitempty"` OperatorID string `json:"operator_id,omitempty"`
CompanyName string `json:"company_name,omitempty"` CompanyName string `json:"company_name,omitempty"`
@@ -108,6 +110,8 @@ type AlipayRechargeOrderResponse struct {
type RechargeConfigResponse struct { type RechargeConfigResponse struct {
MinAmount string `json:"min_amount"` // 最低充值金额 MinAmount string `json:"min_amount"` // 最低充值金额
MaxAmount string `json:"max_amount"` // 最高充值金额 MaxAmount string `json:"max_amount"` // 最高充值金额
RechargeBonusEnabled bool `json:"recharge_bonus_enabled"` // 是否启用充值赠送
ApiStoreRechargeTip string `json:"api_store_recharge_tip"` // API 商店充值提示(大额/批量联系商务)
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"` AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
} }
@@ -123,3 +127,45 @@ type UserSimpleResponse struct {
CompanyName string `json:"company_name"` CompanyName string `json:"company_name"`
Phone string `json:"phone"` Phone string `json:"phone"`
} }
// PurchaseRecordResponse 购买记录响应
type PurchaseRecordResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
OrderNo string `json:"order_no"`
TradeNo *string `json:"trade_no,omitempty"`
ProductID string `json:"product_id"`
ProductCode string `json:"product_code"`
ProductName string `json:"product_name"`
Category string `json:"category,omitempty"`
Subject string `json:"subject"`
Amount decimal.Decimal `json:"amount"`
PayAmount *decimal.Decimal `json:"pay_amount,omitempty"`
Status string `json:"status"`
Platform string `json:"platform"`
PayChannel string `json:"pay_channel"`
PaymentType string `json:"payment_type"`
BuyerID string `json:"buyer_id,omitempty"`
SellerID string `json:"seller_id,omitempty"`
ReceiptAmount decimal.Decimal `json:"receipt_amount,omitempty"`
NotifyTime *time.Time `json:"notify_time,omitempty"`
ReturnTime *time.Time `json:"return_time,omitempty"`
PayTime *time.Time `json:"pay_time,omitempty"`
FilePath *string `json:"file_path,omitempty"`
FileSize *int64 `json:"file_size,omitempty"`
Remark string `json:"remark,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
CompanyName string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// PurchaseRecordListResponse 购买记录列表响应
type PurchaseRecordListResponse struct {
Items []PurchaseRecordResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}

View File

@@ -0,0 +1,25 @@
package responses
import (
"time"
"github.com/shopspring/decimal"
)
// WechatOrderStatusResponse 微信订单状态响应
type WechatOrderStatusResponse struct {
OutTradeNo string `json:"out_trade_no"` // 商户订单号
TransactionID *string `json:"transaction_id"` // 微信支付交易号
Status string `json:"status"` // 订单状态
Amount decimal.Decimal `json:"amount"` // 订单金额
Subject string `json:"subject"` // 订单标题
Platform string `json:"platform"` // 支付平台
CreatedAt time.Time `json:"created_at"` // 创建时间
UpdatedAt time.Time `json:"updated_at"` // 更新时间
NotifyTime *time.Time `json:"notify_time"` // 异步通知时间
ReturnTime *time.Time `json:"return_time"` // 同步返回时间
ErrorCode *string `json:"error_code"` // 错误码
ErrorMessage *string `json:"error_message"` // 错误信息
IsProcessing bool `json:"is_processing"` // 是否处理中
CanRetry bool `json:"can_retry"` // 是否可以重试
}

View File

@@ -0,0 +1,12 @@
package responses
import "github.com/shopspring/decimal"
// WechatRechargeOrderResponse 微信充值下单响应
type WechatRechargeOrderResponse struct {
OutTradeNo string `json:"out_trade_no"` // 商户订单号
Amount decimal.Decimal `json:"amount"` // 充值金额
Platform string `json:"platform"` // 支付平台
Subject string `json:"subject"` // 订单标题
PrepayData interface{} `json:"prepay_data"` // 预支付数据APP预支付ID或JSAPI参数
}

View File

@@ -3,7 +3,6 @@ package finance
import ( import (
"context" "context"
"net/http" "net/http"
"tyapi-server/internal/application/finance/dto/commands" "tyapi-server/internal/application/finance/dto/commands"
"tyapi-server/internal/application/finance/dto/queries" "tyapi-server/internal/application/finance/dto/queries"
"tyapi-server/internal/application/finance/dto/responses" "tyapi-server/internal/application/finance/dto/responses"
@@ -12,29 +11,42 @@ import (
// FinanceApplicationService 财务应用服务接口 // FinanceApplicationService 财务应用服务接口
type FinanceApplicationService interface { type FinanceApplicationService interface {
// 钱包管理
CreateWallet(ctx context.Context, cmd *commands.CreateWalletCommand) (*responses.WalletResponse, error)
GetWallet(ctx context.Context, query *queries.GetWalletInfoQuery) (*responses.WalletResponse, error) GetWallet(ctx context.Context, query *queries.GetWalletInfoQuery) (*responses.WalletResponse, error)
// 充值管理
CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error) CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error)
CreateWechatRechargeOrder(ctx context.Context, cmd *commands.CreateWechatRechargeCommand) (*responses.WechatRechargeOrderResponse, error)
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error)
// 交易记录
GetUserWalletTransactions(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
// 导出功能
ExportAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
ExportAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
// 支付宝回调处理
HandleAlipayCallback(ctx context.Context, r *http.Request) error HandleAlipayCallback(ctx context.Context, r *http.Request) error
HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error) HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error) GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error)
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error) // 微信支付回调处理
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error) HandleWechatPayCallback(ctx context.Context, r *http.Request) error
HandleWechatRefundCallback(ctx context.Context, r *http.Request) error
GetWechatOrderStatus(ctx context.Context, outTradeNo string) (*responses.WechatOrderStatusResponse, error)
// 获取用户钱包交易记录 // 充值记录
GetUserWalletTransactions(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
// 管理端消费记录
GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
// 获取用户充值记录
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
// 管理员获取充值记录
GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
// 购买记录
GetUserPurchaseRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error)
GetAdminPurchaseRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error)
// 获取充值配置 // 获取充值配置
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error) GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
} }

View File

@@ -7,12 +7,14 @@ import (
"time" "time"
"tyapi-server/internal/application/finance/dto" "tyapi-server/internal/application/finance/dto"
"tyapi-server/internal/config"
"tyapi-server/internal/domains/finance/entities" "tyapi-server/internal/domains/finance/entities"
finance_repo "tyapi-server/internal/domains/finance/repositories" finance_repo "tyapi-server/internal/domains/finance/repositories"
"tyapi-server/internal/domains/finance/services" "tyapi-server/internal/domains/finance/services"
"tyapi-server/internal/domains/finance/value_objects" "tyapi-server/internal/domains/finance/value_objects"
user_repo "tyapi-server/internal/domains/user/repositories" user_repo "tyapi-server/internal/domains/user/repositories"
user_service "tyapi-server/internal/domains/user/services" user_service "tyapi-server/internal/domains/user/services"
"tyapi-server/internal/infrastructure/external/notification"
"tyapi-server/internal/infrastructure/external/storage" "tyapi-server/internal/infrastructure/external/storage"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
@@ -61,6 +63,7 @@ type InvoiceApplicationServiceImpl struct {
// 外部服务依赖 // 外部服务依赖
storageService *storage.QiNiuStorageService storageService *storage.QiNiuStorageService
logger *zap.Logger logger *zap.Logger
wechatWorkServer *notification.WeChatWorkService
} }
// NewInvoiceApplicationService 创建发票应用服务 // NewInvoiceApplicationService 创建发票应用服务
@@ -76,7 +79,13 @@ func NewInvoiceApplicationService(
userInvoiceInfoService services.UserInvoiceInfoService, userInvoiceInfoService services.UserInvoiceInfoService,
storageService *storage.QiNiuStorageService, storageService *storage.QiNiuStorageService,
logger *zap.Logger, logger *zap.Logger,
cfg *config.Config,
) InvoiceApplicationService { ) InvoiceApplicationService {
var wechatSvc *notification.WeChatWorkService
if cfg != nil && cfg.WechatWork.WebhookURL != "" {
wechatSvc = notification.NewWeChatWorkService(cfg.WechatWork.WebhookURL, cfg.WechatWork.Secret, logger)
}
return &InvoiceApplicationServiceImpl{ return &InvoiceApplicationServiceImpl{
invoiceRepo: invoiceRepo, invoiceRepo: invoiceRepo,
userInvoiceInfoRepo: userInvoiceInfoRepo, userInvoiceInfoRepo: userInvoiceInfoRepo,
@@ -89,6 +98,7 @@ func NewInvoiceApplicationService(
userInvoiceInfoService: userInvoiceInfoService, userInvoiceInfoService: userInvoiceInfoService,
storageService: storageService, storageService: storageService,
logger: logger, logger: logger,
wechatWorkServer: wechatSvc,
} }
} }
@@ -175,7 +185,7 @@ func (s *InvoiceApplicationServiceImpl) ApplyInvoice(ctx context.Context, userID
} }
// 10. 构建响应DTO // 10. 构建响应DTO
return &dto.InvoiceApplicationResponse{ resp := &dto.InvoiceApplicationResponse{
ID: application.ID, ID: application.ID,
UserID: application.UserID, UserID: application.UserID,
InvoiceType: application.InvoiceType, InvoiceType: application.InvoiceType,
@@ -183,7 +193,33 @@ func (s *InvoiceApplicationServiceImpl) ApplyInvoice(ctx context.Context, userID
Status: application.Status, Status: application.Status,
InvoiceInfo: invoiceInfo, InvoiceInfo: invoiceInfo,
CreatedAt: application.CreatedAt, CreatedAt: application.CreatedAt,
}, nil }
// 11. 企业微信通知(忽略发送错误),只使用企业名称和联系电话
if s.wechatWorkServer != nil {
companyName := userWithEnterprise.EnterpriseInfo.CompanyName
phone := user.Phone
if userWithEnterprise.EnterpriseInfo.LegalPersonPhone != "" {
phone = userWithEnterprise.EnterpriseInfo.LegalPersonPhone
}
content := fmt.Sprintf(
"### 【天远API】用户申请开发票\n"+
"> 企业名称:%s\n"+
"> 联系手机:%s\n"+
"> 申请开票金额:%s 元\n"+
"> 发票类型:%s\n"+
"> 申请时间:%s\n",
companyName,
phone,
application.Amount.String(),
string(application.InvoiceType),
time.Now().Format("2006-01-02 15:04:05"),
)
_ = s.wechatWorkServer.SendMarkdownMessage(ctx, content)
}
return resp, nil
} }
// GetUserInvoiceInfo 获取用户发票信息 // GetUserInvoiceInfo 获取用户发票信息
@@ -355,27 +391,56 @@ func (s *InvoiceApplicationServiceImpl) DownloadInvoiceFile(ctx context.Context,
return nil, fmt.Errorf("发票尚未通过审核") return nil, fmt.Errorf("发票尚未通过审核")
} }
// 4. 验证文件信息 fileContent, fileID, fileName, fileSize, fileURL, err := downloadInvoiceFileFromStorage(ctx, s.storageService, application)
if application.FileURL == nil || *application.FileURL == "" {
return nil, fmt.Errorf("发票文件不存在")
}
// 5. 从七牛云下载文件内容
fileContent, err := s.storageService.DownloadFile(ctx, *application.FileURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("下载文件失败: %w", err) return nil, err
} }
// 6. 构建响应DTO
return &dto.FileDownloadResponse{ return &dto.FileDownloadResponse{
FileID: *application.FileID, FileID: fileID,
FileName: *application.FileName, FileName: fileName,
FileSize: *application.FileSize, FileSize: fileSize,
FileURL: *application.FileURL, FileURL: fileURL,
FileContent: fileContent, FileContent: fileContent,
}, nil }, nil
} }
// downloadInvoiceFileFromStorage 从七牛云下载发票文件(优先用 FileID 生成新 URL避免私有空间签名过期
func downloadInvoiceFileFromStorage(ctx context.Context, storageService *storage.QiNiuStorageService, application *entities.InvoiceApplication) ([]byte, string, string, int64, string, error) {
if application.FileID == nil || *application.FileID == "" {
if application.FileURL == nil || *application.FileURL == "" {
return nil, "", "", 0, "", fmt.Errorf("发票文件不存在")
}
fileContent, err := storageService.DownloadFile(ctx, *application.FileURL)
if err != nil {
return nil, "", "", 0, "", fmt.Errorf("下载文件失败: %w", err)
}
return fileContent, derefString(application.FileID), derefString(application.FileName), derefInt64(application.FileSize), *application.FileURL, nil
}
fileContent, err := storageService.DownloadFileByKey(ctx, *application.FileID)
if err != nil {
return nil, "", "", 0, "", fmt.Errorf("下载文件失败: %w", err)
}
fileURL := storageService.GetFileURL(ctx, *application.FileID)
return fileContent, *application.FileID, derefString(application.FileName), derefInt64(application.FileSize), fileURL, nil
}
func derefString(v *string) string {
if v == nil {
return ""
}
return *v
}
func derefInt64(v *int64) int64 {
if v == nil {
return 0
}
return *v
}
// GetAvailableAmount 获取可开票金额 // GetAvailableAmount 获取可开票金额
func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context, userID string) (*dto.AvailableAmountResponse, error) { func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context, userID string) (*dto.AvailableAmountResponse, error) {
// 1. 验证用户是否存在 // 1. 验证用户是否存在
@@ -393,7 +458,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
return nil, err return nil, err
} }
// 3. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额 // 3. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID) realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -408,7 +473,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
// 5. 构建响应DTO // 5. 构建响应DTO
return &dto.AvailableAmountResponse{ return &dto.AvailableAmountResponse{
AvailableAmount: availableAmount, AvailableAmount: availableAmount,
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+对公转账) TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+微信充值+对公转账)
TotalGifted: totalGifted, TotalGifted: totalGifted,
TotalInvoiced: totalInvoiced, TotalInvoiced: totalInvoiced,
PendingApplications: pendingAmount, PendingApplications: pendingAmount,
@@ -417,7 +482,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
// calculateAvailableAmount 计算可开票金额(私有方法) // calculateAvailableAmount 计算可开票金额(私有方法)
func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) { func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
// 1. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额 // 1. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID) realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
if err != nil { if err != nil {
return decimal.Zero, err return decimal.Zero, err
@@ -433,7 +498,7 @@ func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Con
fmt.Println("totalInvoiced", totalInvoiced) fmt.Println("totalInvoiced", totalInvoiced)
fmt.Println("pendingAmount", pendingAmount) fmt.Println("pendingAmount", pendingAmount)
// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请 // 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
// 可开票金额 = 真实充值金额(支付宝充值+对公转账) - 已开票金额 - 待处理申请金额 // 可开票金额 = 真实充值金额(支付宝充值+微信充值+对公转账) - 已开票金额 - 待处理申请金额
availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount) availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
fmt.Println("availableAmount", availableAmount) fmt.Println("availableAmount", availableAmount)
// 确保可开票金额不为负数 // 确保可开票金额不为负数
@@ -452,16 +517,16 @@ func (s *InvoiceApplicationServiceImpl) getAmountSummary(ctx context.Context, us
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err) return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
} }
// 2. 计算真实充值金额(支付宝充值 + 对公转账)和总赠送金额 // 2. 计算真实充值金额(支付宝充值 + 微信充值 + 对公转账)和总赠送金额
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 对公转账 var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 微信充值 + 对公转账
var totalGifted decimal.Decimal // 总赠送金额 var totalGifted decimal.Decimal // 总赠送金额
for _, record := range rechargeRecords { for _, record := range rechargeRecords {
if record.IsSuccess() { if record.IsSuccess() {
if record.RechargeType == entities.RechargeTypeGift { if record.RechargeType == entities.RechargeTypeGift {
// 赠送金额不计入可开票金额 // 赠送金额不计入可开票金额
totalGifted = totalGifted.Add(record.Amount) totalGifted = totalGifted.Add(record.Amount)
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeTransfer { } else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeWechat || record.RechargeType == entities.RechargeTypeTransfer {
// 只有支付宝充值和对公转账计入可开票金额 // 支付宝充值、微信充值和对公转账计入可开票金额
realRecharged = realRecharged.Add(record.Amount) realRecharged = realRecharged.Add(record.Amount)
} }
} }
@@ -728,23 +793,16 @@ func (s *AdminInvoiceApplicationServiceImpl) DownloadInvoiceFile(ctx context.Con
return nil, fmt.Errorf("发票尚未通过审核") return nil, fmt.Errorf("发票尚未通过审核")
} }
// 3. 验证文件信息 fileContent, fileID, fileName, fileSize, fileURL, err := downloadInvoiceFileFromStorage(ctx, s.storageService, application)
if application.FileURL == nil || *application.FileURL == "" {
return nil, fmt.Errorf("发票文件不存在")
}
// 4. 从七牛云下载文件内容
fileContent, err := s.storageService.DownloadFile(ctx, *application.FileURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("下载文件失败: %w", err) return nil, err
} }
// 5. 构建响应DTO
return &dto.FileDownloadResponse{ return &dto.FileDownloadResponse{
FileID: *application.FileID, FileID: fileID,
FileName: *application.FileName, FileName: fileName,
FileSize: *application.FileSize, FileSize: fileSize,
FileURL: *application.FileURL, FileURL: fileURL,
FileContent: fileContent, FileContent: fileContent,
}, nil }, nil
} }

View File

@@ -0,0 +1,19 @@
package product
import (
"context"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses"
)
// CategoryApplicationService 分类应用服务接口
type CategoryApplicationService interface {
// 分类管理
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error)
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@ package product
import ( import (
"context" "context"
"fmt"
"strings"
"tyapi-server/internal/application/product/dto/commands" "tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/responses" "tyapi-server/internal/application/product/dto/responses"
@@ -28,6 +30,9 @@ type DocumentationApplicationServiceInterface interface {
// GetDocumentationsByProductIDs 批量获取文档 // GetDocumentationsByProductIDs 批量获取文档
GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error) GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error)
// GenerateFullDocumentation 生成完整的接口文档Markdown格式
GenerateFullDocumentation(ctx context.Context, productID string) (string, error)
} }
// DocumentationApplicationService 文档应用服务 // DocumentationApplicationService 文档应用服务
@@ -53,6 +58,7 @@ func (s *DocumentationApplicationService) CreateDocumentation(ctx context.Contex
ResponseFields: cmd.ResponseFields, ResponseFields: cmd.ResponseFields,
ResponseExample: cmd.ResponseExample, ResponseExample: cmd.ResponseExample,
ErrorCodes: cmd.ErrorCodes, ErrorCodes: cmd.ErrorCodes,
PDFFilePath: cmd.PDFFilePath,
} }
// 调用领域服务创建文档 // 调用领域服务创建文档
@@ -88,6 +94,20 @@ func (s *DocumentationApplicationService) UpdateDocumentation(ctx context.Contex
return nil, err return nil, err
} }
// 更新PDF文件路径如果提供
if cmd.PDFFilePath != "" {
doc.PDFFilePath = cmd.PDFFilePath
err = s.docService.UpdateDocumentationEntity(ctx, doc)
if err != nil {
return nil, fmt.Errorf("更新PDF文件路径失败: %w", err)
}
// 重新获取更新后的文档以确保获取最新数据
doc, err = s.docService.GetDocumentation(ctx, id)
if err != nil {
return nil, err
}
}
// 返回响应 // 返回响应
resp := responses.NewDocumentationResponse(doc) resp := responses.NewDocumentationResponse(doc)
return &resp, nil return &resp, nil
@@ -136,3 +156,93 @@ func (s *DocumentationApplicationService) GetDocumentationsByProductIDs(ctx cont
return docResponses, nil return docResponses, nil
} }
// GenerateFullDocumentation 生成完整的接口文档Markdown格式
func (s *DocumentationApplicationService) GenerateFullDocumentation(ctx context.Context, productID string) (string, error) {
// 通过产品ID获取文档
doc, err := s.docService.GetDocumentationByProductID(ctx, productID)
if err != nil {
return "", fmt.Errorf("获取文档失败: %w", err)
}
// 获取文档时已经包含了产品信息通过GetDocumentationWithProduct
// 如果没有产品信息通过文档ID获取
if doc.Product == nil && doc.ID != "" {
docWithProduct, err := s.docService.GetDocumentationWithProduct(ctx, doc.ID)
if err == nil && docWithProduct != nil {
doc = docWithProduct
}
}
var markdown strings.Builder
// 添加文档标题
productName := "产品"
if doc.Product != nil {
productName = doc.Product.Name
}
markdown.WriteString(fmt.Sprintf("# %s 接口文档\n\n", productName))
// 添加产品基本信息
if doc.Product != nil {
markdown.WriteString("## 产品信息\n\n")
markdown.WriteString(fmt.Sprintf("- **产品名称**: %s\n", doc.Product.Name))
markdown.WriteString(fmt.Sprintf("- **产品编号**: %s\n", doc.Product.Code))
if doc.Product.Description != "" {
markdown.WriteString(fmt.Sprintf("- **产品描述**: %s\n", doc.Product.Description))
}
markdown.WriteString("\n")
}
// 添加请求方式
markdown.WriteString("## 请求方式\n\n")
if doc.RequestURL != "" {
markdown.WriteString(fmt.Sprintf("- **请求方法**: %s\n", doc.RequestMethod))
markdown.WriteString(fmt.Sprintf("- **请求地址**: %s\n", doc.RequestURL))
markdown.WriteString("\n")
}
// 添加请求方式详细说明
if doc.BasicInfo != "" {
markdown.WriteString("### 请求方式说明\n\n")
markdown.WriteString(doc.BasicInfo)
markdown.WriteString("\n\n")
}
// 添加请求参数
if doc.RequestParams != "" {
markdown.WriteString("## 请求参数\n\n")
markdown.WriteString(doc.RequestParams)
markdown.WriteString("\n\n")
}
// 添加返回字段说明
if doc.ResponseFields != "" {
markdown.WriteString("## 返回字段说明\n\n")
markdown.WriteString(doc.ResponseFields)
markdown.WriteString("\n\n")
}
// 添加响应示例
if doc.ResponseExample != "" {
markdown.WriteString("## 响应示例\n\n")
markdown.WriteString(doc.ResponseExample)
markdown.WriteString("\n\n")
}
// 添加错误代码
if doc.ErrorCodes != "" {
markdown.WriteString("## 错误代码\n\n")
markdown.WriteString(doc.ErrorCodes)
markdown.WriteString("\n\n")
}
// 添加文档版本信息
markdown.WriteString("---\n\n")
markdown.WriteString(fmt.Sprintf("**文档版本**: %s\n\n", doc.Version))
if doc.UpdatedAt.Year() > 1900 {
markdown.WriteString(fmt.Sprintf("**更新时间**: %s\n", doc.UpdatedAt.Format("2006-01-02 15:04:05")))
}
return markdown.String(), nil
}

View File

@@ -10,6 +10,7 @@ type CreateDocumentationCommand struct {
ResponseFields string `json:"response_fields"` ResponseFields string `json:"response_fields"`
ResponseExample string `json:"response_example"` ResponseExample string `json:"response_example"`
ErrorCodes string `json:"error_codes"` ErrorCodes string `json:"error_codes"`
PDFFilePath string `json:"pdf_file_path,omitempty"`
} }
// UpdateDocumentationCommand 更新文档命令 // UpdateDocumentationCommand 更新文档命令
@@ -21,4 +22,5 @@ type UpdateDocumentationCommand struct {
ResponseFields string `json:"response_fields"` ResponseFields string `json:"response_fields"`
ResponseExample string `json:"response_example"` ResponseExample string `json:"response_example"`
ErrorCodes string `json:"error_codes"` ErrorCodes string `json:"error_codes"`
PDFFilePath string `json:"pdf_file_path,omitempty"`
} }

View File

@@ -6,12 +6,19 @@ type CreateProductCommand struct {
Code string `json:"code" binding:"required,product_code" comment:"产品编号"` Code string `json:"code" binding:"required,product_code" comment:"产品编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"` Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"`
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"` Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"` CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
SubCategoryID *string `json:"sub_category_id" binding:"omitempty,uuid" comment:"二级分类ID"`
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"` Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"`
Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"` IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"` IsVisible bool `json:"is_visible" comment:"是否展示"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsPackage bool `json:"is_package" comment:"是否组合包"`
// UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格组合包使用"`
// SEO信息 // SEO信息
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"` SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"` SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`
@@ -25,12 +32,19 @@ type UpdateProductCommand struct {
Code string `json:"code" binding:"required,product_code" comment:"产品编号"` Code string `json:"code" binding:"required,product_code" comment:"产品编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"` Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"`
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"` Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"` CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
SubCategoryID *string `json:"sub_category_id" binding:"omitempty,uuid" comment:"二级分类ID"`
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"` Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"`
Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"` IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"` IsVisible bool `json:"is_visible" comment:"是否展示"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsPackage bool `json:"is_package" comment:"是否组合包"`
// UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格组合包使用"`
// SEO信息 // SEO信息
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"` SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"` SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`

View File

@@ -0,0 +1,29 @@
package commands
// CreateSubCategoryCommand 创建二级分类命令
type CreateSubCategoryCommand struct {
Name string `json:"name" binding:"required,min=2,max=100" comment:"二级分类名称"`
Code string `json:"code" binding:"required,min=2,max=50" comment:"二级分类编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"二级分类描述"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
Sort int `json:"sort" binding:"min=0" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
}
// UpdateSubCategoryCommand 更新二级分类命令
type UpdateSubCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"二级分类ID"`
Name string `json:"name" binding:"required,min=2,max=100" comment:"二级分类名称"`
Code string `json:"code" binding:"required,min=2,max=50" comment:"二级分类编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"二级分类描述"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
Sort int `json:"sort" binding:"min=0" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
}
// DeleteSubCategoryCommand 删除二级分类命令
type DeleteSubCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"二级分类ID"`
}

View File

@@ -10,4 +10,14 @@ type CreateSubscriptionCommand struct {
type UpdateSubscriptionPriceCommand struct { type UpdateSubscriptionPriceCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"` ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"`
Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"` Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"`
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件价格组合包使用"`
}
// BatchUpdateSubscriptionPricesCommand 批量更新订阅价格命令
type BatchUpdateSubscriptionPricesCommand struct {
UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"`
AdjustmentType string `json:"adjustment_type" binding:"required,oneof=discount cost_multiple" comment:"调整方式(discount:按售价折扣,cost_multiple:按成本价倍数)"`
Discount float64 `json:"discount,omitempty" binding:"omitempty,min=0.1,max=10" comment:"折扣比例(0.1-10折)"`
CostMultiple float64 `json:"cost_multiple,omitempty" binding:"omitempty,min=0.1" comment:"成本价倍数"`
Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"`
} }

View File

@@ -0,0 +1,17 @@
package queries
// GetSubCategoryQuery 获取二级分类查询
type GetSubCategoryQuery struct {
ID string `json:"id" form:"id" binding:"omitempty,uuid" comment:"二级分类ID"`
}
// ListSubCategoriesQuery 获取二级分类列表查询
type ListSubCategoriesQuery struct {
Page int `json:"page" form:"page" binding:"min=1" comment:"页码"`
PageSize int `json:"page_size" form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
CategoryID string `json:"category_id" form:"category_id" binding:"omitempty,uuid" comment:"一级分类ID"`
IsEnabled *bool `json:"is_enabled" form:"is_enabled" comment:"是否启用"`
IsVisible *bool `json:"is_visible" form:"is_visible" comment:"是否展示"`
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序方向"`
}

View File

@@ -30,3 +30,37 @@ type CategorySimpleResponse struct {
Name string `json:"name" comment:"分类名称"` Name string `json:"name" comment:"分类名称"`
Code string `json:"code" comment:"分类编号"` Code string `json:"code" comment:"分类编号"`
} }
// SubCategoryInfoResponse 二级分类详情响应
type SubCategoryInfoResponse struct {
ID string `json:"id" comment:"二级分类ID"`
Name string `json:"name" comment:"二级分类名称"`
Code string `json:"code" comment:"二级分类编号"`
Description string `json:"description" comment:"二级分类描述"`
CategoryID string `json:"category_id" comment:"一级分类ID"`
Sort int `json:"sort" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
// 关联信息
Category *CategoryInfoResponse `json:"category,omitempty" comment:"一级分类信息"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// SubCategoryListResponse 二级分类列表响应
type SubCategoryListResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []SubCategoryInfoResponse `json:"items" comment:"二级分类列表"`
}
// SubCategorySimpleResponse 二级分类简单信息响应
type SubCategorySimpleResponse struct {
ID string `json:"id" comment:"二级分类ID"`
Name string `json:"name" comment:"二级分类名称"`
Code string `json:"code" comment:"二级分类编号"`
CategoryID string `json:"category_id" comment:"一级分类ID"`
}

View File

@@ -18,6 +18,7 @@ type DocumentationResponse struct {
ResponseExample string `json:"response_example"` ResponseExample string `json:"response_example"`
ErrorCodes string `json:"error_codes"` ErrorCodes string `json:"error_codes"`
Version string `json:"version"` Version string `json:"version"`
PDFFilePath string `json:"pdf_file_path,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
@@ -35,6 +36,7 @@ func NewDocumentationResponse(doc *entities.ProductDocumentation) DocumentationR
ResponseExample: doc.ResponseExample, ResponseExample: doc.ResponseExample,
ErrorCodes: doc.ErrorCodes, ErrorCodes: doc.ErrorCodes,
Version: doc.Version, Version: doc.Version,
PDFFilePath: doc.PDFFilePath,
CreatedAt: doc.CreatedAt, CreatedAt: doc.CreatedAt,
UpdatedAt: doc.UpdatedAt, UpdatedAt: doc.UpdatedAt,
} }

View File

@@ -10,6 +10,7 @@ type PackageItemResponse struct {
ProductName string `json:"product_name" comment:"子产品名称"` ProductName string `json:"product_name" comment:"子产品名称"`
SortOrder int `json:"sort_order" comment:"排序"` SortOrder int `json:"sort_order" comment:"排序"`
Price float64 `json:"price" comment:"子产品价格"` Price float64 `json:"price" comment:"子产品价格"`
CostPrice float64 `json:"cost_price" comment:"子产品成本价"`
} }
// ProductInfoResponse 产品详情响应 // ProductInfoResponse 产品详情响应
@@ -20,11 +21,15 @@ type ProductInfoResponse struct {
Code string `json:"code" comment:"产品编号"` Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"` Description string `json:"description" comment:"产品简介"`
Content string `json:"content" comment:"产品内容"` Content string `json:"content" comment:"产品内容"`
CategoryID string `json:"category_id" comment:"产品分类ID"` CategoryID string `json:"category_id" comment:"一级分类ID"`
SubCategoryID *string `json:"sub_category_id,omitempty" comment:"二级分类ID"`
Price float64 `json:"price" comment:"产品价格"` Price float64 `json:"price" comment:"产品价格"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"` IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsPackage bool `json:"is_package" comment:"是否组合包"`
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"` IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
// UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格组合包使用"`
// SEO信息 // SEO信息
SEOTitle string `json:"seo_title" comment:"SEO标题"` SEOTitle string `json:"seo_title" comment:"SEO标题"`
@@ -32,7 +37,8 @@ type ProductInfoResponse struct {
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"` SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
// 关联信息 // 关联信息
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"` Category *CategoryInfoResponse `json:"category,omitempty" comment:"一级分类信息"`
SubCategory *SubCategoryInfoResponse `json:"sub_category,omitempty" comment:"二级分类信息"`
// 组合包信息 // 组合包信息
PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"` PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"`
@@ -70,6 +76,13 @@ type ProductSimpleResponse struct {
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"` IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
} }
// ProductSimpleAdminResponse 管理员产品简单信息响应(包含成本价)
type ProductSimpleAdminResponse struct {
ProductSimpleResponse
CostPrice float64 `json:"cost_price" comment:"成本价"`
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件价格组合包使用"`
}
// ProductStatsResponse 产品统计响应 // ProductStatsResponse 产品统计响应
type ProductStatsResponse struct { type ProductStatsResponse struct {
TotalProducts int64 `json:"total_products" comment:"产品总数"` TotalProducts int64 `json:"total_products" comment:"产品总数"`
@@ -86,19 +99,27 @@ type ProductAdminInfoResponse struct {
Code string `json:"code" comment:"产品编号"` Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"` Description string `json:"description" comment:"产品简介"`
Content string `json:"content" comment:"产品内容"` Content string `json:"content" comment:"产品内容"`
CategoryID string `json:"category_id" comment:"产品分类ID"` CategoryID string `json:"category_id" comment:"一级分类ID"`
SubCategoryID *string `json:"sub_category_id,omitempty" comment:"二级分类ID"`
Price float64 `json:"price" comment:"产品价格"` Price float64 `json:"price" comment:"产品价格"`
CostPrice float64 `json:"cost_price" comment:"成本价"`
Remark string `json:"remark" comment:"备注"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"` IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否可见"` IsVisible bool `json:"is_visible" comment:"是否可见"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsPackage bool `json:"is_package" comment:"是否组合包"`
// UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格组合包使用"`
// SEO信息 // SEO信息
SEOTitle string `json:"seo_title" comment:"SEO标题"` SEOTitle string `json:"seo_title" comment:"SEO标题"`
SEODescription string `json:"seo_description" comment:"SEO描述"` SEODescription string `json:"seo_description" comment:"SEO描述"`
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"` SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
// 关联信息 // 关联信息
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"` Category *CategoryInfoResponse `json:"category,omitempty" comment:"一级分类信息"`
SubCategory *SubCategoryInfoResponse `json:"sub_category,omitempty" comment:"二级分类信息"`
// 组合包信息 // 组合包信息
PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"` PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"`

View File

@@ -17,11 +17,14 @@ type SubscriptionInfoResponse struct {
UserID string `json:"user_id" comment:"用户ID"` UserID string `json:"user_id" comment:"用户ID"`
ProductID string `json:"product_id" comment:"产品ID"` ProductID string `json:"product_id" comment:"产品ID"`
Price float64 `json:"price" comment:"订阅价格"` Price float64 `json:"price" comment:"订阅价格"`
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件价格组合包使用"`
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"` APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
// 关联信息 // 关联信息
User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"` User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"`
Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"` Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"`
// 管理员端使用,包含成本价的产品信息
ProductAdmin *ProductSimpleAdminResponse `json:"product_admin,omitempty" comment:"产品信息(管理员端,包含成本价)"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"` CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`

View File

@@ -11,7 +11,8 @@ import (
// ProductApplicationService 产品应用服务接口 // ProductApplicationService 产品应用服务接口
type ProductApplicationService interface { type ProductApplicationService interface {
// 产品管理 // 产品管理
CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error)
UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error
DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error
@@ -38,46 +39,15 @@ type ProductApplicationService interface {
ReorderPackageItems(ctx context.Context, packageID string, cmd *commands.ReorderPackageItemsCommand) error ReorderPackageItems(ctx context.Context, packageID string, cmd *commands.ReorderPackageItemsCommand) error
UpdatePackageItems(ctx context.Context, packageID string, cmd *commands.UpdatePackageItemsCommand) error UpdatePackageItems(ctx context.Context, packageID string, cmd *commands.UpdatePackageItemsCommand) error
// 可选子产品查询 // 可选子产品查询(管理员端,返回包含成本价的数据)
GetAvailableProducts(ctx context.Context, query *queries.GetAvailableProductsQuery) (*responses.ProductListResponse, error) GetAvailableProducts(ctx context.Context, query *queries.GetAvailableProductsQuery) (*responses.ProductAdminListResponse, error)
// API配置管理 // API配置管理
GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error) GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error)
CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
DeleteProductApiConfig(ctx context.Context, configID string) error DeleteProductApiConfig(ctx context.Context, configID string) error
}
// 产品字典导出
// CategoryApplicationService 分类应用服务接口 ExportProductDictionary(ctx context.Context, format string) ([]byte, error)
type CategoryApplicationService interface {
// 分类管理
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error)
}
// SubscriptionApplicationService 订阅应用服务接口
type SubscriptionApplicationService interface {
// 订阅管理
UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error
// 订阅管理
CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error
GetSubscriptionByID(ctx context.Context, query *queries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error)
ListSubscriptions(ctx context.Context, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
// 我的订阅(用户专用)
ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error)
// 业务查询
GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
GetProductSubscriptions(ctx context.Context, query *queries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error)
// 统计
GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error)
} }

View File

@@ -0,0 +1,16 @@
package product
import "context"
// SelfSubscribePolicy 是否允许用户在控制台自助发起「订阅产品」
type SelfSubscribePolicy interface {
Allow(ctx context.Context, userID string) (allowed bool, message string, err error)
}
// DefaultAllowSelfSubscribe 未装配下属模块时:恒允许
type DefaultAllowSelfSubscribe struct{}
// Allow 恒允许
func (DefaultAllowSelfSubscribe) Allow(_ context.Context, _ string) (bool, string, error) {
return true, "", nil
}

View File

@@ -0,0 +1,20 @@
package product
import (
"context"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses"
)
// SubCategoryApplicationService 二级分类应用服务接口
type SubCategoryApplicationService interface {
// 二级分类管理
CreateSubCategory(ctx context.Context, cmd *commands.CreateSubCategoryCommand) error
UpdateSubCategory(ctx context.Context, cmd *commands.UpdateSubCategoryCommand) error
DeleteSubCategory(ctx context.Context, cmd *commands.DeleteSubCategoryCommand) error
GetSubCategoryByID(ctx context.Context, query *queries.GetSubCategoryQuery) (*responses.SubCategoryInfoResponse, error)
ListSubCategories(ctx context.Context, query *queries.ListSubCategoriesQuery) (*responses.SubCategoryListResponse, error)
ListSubCategoriesByCategoryID(ctx context.Context, categoryID string) ([]*responses.SubCategorySimpleResponse, error)
}

View File

@@ -0,0 +1,322 @@
package product
import (
"context"
"errors"
"fmt"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
"go.uber.org/zap"
)
// SubCategoryApplicationServiceImpl 二级分类应用服务实现
type SubCategoryApplicationServiceImpl struct {
categoryRepo repositories.ProductCategoryRepository
subCategoryRepo repositories.ProductSubCategoryRepository
logger *zap.Logger
}
// NewSubCategoryApplicationService 创建二级分类应用服务
func NewSubCategoryApplicationService(
categoryRepo repositories.ProductCategoryRepository,
subCategoryRepo repositories.ProductSubCategoryRepository,
logger *zap.Logger,
) SubCategoryApplicationService {
return &SubCategoryApplicationServiceImpl{
categoryRepo: categoryRepo,
subCategoryRepo: subCategoryRepo,
logger: logger,
}
}
// CreateSubCategory 创建二级分类
func (s *SubCategoryApplicationServiceImpl) CreateSubCategory(ctx context.Context, cmd *commands.CreateSubCategoryCommand) error {
// 1. 参数验证
if err := s.validateCreateSubCategory(cmd); err != nil {
return err
}
// 2. 验证一级分类是否存在
category, err := s.categoryRepo.GetByID(ctx, cmd.CategoryID)
if err != nil {
return fmt.Errorf("一级分类不存在: %w", err)
}
if !category.IsValid() {
return errors.New("一级分类已禁用或删除")
}
// 3. 验证二级分类编号唯一性
if err := s.validateSubCategoryCode(cmd.Code, "", cmd.CategoryID); err != nil {
return err
}
// 4. 创建二级分类实体
subCategory := &entities.ProductSubCategory{
Name: cmd.Name,
Code: cmd.Code,
Description: cmd.Description,
CategoryID: cmd.CategoryID,
Sort: cmd.Sort,
IsEnabled: cmd.IsEnabled,
IsVisible: cmd.IsVisible,
}
// 5. 保存到仓储
createdSubCategory, err := s.subCategoryRepo.Create(ctx, *subCategory)
if err != nil {
s.logger.Error("创建二级分类失败", zap.Error(err), zap.String("code", cmd.Code))
return fmt.Errorf("创建二级分类失败: %w", err)
}
s.logger.Info("创建二级分类成功", zap.String("id", createdSubCategory.ID), zap.String("code", cmd.Code))
return nil
}
// UpdateSubCategory 更新二级分类
func (s *SubCategoryApplicationServiceImpl) UpdateSubCategory(ctx context.Context, cmd *commands.UpdateSubCategoryCommand) error {
// 1. 参数验证
if err := s.validateUpdateSubCategory(cmd); err != nil {
return err
}
// 2. 获取现有二级分类
existingSubCategory, err := s.subCategoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("二级分类不存在: %w", err)
}
// 3. 验证一级分类是否存在
category, err := s.categoryRepo.GetByID(ctx, cmd.CategoryID)
if err != nil {
return fmt.Errorf("一级分类不存在: %w", err)
}
if !category.IsValid() {
return errors.New("一级分类已禁用或删除")
}
// 4. 验证二级分类编号唯一性(排除当前分类)
if err := s.validateSubCategoryCode(cmd.Code, cmd.ID, cmd.CategoryID); err != nil {
return err
}
// 5. 更新二级分类信息
existingSubCategory.Name = cmd.Name
existingSubCategory.Code = cmd.Code
existingSubCategory.Description = cmd.Description
existingSubCategory.CategoryID = cmd.CategoryID
existingSubCategory.Sort = cmd.Sort
existingSubCategory.IsEnabled = cmd.IsEnabled
existingSubCategory.IsVisible = cmd.IsVisible
// 6. 保存到仓储
if err := s.subCategoryRepo.Update(ctx, *existingSubCategory); err != nil {
s.logger.Error("更新二级分类失败", zap.Error(err), zap.String("id", cmd.ID))
return fmt.Errorf("更新二级分类失败: %w", err)
}
s.logger.Info("更新二级分类成功", zap.String("id", cmd.ID), zap.String("code", cmd.Code))
return nil
}
// DeleteSubCategory 删除二级分类
func (s *SubCategoryApplicationServiceImpl) DeleteSubCategory(ctx context.Context, cmd *commands.DeleteSubCategoryCommand) error {
// 1. 检查二级分类是否存在
existingSubCategory, err := s.subCategoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("二级分类不存在: %w", err)
}
// 2. 删除二级分类
if err := s.subCategoryRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除二级分类失败", zap.Error(err), zap.String("id", cmd.ID))
return fmt.Errorf("删除二级分类失败: %w", err)
}
s.logger.Info("删除二级分类成功", zap.String("id", cmd.ID), zap.String("code", existingSubCategory.Code))
return nil
}
// GetSubCategoryByID 根据ID获取二级分类
func (s *SubCategoryApplicationServiceImpl) GetSubCategoryByID(ctx context.Context, query *queries.GetSubCategoryQuery) (*responses.SubCategoryInfoResponse, error) {
subCategory, err := s.subCategoryRepo.GetByID(ctx, query.ID)
if err != nil {
return nil, fmt.Errorf("二级分类不存在: %w", err)
}
// 加载一级分类信息
if subCategory.CategoryID != "" {
category, err := s.categoryRepo.GetByID(ctx, subCategory.CategoryID)
if err == nil {
subCategory.Category = &category
}
}
// 转换为响应对象
response := s.convertToSubCategoryInfoResponse(subCategory)
return response, nil
}
// ListSubCategories 获取二级分类列表
func (s *SubCategoryApplicationServiceImpl) ListSubCategories(ctx context.Context, query *queries.ListSubCategoriesQuery) (*responses.SubCategoryListResponse, error) {
// 构建查询条件
categoryID := query.CategoryID
isEnabled := query.IsEnabled
isVisible := query.IsVisible
var subCategories []*entities.ProductSubCategory
var err error
// 根据条件查询
if categoryID != "" {
// 按一级分类查询
subCategories, err = s.subCategoryRepo.FindByCategoryID(ctx, categoryID)
} else {
// 查询所有二级分类
subCategories, err = s.subCategoryRepo.List(ctx)
}
if err != nil {
s.logger.Error("获取二级分类列表失败", zap.Error(err))
return nil, fmt.Errorf("获取二级分类列表失败: %w", err)
}
// 过滤状态
filteredSubCategories := make([]*entities.ProductSubCategory, 0)
for _, subCategory := range subCategories {
if isEnabled != nil && *isEnabled != subCategory.IsEnabled {
continue
}
if isVisible != nil && *isVisible != subCategory.IsVisible {
continue
}
filteredSubCategories = append(filteredSubCategories, subCategory)
}
// 加载一级分类信息
for _, subCategory := range filteredSubCategories {
if subCategory.CategoryID != "" {
category, err := s.categoryRepo.GetByID(ctx, subCategory.CategoryID)
if err == nil {
subCategory.Category = &category
}
}
}
// 转换为响应对象
items := make([]responses.SubCategoryInfoResponse, len(filteredSubCategories))
for i, subCategory := range filteredSubCategories {
items[i] = *s.convertToSubCategoryInfoResponse(subCategory)
}
return &responses.SubCategoryListResponse{
Total: int64(len(items)),
Page: query.Page,
Size: query.PageSize,
Items: items,
}, nil
}
// ListSubCategoriesByCategoryID 根据一级分类ID获取二级分类列表
func (s *SubCategoryApplicationServiceImpl) ListSubCategoriesByCategoryID(ctx context.Context, categoryID string) ([]*responses.SubCategorySimpleResponse, error) {
subCategories, err := s.subCategoryRepo.FindByCategoryID(ctx, categoryID)
if err != nil {
return nil, fmt.Errorf("获取二级分类列表失败: %w", err)
}
// 转换为响应对象
items := make([]*responses.SubCategorySimpleResponse, len(subCategories))
for i, subCategory := range subCategories {
items[i] = &responses.SubCategorySimpleResponse{
ID: subCategory.ID,
Name: subCategory.Name,
Code: subCategory.Code,
CategoryID: subCategory.CategoryID,
}
}
return items, nil
}
// convertToSubCategoryInfoResponse 转换为二级分类信息响应
func (s *SubCategoryApplicationServiceImpl) convertToSubCategoryInfoResponse(subCategory *entities.ProductSubCategory) *responses.SubCategoryInfoResponse {
response := &responses.SubCategoryInfoResponse{
ID: subCategory.ID,
Name: subCategory.Name,
Code: subCategory.Code,
Description: subCategory.Description,
CategoryID: subCategory.CategoryID,
Sort: subCategory.Sort,
IsEnabled: subCategory.IsEnabled,
IsVisible: subCategory.IsVisible,
CreatedAt: subCategory.CreatedAt,
UpdatedAt: subCategory.UpdatedAt,
}
// 添加一级分类信息
if subCategory.Category != nil {
response.Category = &responses.CategoryInfoResponse{
ID: subCategory.Category.ID,
Name: subCategory.Category.Name,
Description: subCategory.Category.Description,
Sort: subCategory.Category.Sort,
IsEnabled: subCategory.Category.IsEnabled,
IsVisible: subCategory.Category.IsVisible,
CreatedAt: subCategory.Category.CreatedAt,
UpdatedAt: subCategory.Category.UpdatedAt,
}
}
return response
}
// validateCreateSubCategory 验证创建二级分类参数
func (s *SubCategoryApplicationServiceImpl) validateCreateSubCategory(cmd *commands.CreateSubCategoryCommand) error {
if cmd.Name == "" {
return errors.New("二级分类名称不能为空")
}
if cmd.Code == "" {
return errors.New("二级分类编号不能为空")
}
if cmd.CategoryID == "" {
return errors.New("一级分类ID不能为空")
}
return nil
}
// validateUpdateSubCategory 验证更新二级分类参数
func (s *SubCategoryApplicationServiceImpl) validateUpdateSubCategory(cmd *commands.UpdateSubCategoryCommand) error {
if cmd.ID == "" {
return errors.New("二级分类ID不能为空")
}
if cmd.Name == "" {
return errors.New("二级分类名称不能为空")
}
if cmd.Code == "" {
return errors.New("二级分类编号不能为空")
}
if cmd.CategoryID == "" {
return errors.New("一级分类ID不能为空")
}
return nil
}
// validateSubCategoryCode 验证二级分类编号唯一性
func (s *SubCategoryApplicationServiceImpl) validateSubCategoryCode(code, excludeID, categoryID string) error {
if code == "" {
return errors.New("二级分类编号不能为空")
}
existingSubCategory, err := s.subCategoryRepo.FindByCode(context.Background(), code)
if err == nil && existingSubCategory != nil && existingSubCategory.ID != excludeID {
// 如果指定了分类ID检查是否在同一分类下
if categoryID == "" || existingSubCategory.CategoryID == categoryID {
return errors.New("二级分类编号已存在")
}
}
return nil
}

View File

@@ -0,0 +1,35 @@
package product
import (
"context"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses"
)
// SubscriptionApplicationService 订阅应用服务接口
type SubscriptionApplicationService interface {
// 订阅管理
UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error
// 订阅管理
CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error
GetSubscriptionByID(ctx context.Context, query *queries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error)
ListSubscriptions(ctx context.Context, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
// 我的订阅(用户专用)
ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error)
CancelMySubscription(ctx context.Context, userID string, subscriptionID string) error
// 业务查询
GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
GetProductSubscriptions(ctx context.Context, query *queries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error)
// 统计
GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error)
// 一键改价
BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error
}

View File

@@ -2,12 +2,15 @@ package product
import ( import (
"context" "context"
"fmt"
"github.com/shopspring/decimal"
"go.uber.org/zap" "go.uber.org/zap"
"tyapi-server/internal/application/product/dto/commands" "tyapi-server/internal/application/product/dto/commands"
appQueries "tyapi-server/internal/application/product/dto/queries" appQueries "tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses" "tyapi-server/internal/application/product/dto/responses"
domain_api_repo "tyapi-server/internal/domains/api/repositories"
"tyapi-server/internal/domains/product/entities" "tyapi-server/internal/domains/product/entities"
repoQueries "tyapi-server/internal/domains/product/repositories/queries" repoQueries "tyapi-server/internal/domains/product/repositories/queries"
product_service "tyapi-server/internal/domains/product/services" product_service "tyapi-server/internal/domains/product/services"
@@ -19,6 +22,8 @@ import (
type SubscriptionApplicationServiceImpl struct { type SubscriptionApplicationServiceImpl struct {
productSubscriptionService *product_service.ProductSubscriptionService productSubscriptionService *product_service.ProductSubscriptionService
userRepo user_repositories.UserRepository userRepo user_repositories.UserRepository
apiCallRepository domain_api_repo.ApiCallRepository
selfSubscribePolicy SelfSubscribePolicy
logger *zap.Logger logger *zap.Logger
} }
@@ -26,11 +31,18 @@ type SubscriptionApplicationServiceImpl struct {
func NewSubscriptionApplicationService( func NewSubscriptionApplicationService(
productSubscriptionService *product_service.ProductSubscriptionService, productSubscriptionService *product_service.ProductSubscriptionService,
userRepo user_repositories.UserRepository, userRepo user_repositories.UserRepository,
apiCallRepository domain_api_repo.ApiCallRepository,
selfSubscribePolicy SelfSubscribePolicy,
logger *zap.Logger, logger *zap.Logger,
) SubscriptionApplicationService { ) SubscriptionApplicationService {
if selfSubscribePolicy == nil {
selfSubscribePolicy = DefaultAllowSelfSubscribe{}
}
return &SubscriptionApplicationServiceImpl{ return &SubscriptionApplicationServiceImpl{
productSubscriptionService: productSubscriptionService, productSubscriptionService: productSubscriptionService,
userRepo: userRepo, userRepo: userRepo,
apiCallRepository: apiCallRepository,
selfSubscribePolicy: selfSubscribePolicy,
logger: logger, logger: logger,
} }
} }
@@ -38,13 +50,130 @@ func NewSubscriptionApplicationService(
// UpdateSubscriptionPrice 更新订阅价格 // UpdateSubscriptionPrice 更新订阅价格
// 业务流程1. 获取订阅 2. 更新价格 3. 保存订阅 // 业务流程1. 获取订阅 2. 更新价格 3. 保存订阅
func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error { func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error {
return s.productSubscriptionService.UpdateSubscriptionPrice(ctx, cmd.ID, cmd.Price) return s.productSubscriptionService.UpdateSubscriptionPriceWithUIComponent(ctx, cmd.ID, cmd.Price, cmd.UIComponentPrice)
}
// BatchUpdateSubscriptionPrices 一键改价
// 业务流程1. 获取用户所有订阅 2. 根据范围筛选 3. 批量更新价格
func (s *SubscriptionApplicationServiceImpl) BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error {
// 记录请求参数
s.logger.Info("开始批量更新订阅价格",
zap.String("user_id", cmd.UserID),
zap.String("adjustment_type", cmd.AdjustmentType),
zap.Float64("discount", cmd.Discount),
zap.Float64("cost_multiple", cmd.CostMultiple),
zap.String("scope", cmd.Scope))
// 验证调整方式对应的参数
if cmd.AdjustmentType == "discount" && cmd.Discount <= 0 {
return fmt.Errorf("按售价折扣调整时折扣比例必须大于0")
}
if cmd.AdjustmentType == "cost_multiple" && cmd.CostMultiple <= 0 {
return fmt.Errorf("按成本价倍数调整时倍数必须大于0")
}
subscriptions, _, err := s.productSubscriptionService.ListSubscriptions(ctx, &repoQueries.ListSubscriptionsQuery{
UserID: cmd.UserID,
Page: 1,
PageSize: 1000,
})
if err != nil {
return err
}
s.logger.Info("获取到订阅列表",
zap.Int("total_subscriptions", len(subscriptions)))
// 根据范围筛选订阅
var targetSubscriptions []*entities.Subscription
for _, sub := range subscriptions {
if cmd.Scope == "all" {
// 所有订阅都修改
targetSubscriptions = append(targetSubscriptions, sub)
} else if cmd.Scope == "undiscounted" {
// 只修改未打折的订阅(价格等于产品原价)
if sub.Product != nil && sub.Price.Equal(sub.Product.Price) {
targetSubscriptions = append(targetSubscriptions, sub)
}
}
}
// 批量更新价格
updatedCount := 0
skippedCount := 0
for _, sub := range targetSubscriptions {
if sub.Product == nil {
skippedCount++
continue
}
var newPrice decimal.Decimal
if cmd.AdjustmentType == "discount" {
// 按售价折扣调整
discountRatio := cmd.Discount / 10
newPrice = sub.Product.Price.Mul(decimal.NewFromFloat(discountRatio))
} else if cmd.AdjustmentType == "cost_multiple" {
// 按成本价倍数调整
// 检查成本价是否有效必须大于0
// 使用严格检查成本价必须大于0
if !sub.Product.CostPrice.GreaterThan(decimal.Zero) {
// 跳过没有成本价或成本价为0的产品
skippedCount++
s.logger.Info("跳过未设置成本价或成本价为0的订阅",
zap.String("subscription_id", sub.ID),
zap.String("product_id", sub.ProductID),
zap.String("product_name", sub.Product.Name),
zap.String("cost_price", sub.Product.CostPrice.String()))
continue
}
// 计算成本价倍数后的价格
newPrice = sub.Product.CostPrice.Mul(decimal.NewFromFloat(cmd.CostMultiple))
} else {
s.logger.Warn("未知的调整方式",
zap.String("adjustment_type", cmd.AdjustmentType),
zap.String("subscription_id", sub.ID))
skippedCount++
continue
}
// 四舍五入到2位小数
newPrice = newPrice.Round(2)
err := s.productSubscriptionService.UpdateSubscriptionPrice(ctx, sub.ID, newPrice.InexactFloat64())
if err != nil {
s.logger.Error("批量更新订阅价格失败",
zap.String("subscription_id", sub.ID),
zap.Error(err))
skippedCount++
// 继续处理其他订阅,不中断整个流程
} else {
updatedCount++
}
}
s.logger.Info("批量更新订阅价格完成",
zap.Int("total", len(targetSubscriptions)),
zap.Int("updated", updatedCount),
zap.Int("skipped", skippedCount))
return nil
} }
// CreateSubscription 创建订阅 // CreateSubscription 创建订阅
// 业务流程1. 创建订阅 // 业务流程1. 创建订阅
func (s *SubscriptionApplicationServiceImpl) CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error { func (s *SubscriptionApplicationServiceImpl) CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error {
_, err := s.productSubscriptionService.CreateSubscription(ctx, cmd.UserID, cmd.ProductID) allow, msg, err := s.selfSubscribePolicy.Allow(ctx, cmd.UserID)
if err != nil {
return err
}
if !allow {
if msg == "" {
msg = "当前账号不允许自助订阅"
}
return fmt.Errorf("%s", msg)
}
_, err = s.productSubscriptionService.CreateSubscription(ctx, cmd.UserID, cmd.ProductID)
return err return err
} }
@@ -80,7 +209,7 @@ func (s *SubscriptionApplicationServiceImpl) ListSubscriptions(ctx context.Conte
} }
items := make([]responses.SubscriptionInfoResponse, len(subscriptions)) items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
for i := range subscriptions { for i := range subscriptions {
resp := s.convertToSubscriptionInfoResponse(subscriptions[i]) resp := s.convertToSubscriptionInfoResponseForAdmin(subscriptions[i])
if resp != nil { if resp != nil {
items[i] = *resp // 解引用指针 items[i] = *resp // 解引用指针
} }
@@ -153,17 +282,30 @@ func (s *SubscriptionApplicationServiceImpl) GetProductSubscriptions(ctx context
} }
// GetSubscriptionUsage 获取订阅使用情况 // GetSubscriptionUsage 获取订阅使用情况
// 业务流程1. 获取订阅使用情况 2. 构建响应数据 // 业务流程1. 获取订阅信息 2. 根据产品ID和用户ID统计API调用次数 3. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) { func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) {
// 获取订阅信息
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID) subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 根据用户ID和产品ID统计API调用次数
apiCallCount, err := s.apiCallRepository.CountByUserIdAndProductId(ctx, subscription.UserID, subscription.ProductID)
if err != nil {
s.logger.Warn("统计API调用次数失败使用订阅记录中的值",
zap.String("subscription_id", subscriptionID),
zap.String("user_id", subscription.UserID),
zap.String("product_id", subscription.ProductID),
zap.Error(err))
// 如果统计失败使用订阅实体中的APIUsed字段作为备选
apiCallCount = subscription.APIUsed
}
return &responses.SubscriptionUsageResponse{ return &responses.SubscriptionUsageResponse{
ID: subscription.ID, ID: subscription.ID,
ProductID: subscription.ProductID, ProductID: subscription.ProductID,
APIUsed: subscription.APIUsed, APIUsed: apiCallCount,
}, nil }, nil
} }
@@ -195,6 +337,38 @@ func (s *SubscriptionApplicationServiceImpl) GetMySubscriptionStats(ctx context.
}, nil }, nil
} }
// CancelMySubscription 取消我的订阅
// 业务流程1. 验证订阅是否属于当前用户 2. 取消订阅
func (s *SubscriptionApplicationServiceImpl) CancelMySubscription(ctx context.Context, userID string, subscriptionID string) error {
// 1. 获取订阅信息
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
if err != nil {
s.logger.Error("获取订阅信息失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
return fmt.Errorf("订阅不存在")
}
// 2. 验证订阅是否属于当前用户
if subscription.UserID != userID {
s.logger.Warn("用户尝试取消不属于自己的订阅",
zap.String("user_id", userID),
zap.String("subscription_id", subscriptionID),
zap.String("subscription_user_id", subscription.UserID))
return fmt.Errorf("无权取消此订阅")
}
// 3. 取消订阅(软删除)
if err := s.productSubscriptionService.CancelSubscription(ctx, subscriptionID); err != nil {
s.logger.Error("取消订阅失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
return fmt.Errorf("取消订阅失败: %w", err)
}
s.logger.Info("用户取消订阅成功",
zap.String("user_id", userID),
zap.String("subscription_id", subscriptionID))
return nil
}
// convertToSubscriptionInfoResponse 转换为订阅信息响应 // convertToSubscriptionInfoResponse 转换为订阅信息响应
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse { func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
// 查询用户信息 // 查询用户信息
@@ -219,11 +393,18 @@ func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(s
productResponse = s.convertToProductSimpleResponse(subscription.Product) productResponse = s.convertToProductSimpleResponse(subscription.Product)
} }
// 获取UI组件价格如果订阅中没有设置则从产品中获取
uiComponentPrice := subscription.UIComponentPrice.InexactFloat64()
if uiComponentPrice == 0 && subscription.Product != nil && (subscription.Product.IsPackage) {
uiComponentPrice = subscription.Product.UIComponentPrice.InexactFloat64()
}
return &responses.SubscriptionInfoResponse{ return &responses.SubscriptionInfoResponse{
ID: subscription.ID, ID: subscription.ID,
UserID: subscription.UserID, UserID: subscription.UserID,
ProductID: subscription.ProductID, ProductID: subscription.ProductID,
Price: subscription.Price.InexactFloat64(), Price: subscription.Price.InexactFloat64(),
UIComponentPrice: uiComponentPrice,
User: userInfo, User: userInfo,
Product: productResponse, Product: productResponse,
APIUsed: subscription.APIUsed, APIUsed: subscription.APIUsed,
@@ -251,6 +432,73 @@ func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(prod
} }
} }
// convertToSubscriptionInfoResponseForAdmin 转换为订阅信息响应(管理员端,包含成本价)
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponseForAdmin(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
// 查询用户信息
var userInfo *responses.UserSimpleResponse
if subscription.UserID != "" {
user, err := s.userRepo.GetByIDWithEnterpriseInfo(context.Background(), subscription.UserID)
if err == nil {
companyName := "未知公司"
if user.EnterpriseInfo != nil {
companyName = user.EnterpriseInfo.CompanyName
}
userInfo = &responses.UserSimpleResponse{
ID: user.ID,
CompanyName: companyName,
Phone: user.Phone,
}
}
}
var productAdminResponse *responses.ProductSimpleAdminResponse
if subscription.Product != nil {
productAdminResponse = s.convertToProductSimpleAdminResponse(subscription.Product)
}
// 获取UI组件价格如果订阅中没有设置则从产品中获取
uiComponentPrice := subscription.UIComponentPrice.InexactFloat64()
if uiComponentPrice == 0 && subscription.Product != nil && (subscription.Product.IsPackage) {
uiComponentPrice = subscription.Product.UIComponentPrice.InexactFloat64()
}
return &responses.SubscriptionInfoResponse{
ID: subscription.ID,
UserID: subscription.UserID,
ProductID: subscription.ProductID,
Price: subscription.Price.InexactFloat64(),
UIComponentPrice: uiComponentPrice,
User: userInfo,
ProductAdmin: productAdminResponse,
APIUsed: subscription.APIUsed,
CreatedAt: subscription.CreatedAt,
UpdatedAt: subscription.UpdatedAt,
}
}
// convertToProductSimpleAdminResponse 转换为管理员产品简单信息响应(包含成本价)
func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleAdminResponse(product *entities.Product) *responses.ProductSimpleAdminResponse {
var categoryResponse *responses.CategorySimpleResponse
if product.Category != nil {
categoryResponse = s.convertToCategorySimpleResponse(product.Category)
}
return &responses.ProductSimpleAdminResponse{
ProductSimpleResponse: responses.ProductSimpleResponse{
ID: product.ID,
OldID: product.OldID,
Name: product.Name,
Code: product.Code,
Description: product.Description,
Price: product.Price.InexactFloat64(),
Category: categoryResponse,
IsPackage: product.IsPackage,
},
CostPrice: product.CostPrice.InexactFloat64(),
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
}
}
// convertToCategorySimpleResponse 转换为分类简单信息响应 // convertToCategorySimpleResponse 转换为分类简单信息响应
func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse { func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
if category == nil { if category == nil {

View File

@@ -0,0 +1,743 @@
package product
import (
"context"
"fmt"
"io"
"mime/multipart"
"path/filepath"
"strings"
"time"
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
// UIComponentApplicationService UI组件应用服务接口
type UIComponentApplicationService interface {
// 基本CRUD操作
CreateUIComponent(ctx context.Context, req CreateUIComponentRequest) (entities.UIComponent, error)
CreateUIComponentWithFile(ctx context.Context, req CreateUIComponentRequest, file *multipart.FileHeader) (entities.UIComponent, error)
CreateUIComponentWithFiles(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader) (entities.UIComponent, error)
CreateUIComponentWithFilesAndPaths(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader, paths []string) (entities.UIComponent, error)
GetUIComponentByID(ctx context.Context, id string) (*entities.UIComponent, error)
GetUIComponentByCode(ctx context.Context, code string) (*entities.UIComponent, error)
UpdateUIComponent(ctx context.Context, req UpdateUIComponentRequest) error
DeleteUIComponent(ctx context.Context, id string) error
ListUIComponents(ctx context.Context, req ListUIComponentsRequest) (ListUIComponentsResponse, error)
// 文件操作
UploadUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) (string, error)
UploadAndExtractUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) error
DownloadUIComponentFile(ctx context.Context, id string) (string, error)
GetUIComponentFolderContent(ctx context.Context, id string) ([]FileInfo, error)
DeleteUIComponentFolder(ctx context.Context, id string) error
// 产品关联操作
AssociateUIComponentToProduct(ctx context.Context, req AssociateUIComponentRequest) error
GetProductUIComponents(ctx context.Context, productID string) ([]entities.ProductUIComponent, error)
RemoveUIComponentFromProduct(ctx context.Context, productID, componentID string) error
}
// CreateUIComponentRequest 创建UI组件请求
type CreateUIComponentRequest struct {
ComponentCode string `json:"component_code" binding:"required"`
ComponentName string `json:"component_name" binding:"required"`
Description string `json:"description"`
Version string `json:"version"`
IsActive bool `json:"is_active"`
SortOrder int `json:"sort_order"`
}
// UpdateUIComponentRequest 更新UI组件请求
type UpdateUIComponentRequest struct {
ID string `json:"id" binding:"required"`
ComponentCode string `json:"component_code"`
ComponentName string `json:"component_name"`
Description string `json:"description"`
Version string `json:"version"`
IsActive *bool `json:"is_active"`
SortOrder *int `json:"sort_order"`
}
// ListUIComponentsRequest 获取UI组件列表请求
type ListUIComponentsRequest struct {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=10"`
Keyword string `form:"keyword"`
IsActive *bool `form:"is_active"`
SortBy string `form:"sort_by,default=sort_order"`
SortOrder string `form:"sort_order,default=asc"`
}
// ListUIComponentsResponse 获取UI组件列表响应
type ListUIComponentsResponse struct {
Components []entities.UIComponent `json:"components"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// AssociateUIComponentRequest 关联UI组件到产品请求
type AssociateUIComponentRequest struct {
ProductID string `json:"product_id" binding:"required"`
UIComponentID string `json:"ui_component_id" binding:"required"`
Price float64 `json:"price" binding:"required,min=0"`
IsEnabled bool `json:"is_enabled"`
}
// UIComponentApplicationServiceImpl UI组件应用服务实现
type UIComponentApplicationServiceImpl struct {
uiComponentRepo repositories.UIComponentRepository
productUIComponentRepo repositories.ProductUIComponentRepository
fileStorageService FileStorageService
fileService UIComponentFileService
logger *zap.Logger
}
// FileStorageService 文件存储服务接口
type FileStorageService interface {
StoreFile(ctx context.Context, file io.Reader, filename string) (string, error)
GetFileURL(ctx context.Context, filePath string) (string, error)
DeleteFile(ctx context.Context, filePath string) error
}
// NewUIComponentApplicationService 创建UI组件应用服务
func NewUIComponentApplicationService(
uiComponentRepo repositories.UIComponentRepository,
productUIComponentRepo repositories.ProductUIComponentRepository,
fileStorageService FileStorageService,
fileService UIComponentFileService,
logger *zap.Logger,
) UIComponentApplicationService {
return &UIComponentApplicationServiceImpl{
uiComponentRepo: uiComponentRepo,
productUIComponentRepo: productUIComponentRepo,
fileStorageService: fileStorageService,
fileService: fileService,
logger: logger,
}
}
// CreateUIComponent 创建UI组件
func (s *UIComponentApplicationServiceImpl) CreateUIComponent(ctx context.Context, req CreateUIComponentRequest) (entities.UIComponent, error) {
// 检查编码是否已存在
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
if existing != nil {
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
}
component := entities.UIComponent{
ComponentCode: req.ComponentCode,
ComponentName: req.ComponentName,
Description: req.Description,
Version: req.Version,
IsActive: req.IsActive,
SortOrder: req.SortOrder,
}
return s.uiComponentRepo.Create(ctx, component)
}
// CreateUIComponentWithFile 创建UI组件并上传文件
func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFile(ctx context.Context, req CreateUIComponentRequest, file *multipart.FileHeader) (entities.UIComponent, error) {
// 检查编码是否已存在
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
if existing != nil {
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
}
// 创建组件
component := entities.UIComponent{
ComponentCode: req.ComponentCode,
ComponentName: req.ComponentName,
Description: req.Description,
Version: req.Version,
IsActive: req.IsActive,
SortOrder: req.SortOrder,
}
createdComponent, err := s.uiComponentRepo.Create(ctx, component)
if err != nil {
return entities.UIComponent{}, err
}
// 如果有文件,则上传并处理文件
if file != nil {
// 打开上传的文件
src, err := file.Open()
if err != nil {
// 删除已创建的组件记录
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
return entities.UIComponent{}, fmt.Errorf("打开上传文件失败: %w", err)
}
defer src.Close()
// 上传并解压文件
if err := s.fileService.UploadAndExtract(ctx, createdComponent.ID, createdComponent.ComponentCode, src, file.Filename); err != nil {
// 删除已创建的组件记录
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
return entities.UIComponent{}, err
}
// 获取文件类型
fileType := strings.ToLower(filepath.Ext(file.Filename))
// 更新组件信息
folderPath := "resources/Pure_Component/src/ui"
createdComponent.FolderPath = &folderPath
createdComponent.FileType = &fileType
// 记录文件上传时间
now := time.Now()
createdComponent.FileUploadTime = &now
// 仅对ZIP文件设置已解压标记
if fileType == ".zip" {
createdComponent.IsExtracted = true
}
// 更新组件信息
err = s.uiComponentRepo.Update(ctx, createdComponent)
if err != nil {
// 尝试删除已创建的组件记录
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
return entities.UIComponent{}, err
}
return createdComponent, nil
}
return createdComponent, nil
}
// CreateUIComponentWithFiles 创建UI组件并上传多个文件
func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFiles(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader) (entities.UIComponent, error) {
// 检查编码是否已存在
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
if existing != nil {
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
}
// 创建组件
component := entities.UIComponent{
ComponentCode: req.ComponentCode,
ComponentName: req.ComponentName,
Description: req.Description,
Version: req.Version,
IsActive: req.IsActive,
SortOrder: req.SortOrder,
}
createdComponent, err := s.uiComponentRepo.Create(ctx, component)
if err != nil {
return entities.UIComponent{}, err
}
// 如果有文件,则上传并处理文件
if len(files) > 0 {
// 处理每个文件
var extractedFiles []string
for _, fileHeader := range files {
// 打开上传的文件
src, err := fileHeader.Open()
if err != nil {
// 删除已创建的组件记录
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
return entities.UIComponent{}, fmt.Errorf("打开上传文件失败: %w", err)
}
// 上传并解压文件
if err := s.fileService.UploadAndExtract(ctx, createdComponent.ID, createdComponent.ComponentCode, src, fileHeader.Filename); err != nil {
src.Close()
// 删除已创建的组件记录
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
return entities.UIComponent{}, err
}
src.Close()
// 记录已处理的文件,用于日志
extractedFiles = append(extractedFiles, fileHeader.Filename)
}
// 更新组件信息
folderPath := "resources/Pure_Component/src/ui"
createdComponent.FolderPath = &folderPath
// 记录文件上传时间
now := time.Now()
createdComponent.FileUploadTime = &now
// 检查是否有ZIP文件
hasZipFile := false
for _, fileHeader := range files {
if strings.HasSuffix(strings.ToLower(fileHeader.Filename), ".zip") {
hasZipFile = true
break
}
}
// 如果有ZIP文件则标记为已解压
if hasZipFile {
createdComponent.IsExtracted = true
}
// 更新组件信息
err = s.uiComponentRepo.Update(ctx, createdComponent)
if err != nil {
// 尝试删除已创建的组件记录
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
return entities.UIComponent{}, err
}
}
return createdComponent, nil
}
// CreateUIComponentWithFilesAndPaths 创建UI组件并上传带路径的文件
func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFilesAndPaths(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader, paths []string) (entities.UIComponent, error) {
// 检查编码是否已存在
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
if existing != nil {
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
}
// 创建组件
component := entities.UIComponent{
ComponentCode: req.ComponentCode,
ComponentName: req.ComponentName,
Description: req.Description,
Version: req.Version,
IsActive: req.IsActive,
SortOrder: req.SortOrder,
}
createdComponent, err := s.uiComponentRepo.Create(ctx, component)
if err != nil {
return entities.UIComponent{}, err
}
// 如果有文件,则上传并处理文件
if len(files) > 0 {
// 打开所有文件
var readers []io.Reader
var filenames []string
var filePaths []string
for i, fileHeader := range files {
// 打开上传的文件
src, err := fileHeader.Open()
if err != nil {
// 关闭已打开的文件
for _, r := range readers {
if closer, ok := r.(io.Closer); ok {
closer.Close()
}
}
// 删除已创建的组件记录
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
return entities.UIComponent{}, fmt.Errorf("打开上传文件失败: %w", err)
}
readers = append(readers, src)
filenames = append(filenames, fileHeader.Filename)
// 确定文件路径
var path string
if i < len(paths) && paths[i] != "" {
path = paths[i]
} else {
path = fileHeader.Filename
}
filePaths = append(filePaths, path)
}
// 使用新的批量上传方法
if err := s.fileService.UploadMultipleFiles(ctx, createdComponent.ID, createdComponent.ComponentCode, readers, filenames, filePaths); err != nil {
// 关闭已打开的文件
for _, r := range readers {
if closer, ok := r.(io.Closer); ok {
closer.Close()
}
}
// 删除已创建的组件记录
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
return entities.UIComponent{}, err
}
// 关闭所有文件
for _, r := range readers {
if closer, ok := r.(io.Closer); ok {
closer.Close()
}
}
// 更新组件信息
folderPath := "resources/Pure_Component/src/ui"
createdComponent.FolderPath = &folderPath
// 记录文件上传时间
now := time.Now()
createdComponent.FileUploadTime = &now
// 检查是否有ZIP文件
hasZipFile := false
for _, fileHeader := range files {
if strings.HasSuffix(strings.ToLower(fileHeader.Filename), ".zip") {
hasZipFile = true
break
}
}
// 如果有ZIP文件则标记为已解压
if hasZipFile {
createdComponent.IsExtracted = true
}
// 更新组件信息
err = s.uiComponentRepo.Update(ctx, createdComponent)
if err != nil {
// 尝试删除已创建的组件记录
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
return entities.UIComponent{}, err
}
}
return createdComponent, nil
}
// GetUIComponentByID 根据ID获取UI组件
func (s *UIComponentApplicationServiceImpl) GetUIComponentByID(ctx context.Context, id string) (*entities.UIComponent, error) {
return s.uiComponentRepo.GetByID(ctx, id)
}
// GetUIComponentByCode 根据编码获取UI组件
func (s *UIComponentApplicationServiceImpl) GetUIComponentByCode(ctx context.Context, code string) (*entities.UIComponent, error) {
return s.uiComponentRepo.GetByCode(ctx, code)
}
// UpdateUIComponent 更新UI组件
func (s *UIComponentApplicationServiceImpl) UpdateUIComponent(ctx context.Context, req UpdateUIComponentRequest) error {
component, err := s.uiComponentRepo.GetByID(ctx, req.ID)
if err != nil {
return err
}
if component == nil {
return ErrComponentNotFound
}
// 如果更新编码,检查是否与其他组件冲突
if req.ComponentCode != "" && req.ComponentCode != component.ComponentCode {
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
if existing != nil && existing.ID != req.ID {
return ErrComponentCodeAlreadyExists
}
component.ComponentCode = req.ComponentCode
}
if req.ComponentName != "" {
component.ComponentName = req.ComponentName
}
if req.Description != "" {
component.Description = req.Description
}
if req.Version != "" {
component.Version = req.Version
}
if req.IsActive != nil {
component.IsActive = *req.IsActive
}
if req.SortOrder != nil {
component.SortOrder = *req.SortOrder
}
return s.uiComponentRepo.Update(ctx, *component)
}
// DeleteUIComponent 删除UI组件
func (s *UIComponentApplicationServiceImpl) DeleteUIComponent(ctx context.Context, id string) error {
// 获取组件信息
component, err := s.uiComponentRepo.GetByID(ctx, id)
if err != nil {
s.logger.Error("获取UI组件失败", zap.Error(err), zap.String("id", id))
return fmt.Errorf("获取UI组件失败: %w", err)
}
if component == nil {
s.logger.Warn("UI组件不存在", zap.String("id", id))
return ErrComponentNotFound
}
// 记录组件信息
s.logger.Info("开始删除UI组件",
zap.String("id", id),
zap.String("componentCode", component.ComponentCode),
zap.String("componentName", component.ComponentName),
zap.Bool("isExtracted", component.IsExtracted),
zap.Any("filePath", component.FilePath),
zap.Any("folderPath", component.FolderPath))
// 使用智能删除方法,根据组件编码和上传时间删除相关文件
if err := s.fileService.DeleteFilesByComponentCode(component.ComponentCode, component.FileUploadTime); err != nil {
// 记录错误但不阻止删除数据库记录
s.logger.Error("删除组件文件失败",
zap.Error(err),
zap.String("componentCode", component.ComponentCode),
zap.Any("fileUploadTime", component.FileUploadTime))
}
// 删除关联的文件(FilePath指向的文件)
if component.FilePath != nil {
if err := s.fileStorageService.DeleteFile(ctx, *component.FilePath); err != nil {
s.logger.Error("删除文件失败",
zap.Error(err),
zap.String("filePath", *component.FilePath))
}
}
// 删除数据库记录
if err := s.uiComponentRepo.Delete(ctx, id); err != nil {
s.logger.Error("删除UI组件数据库记录失败",
zap.Error(err),
zap.String("id", id))
return fmt.Errorf("删除UI组件数据库记录失败: %w", err)
}
s.logger.Info("UI组件删除成功",
zap.String("id", id),
zap.String("componentCode", component.ComponentCode))
return nil
}
// ListUIComponents 获取UI组件列表
func (s *UIComponentApplicationServiceImpl) ListUIComponents(ctx context.Context, req ListUIComponentsRequest) (ListUIComponentsResponse, error) {
filters := make(map[string]interface{})
if req.Keyword != "" {
filters["keyword"] = req.Keyword
}
if req.IsActive != nil {
filters["is_active"] = *req.IsActive
}
filters["page"] = req.Page
filters["page_size"] = req.PageSize
filters["sort_by"] = req.SortBy
filters["sort_order"] = req.SortOrder
components, total, err := s.uiComponentRepo.List(ctx, filters)
if err != nil {
return ListUIComponentsResponse{}, err
}
return ListUIComponentsResponse{
Components: components,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// UploadUIComponentFile 上传UI组件文件
func (s *UIComponentApplicationServiceImpl) UploadUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) (string, error) {
component, err := s.uiComponentRepo.GetByID(ctx, id)
if err != nil {
return "", err
}
if component == nil {
return "", ErrComponentNotFound
}
// 检查文件大小100MB
if file.Size > 100*1024*1024 {
return "", ErrInvalidFileType // 复用此错误表示文件太大
}
// 打开上传的文件
src, err := file.Open()
if err != nil {
return "", err
}
defer src.Close()
// 生成文件路径
filePath := filepath.Join("ui-components", id+"_"+file.Filename)
// 存储文件
storedPath, err := s.fileStorageService.StoreFile(ctx, src, filePath)
if err != nil {
return "", err
}
// 删除旧文件
if component.FilePath != nil {
_ = s.fileStorageService.DeleteFile(ctx, *component.FilePath)
}
// 获取文件类型
fileType := strings.ToLower(filepath.Ext(file.Filename))
// 更新组件信息
component.FilePath = &storedPath
component.FileSize = &file.Size
component.FileType = &fileType
if err := s.uiComponentRepo.Update(ctx, *component); err != nil {
// 如果更新失败,尝试删除已上传的文件
_ = s.fileStorageService.DeleteFile(ctx, storedPath)
return "", err
}
return storedPath, nil
}
// DownloadUIComponentFile 下载UI组件文件
func (s *UIComponentApplicationServiceImpl) DownloadUIComponentFile(ctx context.Context, id string) (string, error) {
component, err := s.uiComponentRepo.GetByID(ctx, id)
if err != nil {
return "", err
}
if component == nil {
return "", ErrComponentNotFound
}
if component.FilePath == nil {
return "", ErrComponentFileNotFound
}
return s.fileStorageService.GetFileURL(ctx, *component.FilePath)
}
// AssociateUIComponentToProduct 关联UI组件到产品
func (s *UIComponentApplicationServiceImpl) AssociateUIComponentToProduct(ctx context.Context, req AssociateUIComponentRequest) error {
// 检查组件是否存在
component, err := s.uiComponentRepo.GetByID(ctx, req.UIComponentID)
if err != nil {
return err
}
if component == nil {
return ErrComponentNotFound
}
// 创建关联
relation := entities.ProductUIComponent{
ProductID: req.ProductID,
UIComponentID: req.UIComponentID,
Price: decimal.NewFromFloat(req.Price),
IsEnabled: req.IsEnabled,
}
_, err = s.productUIComponentRepo.Create(ctx, relation)
return err
}
// GetProductUIComponents 获取产品的UI组件列表
func (s *UIComponentApplicationServiceImpl) GetProductUIComponents(ctx context.Context, productID string) ([]entities.ProductUIComponent, error) {
return s.productUIComponentRepo.GetByProductID(ctx, productID)
}
// RemoveUIComponentFromProduct 从产品中移除UI组件
func (s *UIComponentApplicationServiceImpl) RemoveUIComponentFromProduct(ctx context.Context, productID, componentID string) error {
// 查找关联记录
relations, err := s.productUIComponentRepo.GetByProductID(ctx, productID)
if err != nil {
return err
}
// 找到要删除的关联记录
var relationID string
for _, relation := range relations {
if relation.UIComponentID == componentID {
relationID = relation.ID
break
}
}
if relationID == "" {
return ErrProductComponentRelationNotFound
}
return s.productUIComponentRepo.Delete(ctx, relationID)
}
// UploadAndExtractUIComponentFile 上传并解压UI组件文件
func (s *UIComponentApplicationServiceImpl) UploadAndExtractUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) error {
// 获取组件信息
component, err := s.uiComponentRepo.GetByID(ctx, id)
if err != nil {
return err
}
if component == nil {
return ErrComponentNotFound
}
// 打开上传的文件
src, err := file.Open()
if err != nil {
return fmt.Errorf("打开上传文件失败: %w", err)
}
defer src.Close()
// 上传并解压文件
if err := s.fileService.UploadAndExtract(ctx, id, component.ComponentCode, src, file.Filename); err != nil {
return err
}
// 获取文件类型
fileType := strings.ToLower(filepath.Ext(file.Filename))
// 更新组件信息
folderPath := "resources/Pure_Component/src/ui"
component.FolderPath = &folderPath
component.FileType = &fileType
// 记录文件上传时间
now := time.Now()
component.FileUploadTime = &now
// 仅对ZIP文件设置已解压标记
if fileType == ".zip" {
component.IsExtracted = true
}
return s.uiComponentRepo.Update(ctx, *component)
}
// GetUIComponentFolderContent 获取UI组件文件夹内容
func (s *UIComponentApplicationServiceImpl) GetUIComponentFolderContent(ctx context.Context, id string) ([]FileInfo, error) {
// 获取组件信息
component, err := s.uiComponentRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if component == nil {
return nil, ErrComponentNotFound
}
// 如果没有文件夹路径,返回空
if component.FolderPath == nil {
return []FileInfo{}, nil
}
// 获取文件夹内容
return s.fileService.GetFolderContent(*component.FolderPath)
}
// DeleteUIComponentFolder 删除UI组件文件夹
func (s *UIComponentApplicationServiceImpl) DeleteUIComponentFolder(ctx context.Context, id string) error {
// 获取组件信息
component, err := s.uiComponentRepo.GetByID(ctx, id)
if err != nil {
return err
}
if component == nil {
return ErrComponentNotFound
}
// 注意我们不再删除整个UI目录因为所有组件共享同一个目录
// 这里只更新组件信息,标记为未上传状态
// 更新组件信息
component.FolderPath = nil
component.IsExtracted = false
return s.uiComponentRepo.Update(ctx, *component)
}

View File

@@ -0,0 +1,21 @@
package product
import "errors"
// UI组件相关错误定义
var (
// ErrComponentNotFound UI组件不存在
ErrComponentNotFound = errors.New("UI组件不存在")
// ErrComponentCodeAlreadyExists UI组件编码已存在
ErrComponentCodeAlreadyExists = errors.New("UI组件编码已存在")
// ErrComponentFileNotFound UI组件文件不存在
ErrComponentFileNotFound = errors.New("UI组件文件不存在")
// ErrInvalidFileType 无效的文件类型
ErrInvalidFileType = errors.New("无效的文件类型仅支持ZIP文件")
// ErrProductComponentRelationNotFound 产品UI组件关联不存在
ErrProductComponentRelationNotFound = errors.New("产品UI组件关联不存在")
)

View File

@@ -0,0 +1,459 @@
package product
import (
"archive/zip"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"go.uber.org/zap"
)
// UIComponentFileService UI组件文件服务接口
type UIComponentFileService interface {
// 上传并解压UI组件文件
UploadAndExtract(ctx context.Context, componentID, componentCode string, file io.Reader, filename string) error
// 批量上传UI组件文件支持文件夹结构
UploadMultipleFiles(ctx context.Context, componentID, componentCode string, files []io.Reader, filenames []string, paths []string) error
// 根据组件编码创建文件夹
CreateFolderByCode(componentCode string) (string, error)
// 删除组件文件夹
DeleteFolder(folderPath string) error
// 检查文件夹是否存在
FolderExists(folderPath string) bool
// 获取文件夹内容
GetFolderContent(folderPath string) ([]FileInfo, error)
// 根据组件编码和上传时间智能删除组件相关文件
DeleteFilesByComponentCode(componentCode string, uploadTime *time.Time) error
}
// FileInfo 文件信息
type FileInfo struct {
Name string `json:"name"`
Path string `json:"path"`
Size int64 `json:"size"`
Type string `json:"type"` // "file" or "folder"
Modified time.Time `json:"modified"`
}
// UIComponentFileServiceImpl UI组件文件服务实现
type UIComponentFileServiceImpl struct {
basePath string
logger *zap.Logger
}
// NewUIComponentFileService 创建UI组件文件服务
func NewUIComponentFileService(basePath string, logger *zap.Logger) UIComponentFileService {
// 确保基础路径存在
if err := os.MkdirAll(basePath, 0755); err != nil {
logger.Error("创建基础存储目录失败", zap.Error(err), zap.String("path", basePath))
}
return &UIComponentFileServiceImpl{
basePath: basePath,
logger: logger,
}
}
// UploadAndExtract 上传并解压UI组件文件
func (s *UIComponentFileServiceImpl) UploadAndExtract(ctx context.Context, componentID, componentCode string, file io.Reader, filename string) error {
// 直接使用基础路径作为文件夹路径,不再创建组件编码子文件夹
folderPath := s.basePath
// 确保基础目录存在
if err := os.MkdirAll(folderPath, 0755); err != nil {
return fmt.Errorf("创建基础目录失败: %w", err)
}
// 保存上传的文件
filePath := filepath.Join(folderPath, filename)
savedFile, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("创建文件失败: %w", err)
}
defer savedFile.Close()
// 复制文件内容
if _, err := io.Copy(savedFile, file); err != nil {
// 删除部分写入的文件
_ = os.Remove(filePath)
return fmt.Errorf("保存文件失败: %w", err)
}
// 仅对ZIP文件执行解压逻辑
if strings.HasSuffix(strings.ToLower(filename), ".zip") {
// 解压文件到基础目录
if err := s.extractZipFile(filePath, folderPath); err != nil {
// 删除ZIP文件
_ = os.Remove(filePath)
return fmt.Errorf("解压文件失败: %w", err)
}
// 删除ZIP文件
_ = os.Remove(filePath)
s.logger.Info("UI组件文件上传并解压成功",
zap.String("componentID", componentID),
zap.String("componentCode", componentCode),
zap.String("folderPath", folderPath))
} else {
s.logger.Info("UI组件文件上传成功未解压",
zap.String("componentID", componentID),
zap.String("componentCode", componentCode),
zap.String("filePath", filePath))
}
return nil
}
// UploadMultipleFiles 批量上传UI组件文件支持文件夹结构
func (s *UIComponentFileServiceImpl) UploadMultipleFiles(ctx context.Context, componentID, componentCode string, files []io.Reader, filenames []string, paths []string) error {
// 直接使用基础路径作为文件夹路径,不再创建组件编码子文件夹
folderPath := s.basePath
// 确保基础目录存在
if err := os.MkdirAll(folderPath, 0755); err != nil {
return fmt.Errorf("创建基础目录失败: %w", err)
}
// 处理每个文件
for i, file := range files {
filename := filenames[i]
path := paths[i]
// 如果有路径信息,创建对应的子文件夹
if path != "" && path != filename {
// 获取文件所在目录
dir := filepath.Dir(path)
if dir != "." {
// 创建子文件夹
subDirPath := filepath.Join(folderPath, dir)
if err := os.MkdirAll(subDirPath, 0755); err != nil {
return fmt.Errorf("创建子文件夹失败: %w", err)
}
}
}
// 确定文件保存路径
var filePath string
if path != "" && path != filename {
// 有路径信息,使用完整路径
filePath = filepath.Join(folderPath, path)
} else {
// 没有路径信息,直接保存在根目录
filePath = filepath.Join(folderPath, filename)
}
// 保存上传的文件
savedFile, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("创建文件失败: %w", err)
}
defer savedFile.Close()
// 复制文件内容
if _, err := io.Copy(savedFile, file); err != nil {
// 删除部分写入的文件
_ = os.Remove(filePath)
return fmt.Errorf("保存文件失败: %w", err)
}
// 对ZIP文件执行解压逻辑
if strings.HasSuffix(strings.ToLower(filename), ".zip") {
// 确定解压目录
var extractDir string
if path != "" && path != filename {
// 有路径信息,解压到对应目录
dir := filepath.Dir(path)
if dir != "." {
extractDir = filepath.Join(folderPath, dir)
} else {
extractDir = folderPath
}
} else {
// 没有路径信息,解压到根目录
extractDir = folderPath
}
// 解压文件
if err := s.extractZipFile(filePath, extractDir); err != nil {
// 删除ZIP文件
_ = os.Remove(filePath)
return fmt.Errorf("解压文件失败: %w", err)
}
// 删除ZIP文件
_ = os.Remove(filePath)
s.logger.Info("UI组件文件上传并解压成功",
zap.String("componentID", componentID),
zap.String("componentCode", componentCode),
zap.String("filePath", filePath),
zap.String("extractDir", extractDir))
} else {
s.logger.Info("UI组件文件上传成功未解压",
zap.String("componentID", componentID),
zap.String("componentCode", componentCode),
zap.String("filePath", filePath))
}
}
return nil
}
// CreateFolderByCode 根据组件编码创建文件夹
func (s *UIComponentFileServiceImpl) CreateFolderByCode(componentCode string) (string, error) {
folderPath := filepath.Join(s.basePath, componentCode)
// 创建文件夹(如果不存在)
if err := os.MkdirAll(folderPath, 0755); err != nil {
return "", fmt.Errorf("创建文件夹失败: %w", err)
}
return folderPath, nil
}
// DeleteFolder 删除组件文件夹
func (s *UIComponentFileServiceImpl) DeleteFolder(folderPath string) error {
// 记录尝试删除的文件夹路径
s.logger.Info("尝试删除文件夹", zap.String("folderPath", folderPath))
// 获取文件夹信息,用于调试
if info, err := os.Stat(folderPath); err == nil {
s.logger.Info("文件夹信息",
zap.String("folderPath", folderPath),
zap.Bool("isDir", info.IsDir()),
zap.Int64("size", info.Size()),
zap.Time("modTime", info.ModTime()))
} else {
s.logger.Error("获取文件夹信息失败",
zap.Error(err),
zap.String("folderPath", folderPath))
}
// 检查文件夹是否存在
if !s.FolderExists(folderPath) {
s.logger.Info("文件夹不存在", zap.String("folderPath", folderPath))
return nil // 文件夹不存在,不视为错误
}
// 尝试删除文件夹
s.logger.Info("开始删除文件夹", zap.String("folderPath", folderPath))
if err := os.RemoveAll(folderPath); err != nil {
s.logger.Error("删除文件夹失败",
zap.Error(err),
zap.String("folderPath", folderPath))
return fmt.Errorf("删除文件夹失败: %w", err)
}
s.logger.Info("删除组件文件夹成功", zap.String("folderPath", folderPath))
return nil
}
// FolderExists 检查文件夹是否存在
func (s *UIComponentFileServiceImpl) FolderExists(folderPath string) bool {
info, err := os.Stat(folderPath)
if err != nil {
return false
}
return info.IsDir()
}
// GetFolderContent 获取文件夹内容
func (s *UIComponentFileServiceImpl) GetFolderContent(folderPath string) ([]FileInfo, error) {
var files []FileInfo
err := filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 跳过根目录
if path == folderPath {
return nil
}
// 获取相对路径
relPath, err := filepath.Rel(folderPath, path)
if err != nil {
return err
}
fileType := "file"
if info.IsDir() {
fileType = "folder"
}
files = append(files, FileInfo{
Name: info.Name(),
Path: relPath,
Size: info.Size(),
Type: fileType,
Modified: info.ModTime(),
})
return nil
})
if err != nil {
return nil, fmt.Errorf("扫描文件夹失败: %w", err)
}
return files, nil
}
// extractZipFile 解压ZIP文件
func (s *UIComponentFileServiceImpl) extractZipFile(zipPath, destPath string) error {
reader, err := zip.OpenReader(zipPath)
if err != nil {
return fmt.Errorf("打开ZIP文件失败: %w", err)
}
defer reader.Close()
for _, file := range reader.File {
path := filepath.Join(destPath, file.Name)
// 防止路径遍历攻击
if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(destPath)+string(os.PathSeparator)) {
return fmt.Errorf("无效的文件路径: %s", file.Name)
}
if file.FileInfo().IsDir() {
// 创建目录
if err := os.MkdirAll(path, file.Mode()); err != nil {
return fmt.Errorf("创建目录失败: %w", err)
}
continue
}
// 创建文件
fileReader, err := file.Open()
if err != nil {
return fmt.Errorf("打开ZIP内文件失败: %w", err)
}
// 确保父目录存在
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
fileReader.Close()
return fmt.Errorf("创建父目录失败: %w", err)
}
destFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
fileReader.Close()
return fmt.Errorf("创建目标文件失败: %w", err)
}
_, err = io.Copy(destFile, fileReader)
fileReader.Close()
destFile.Close()
if err != nil {
return fmt.Errorf("写入文件失败: %w", err)
}
}
return nil
}
// DeleteFilesByComponentCode 根据组件编码和上传时间智能删除组件相关文件
func (s *UIComponentFileServiceImpl) DeleteFilesByComponentCode(componentCode string, uploadTime *time.Time) error {
// 记录基础路径和组件编码
s.logger.Info("开始删除组件文件",
zap.String("basePath", s.basePath),
zap.String("componentCode", componentCode),
zap.Any("uploadTime", uploadTime))
// 1. 查找名为组件编码的文件夹
componentDir := filepath.Join(s.basePath, componentCode)
s.logger.Info("检查组件文件夹", zap.String("componentDir", componentDir))
if s.FolderExists(componentDir) {
s.logger.Info("找到组件文件夹,开始删除", zap.String("componentDir", componentDir))
if err := s.DeleteFolder(componentDir); err != nil {
s.logger.Error("删除组件文件夹失败",
zap.Error(err),
zap.String("componentCode", componentCode),
zap.String("componentDir", componentDir))
return fmt.Errorf("删除组件文件夹失败: %w", err)
}
s.logger.Info("成功删除组件文件夹", zap.String("componentCode", componentCode))
return nil
} else {
s.logger.Info("组件文件夹不存在", zap.String("componentDir", componentDir))
}
// 2. 查找文件名包含组件编码的文件
pattern := filepath.Join(s.basePath, "*"+componentCode+"*")
s.logger.Info("查找匹配文件", zap.String("pattern", pattern))
files, err := filepath.Glob(pattern)
if err != nil {
s.logger.Error("查找组件文件失败",
zap.Error(err),
zap.String("pattern", pattern))
return fmt.Errorf("查找组件文件失败: %w", err)
}
s.logger.Info("找到匹配文件",
zap.Strings("files", files),
zap.Int("count", len(files)))
// 3. 如果没有上传时间,删除所有匹配的文件
if uploadTime == nil {
for _, file := range files {
if err := os.Remove(file); err != nil {
s.logger.Error("删除文件失败", zap.String("file", file), zap.Error(err))
} else {
s.logger.Info("成功删除文件", zap.String("file", file))
}
}
return nil
}
// 4. 如果有上传时间,根据文件修改时间和上传时间的匹配度来删除文件
var deletedFiles []string
for _, file := range files {
// 获取文件信息
fileInfo, err := os.Stat(file)
if err != nil {
s.logger.Warn("获取文件信息失败", zap.String("file", file), zap.Error(err))
continue
}
// 计算文件修改时间与上传时间的差异(以秒为单位)
timeDiff := fileInfo.ModTime().Sub(*uploadTime).Seconds()
// 如果时间差在60秒内认为是最匹配的文件
if timeDiff < 60 && timeDiff > -60 {
if err := os.Remove(file); err != nil {
s.logger.Warn("删除文件失败", zap.String("file", file), zap.Error(err))
} else {
deletedFiles = append(deletedFiles, file)
s.logger.Info("成功删除文件", zap.String("file", file),
zap.Time("uploadTime", *uploadTime),
zap.Time("fileModTime", fileInfo.ModTime()))
}
}
}
// 如果没有找到匹配的文件,记录警告但返回成功
if len(deletedFiles) == 0 && len(files) > 0 {
s.logger.Warn("没有找到匹配时间戳的文件",
zap.String("componentCode", componentCode),
zap.Time("uploadTime", *uploadTime),
zap.Int("foundFiles", len(files)))
}
return nil
}

View File

@@ -0,0 +1,412 @@
package statistics
import (
"fmt"
"time"
)
// ================ 命令对象 ================
// CreateMetricCommand 创建指标命令
type CreateMetricCommand struct {
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" validate:"required" comment:"指标名称"`
Dimension string `json:"dimension" comment:"统计维度"`
Value float64 `json:"value" validate:"min=0" comment:"指标值"`
Metadata string `json:"metadata" comment:"额外维度信息"`
Date time.Time `json:"date" validate:"required" comment:"统计日期"`
}
// UpdateMetricCommand 更新指标命令
type UpdateMetricCommand struct {
ID string `json:"id" validate:"required" comment:"指标ID"`
Value float64 `json:"value" validate:"min=0" comment:"新指标值"`
}
// DeleteMetricCommand 删除指标命令
type DeleteMetricCommand struct {
ID string `json:"id" validate:"required" comment:"指标ID"`
}
// GenerateReportCommand 生成报告命令
type GenerateReportCommand struct {
ReportType string `json:"report_type" validate:"required" comment:"报告类型"`
Title string `json:"title" validate:"required" comment:"报告标题"`
Period string `json:"period" validate:"required" comment:"统计周期"`
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
StartDate time.Time `json:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" comment:"结束日期"`
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
GeneratedBy string `json:"generated_by" validate:"required" comment:"生成者ID"`
}
// CreateDashboardCommand 创建仪表板命令
type CreateDashboardCommand struct {
Name string `json:"name" validate:"required" comment:"仪表板名称"`
Description string `json:"description" comment:"仪表板描述"`
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
Layout string `json:"layout" comment:"布局配置"`
Widgets string `json:"widgets" comment:"组件配置"`
Settings string `json:"settings" comment:"设置配置"`
RefreshInterval int `json:"refresh_interval" validate:"min=30" comment:"刷新间隔(秒)"`
AccessLevel string `json:"access_level" comment:"访问级别"`
CreatedBy string `json:"created_by" validate:"required" comment:"创建者ID"`
}
// UpdateDashboardCommand 更新仪表板命令
type UpdateDashboardCommand struct {
ID string `json:"id" validate:"required" comment:"仪表板ID"`
Name string `json:"name" comment:"仪表板名称"`
Description string `json:"description" comment:"仪表板描述"`
Layout string `json:"layout" comment:"布局配置"`
Widgets string `json:"widgets" comment:"组件配置"`
Settings string `json:"settings" comment:"设置配置"`
RefreshInterval int `json:"refresh_interval" validate:"min=30" comment:"刷新间隔(秒)"`
AccessLevel string `json:"access_level" comment:"访问级别"`
UpdatedBy string `json:"updated_by" validate:"required" comment:"更新者ID"`
}
// SetDefaultDashboardCommand 设置默认仪表板命令
type SetDefaultDashboardCommand struct {
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
UpdatedBy string `json:"updated_by" validate:"required" comment:"更新者ID"`
}
// ActivateDashboardCommand 激活仪表板命令
type ActivateDashboardCommand struct {
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
ActivatedBy string `json:"activated_by" validate:"required" comment:"激活者ID"`
}
// DeactivateDashboardCommand 停用仪表板命令
type DeactivateDashboardCommand struct {
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
DeactivatedBy string `json:"deactivated_by" validate:"required" comment:"停用者ID"`
}
// DeleteDashboardCommand 删除仪表板命令
type DeleteDashboardCommand struct {
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
DeletedBy string `json:"deleted_by" validate:"required" comment:"删除者ID"`
}
// ExportDataCommand 导出数据命令
type ExportDataCommand struct {
Format string `json:"format" validate:"required" comment:"导出格式"`
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
StartDate time.Time `json:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" validate:"required" comment:"结束日期"`
Dimension string `json:"dimension" comment:"统计维度"`
GroupBy string `json:"group_by" comment:"分组维度"`
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
Columns []string `json:"columns" comment:"导出列"`
IncludeCharts bool `json:"include_charts" comment:"是否包含图表"`
ExportedBy string `json:"exported_by" validate:"required" comment:"导出者ID"`
}
// TriggerAggregationCommand 触发数据聚合命令
type TriggerAggregationCommand struct {
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
Period string `json:"period" validate:"required" comment:"聚合周期"`
StartDate time.Time `json:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" comment:"结束日期"`
Force bool `json:"force" comment:"是否强制重新聚合"`
TriggeredBy string `json:"triggered_by" validate:"required" comment:"触发者ID"`
}
// Validate 验证触发聚合命令
func (c *TriggerAggregationCommand) Validate() error {
if c.MetricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if c.Period == "" {
return fmt.Errorf("聚合周期不能为空")
}
if c.TriggeredBy == "" {
return fmt.Errorf("触发者ID不能为空")
}
// 验证周期类型
validPeriods := []string{"hourly", "daily", "weekly", "monthly"}
isValidPeriod := false
for _, period := range validPeriods {
if c.Period == period {
isValidPeriod = true
break
}
}
if !isValidPeriod {
return fmt.Errorf("不支持的聚合周期: %s", c.Period)
}
return nil
}
// ================ 查询对象 ================
// GetMetricsQuery 获取指标查询
type GetMetricsQuery struct {
MetricType string `json:"metric_type" form:"metric_type" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
Limit int `json:"limit" form:"limit" comment:"限制数量"`
Offset int `json:"offset" form:"offset" comment:"偏移量"`
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
}
// GetRealtimeMetricsQuery 获取实时指标查询
type GetRealtimeMetricsQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
TimeRange string `json:"time_range" form:"time_range" comment:"时间范围"`
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
}
// GetHistoricalMetricsQuery 获取历史指标查询
type GetHistoricalMetricsQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
Period string `json:"period" form:"period" comment:"统计周期"`
Limit int `json:"limit" form:"limit" comment:"限制数量"`
Offset int `json:"offset" form:"offset" comment:"偏移量"`
AggregateBy string `json:"aggregate_by" form:"aggregate_by" comment:"聚合维度"`
GroupBy string `json:"group_by" form:"group_by" comment:"分组维度"`
}
// GetDashboardDataQuery 获取仪表板数据查询
type GetDashboardDataQuery struct {
UserRole string `json:"user_role" form:"user_role" validate:"required" comment:"用户角色"`
Period string `json:"period" form:"period" comment:"统计周期"`
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
MetricTypes []string `json:"metric_types" form:"metric_types" comment:"指标类型列表"`
Dimensions []string `json:"dimensions" form:"dimensions" comment:"统计维度列表"`
}
// GetReportsQuery 获取报告查询
type GetReportsQuery struct {
ReportType string `json:"report_type" form:"report_type" comment:"报告类型"`
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
Status string `json:"status" form:"status" comment:"报告状态"`
Period string `json:"period" form:"period" comment:"统计周期"`
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
Limit int `json:"limit" form:"limit" comment:"限制数量"`
Offset int `json:"offset" form:"offset" comment:"偏移量"`
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
GeneratedBy string `json:"generated_by" form:"generated_by" comment:"生成者ID"`
}
// GetDashboardsQuery 获取仪表板查询
type GetDashboardsQuery struct {
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
IsDefault *bool `json:"is_default" form:"is_default" comment:"是否默认"`
IsActive *bool `json:"is_active" form:"is_active" comment:"是否激活"`
AccessLevel string `json:"access_level" form:"access_level" comment:"访问级别"`
CreatedBy string `json:"created_by" form:"created_by" comment:"创建者ID"`
Name string `json:"name" form:"name" comment:"仪表板名称"`
Limit int `json:"limit" form:"limit" comment:"限制数量"`
Offset int `json:"offset" form:"offset" comment:"偏移量"`
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
}
// GetReportQuery 获取单个报告查询
type GetReportQuery struct {
ReportID string `json:"report_id" form:"report_id" validate:"required" comment:"报告ID"`
}
// GetDashboardQuery 获取单个仪表板查询
type GetDashboardQuery struct {
DashboardID string `json:"dashboard_id" form:"dashboard_id" validate:"required" comment:"仪表板ID"`
}
// GetMetricQuery 获取单个指标查询
type GetMetricQuery struct {
MetricID string `json:"metric_id" form:"metric_id" validate:"required" comment:"指标ID"`
}
// CalculateGrowthRateQuery 计算增长率查询
type CalculateGrowthRateQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
CurrentPeriod time.Time `json:"current_period" form:"current_period" validate:"required" comment:"当前周期"`
PreviousPeriod time.Time `json:"previous_period" form:"previous_period" validate:"required" comment:"上一周期"`
}
// CalculateTrendQuery 计算趋势查询
type CalculateTrendQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
}
// CalculateCorrelationQuery 计算相关性查询
type CalculateCorrelationQuery struct {
MetricType1 string `json:"metric_type1" form:"metric_type1" validate:"required" comment:"指标类型1"`
MetricName1 string `json:"metric_name1" form:"metric_name1" validate:"required" comment:"指标名称1"`
MetricType2 string `json:"metric_type2" form:"metric_type2" validate:"required" comment:"指标类型2"`
MetricName2 string `json:"metric_name2" form:"metric_name2" validate:"required" comment:"指标名称2"`
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
}
// CalculateMovingAverageQuery 计算移动平均查询
type CalculateMovingAverageQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
WindowSize int `json:"window_size" form:"window_size" validate:"min=1" comment:"窗口大小"`
}
// CalculateSeasonalityQuery 计算季节性查询
type CalculateSeasonalityQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
}
// ================ 响应对象 ================
// CommandResponse 命令响应
type CommandResponse struct {
Success bool `json:"success" comment:"是否成功"`
Message string `json:"message" comment:"响应消息"`
Data interface{} `json:"data" comment:"响应数据"`
Error string `json:"error,omitempty" comment:"错误信息"`
}
// QueryResponse 查询响应
type QueryResponse struct {
Success bool `json:"success" comment:"是否成功"`
Message string `json:"message" comment:"响应消息"`
Data interface{} `json:"data" comment:"响应数据"`
Meta map[string]interface{} `json:"meta" comment:"元数据"`
Error string `json:"error,omitempty" comment:"错误信息"`
}
// ListResponse 列表响应
type ListResponse struct {
Success bool `json:"success" comment:"是否成功"`
Message string `json:"message" comment:"响应消息"`
Data ListDataDTO `json:"data" comment:"数据列表"`
Meta map[string]interface{} `json:"meta" comment:"元数据"`
Error string `json:"error,omitempty" comment:"错误信息"`
}
// ListDataDTO 列表数据DTO
type ListDataDTO struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []interface{} `json:"items" comment:"数据列表"`
}
// ================ 验证方法 ================
// Validate 验证创建指标命令
func (c *CreateMetricCommand) Validate() error {
if c.MetricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if c.MetricName == "" {
return fmt.Errorf("指标名称不能为空")
}
if c.Value < 0 {
return fmt.Errorf("指标值不能为负数")
}
if c.Date.IsZero() {
return fmt.Errorf("统计日期不能为空")
}
return nil
}
// Validate 验证更新指标命令
func (c *UpdateMetricCommand) Validate() error {
if c.ID == "" {
return fmt.Errorf("指标ID不能为空")
}
if c.Value < 0 {
return fmt.Errorf("指标值不能为负数")
}
return nil
}
// Validate 验证生成报告命令
func (c *GenerateReportCommand) Validate() error {
if c.ReportType == "" {
return fmt.Errorf("报告类型不能为空")
}
if c.Title == "" {
return fmt.Errorf("报告标题不能为空")
}
if c.Period == "" {
return fmt.Errorf("统计周期不能为空")
}
if c.UserRole == "" {
return fmt.Errorf("用户角色不能为空")
}
if c.GeneratedBy == "" {
return fmt.Errorf("生成者ID不能为空")
}
return nil
}
// Validate 验证创建仪表板命令
func (c *CreateDashboardCommand) Validate() error {
if c.Name == "" {
return fmt.Errorf("仪表板名称不能为空")
}
if c.UserRole == "" {
return fmt.Errorf("用户角色不能为空")
}
if c.CreatedBy == "" {
return fmt.Errorf("创建者ID不能为空")
}
if c.RefreshInterval < 30 {
return fmt.Errorf("刷新间隔不能少于30秒")
}
return nil
}
// Validate 验证更新仪表板命令
func (c *UpdateDashboardCommand) Validate() error {
if c.ID == "" {
return fmt.Errorf("仪表板ID不能为空")
}
if c.UpdatedBy == "" {
return fmt.Errorf("更新者ID不能为空")
}
if c.RefreshInterval < 30 {
return fmt.Errorf("刷新间隔不能少于30秒")
}
return nil
}
// Validate 验证导出数据命令
func (c *ExportDataCommand) Validate() error {
if c.Format == "" {
return fmt.Errorf("导出格式不能为空")
}
if c.MetricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if c.StartDate.IsZero() || c.EndDate.IsZero() {
return fmt.Errorf("开始日期和结束日期不能为空")
}
if c.StartDate.After(c.EndDate) {
return fmt.Errorf("开始日期不能晚于结束日期")
}
if c.ExportedBy == "" {
return fmt.Errorf("导出者ID不能为空")
}
return nil
}

View File

@@ -0,0 +1,258 @@
package statistics
import (
"time"
)
// StatisticsMetricDTO 统计指标DTO
type StatisticsMetricDTO struct {
ID string `json:"id" comment:"统计指标唯一标识"`
MetricType string `json:"metric_type" comment:"指标类型"`
MetricName string `json:"metric_name" comment:"指标名称"`
Dimension string `json:"dimension" comment:"统计维度"`
Value float64 `json:"value" comment:"指标值"`
Metadata string `json:"metadata" comment:"额外维度信息"`
Date time.Time `json:"date" comment:"统计日期"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// StatisticsReportDTO 统计报告DTO
type StatisticsReportDTO struct {
ID string `json:"id" comment:"报告唯一标识"`
ReportType string `json:"report_type" comment:"报告类型"`
Title string `json:"title" comment:"报告标题"`
Content string `json:"content" comment:"报告内容"`
Period string `json:"period" comment:"统计周期"`
UserRole string `json:"user_role" comment:"用户角色"`
Status string `json:"status" comment:"报告状态"`
GeneratedBy string `json:"generated_by" comment:"生成者ID"`
GeneratedAt *time.Time `json:"generated_at" comment:"生成时间"`
ExpiresAt *time.Time `json:"expires_at" comment:"过期时间"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// StatisticsDashboardDTO 统计仪表板DTO
type StatisticsDashboardDTO struct {
ID string `json:"id" comment:"仪表板唯一标识"`
Name string `json:"name" comment:"仪表板名称"`
Description string `json:"description" comment:"仪表板描述"`
UserRole string `json:"user_role" comment:"用户角色"`
IsDefault bool `json:"is_default" comment:"是否为默认仪表板"`
IsActive bool `json:"is_active" comment:"是否激活"`
Layout string `json:"layout" comment:"布局配置"`
Widgets string `json:"widgets" comment:"组件配置"`
Settings string `json:"settings" comment:"设置配置"`
RefreshInterval int `json:"refresh_interval" comment:"刷新间隔(秒)"`
CreatedBy string `json:"created_by" comment:"创建者ID"`
AccessLevel string `json:"access_level" comment:"访问级别"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// DashboardDataDTO 仪表板数据DTO
type DashboardDataDTO struct {
// API调用统计
APICalls struct {
TotalCount int64 `json:"total_count" comment:"总调用次数"`
SuccessCount int64 `json:"success_count" comment:"成功调用次数"`
FailedCount int64 `json:"failed_count" comment:"失败调用次数"`
SuccessRate float64 `json:"success_rate" comment:"成功率"`
AvgResponseTime float64 `json:"avg_response_time" comment:"平均响应时间"`
} `json:"api_calls"`
// 用户统计
Users struct {
TotalCount int64 `json:"total_count" comment:"总用户数"`
CertifiedCount int64 `json:"certified_count" comment:"认证用户数"`
ActiveCount int64 `json:"active_count" comment:"活跃用户数"`
CertificationRate float64 `json:"certification_rate" comment:"认证完成率"`
RetentionRate float64 `json:"retention_rate" comment:"留存率"`
} `json:"users"`
// 财务统计
Finance struct {
TotalAmount float64 `json:"total_amount" comment:"总金额"`
RechargeAmount float64 `json:"recharge_amount" comment:"充值金额"`
DeductAmount float64 `json:"deduct_amount" comment:"扣款金额"`
NetAmount float64 `json:"net_amount" comment:"净金额"`
} `json:"finance"`
// 产品统计
Products struct {
TotalProducts int64 `json:"total_products" comment:"总产品数"`
ActiveProducts int64 `json:"active_products" comment:"活跃产品数"`
TotalSubscriptions int64 `json:"total_subscriptions" comment:"总订阅数"`
ActiveSubscriptions int64 `json:"active_subscriptions" comment:"活跃订阅数"`
} `json:"products"`
// 认证统计
Certification struct {
TotalCertifications int64 `json:"total_certifications" comment:"总认证数"`
CompletedCertifications int64 `json:"completed_certifications" comment:"完成认证数"`
PendingCertifications int64 `json:"pending_certifications" comment:"待处理认证数"`
FailedCertifications int64 `json:"failed_certifications" comment:"失败认证数"`
CompletionRate float64 `json:"completion_rate" comment:"完成率"`
} `json:"certification"`
// 时间信息
Period struct {
StartDate string `json:"start_date" comment:"开始日期"`
EndDate string `json:"end_date" comment:"结束日期"`
Period string `json:"period" comment:"统计周期"`
} `json:"period"`
// 元数据
Metadata struct {
GeneratedAt string `json:"generated_at" comment:"生成时间"`
UserRole string `json:"user_role" comment:"用户角色"`
DataVersion string `json:"data_version" comment:"数据版本"`
} `json:"metadata"`
}
// RealtimeMetricsDTO 实时指标DTO
type RealtimeMetricsDTO struct {
MetricType string `json:"metric_type" comment:"指标类型"`
Metrics map[string]float64 `json:"metrics" comment:"指标数据"`
Timestamp time.Time `json:"timestamp" comment:"时间戳"`
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
}
// HistoricalMetricsDTO 历史指标DTO
type HistoricalMetricsDTO struct {
MetricType string `json:"metric_type" comment:"指标类型"`
MetricName string `json:"metric_name" comment:"指标名称"`
Dimension string `json:"dimension" comment:"统计维度"`
DataPoints []DataPointDTO `json:"data_points" comment:"数据点"`
Summary MetricsSummaryDTO `json:"summary" comment:"汇总信息"`
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
}
// DataPointDTO 数据点DTO
type DataPointDTO struct {
Date time.Time `json:"date" comment:"日期"`
Value float64 `json:"value" comment:"值"`
Label string `json:"label" comment:"标签"`
}
// MetricsSummaryDTO 指标汇总DTO
type MetricsSummaryDTO struct {
Total float64 `json:"total" comment:"总值"`
Average float64 `json:"average" comment:"平均值"`
Max float64 `json:"max" comment:"最大值"`
Min float64 `json:"min" comment:"最小值"`
Count int64 `json:"count" comment:"数据点数量"`
GrowthRate float64 `json:"growth_rate" comment:"增长率"`
Trend string `json:"trend" comment:"趋势"`
}
// ReportContentDTO 报告内容DTO
type ReportContentDTO struct {
ReportType string `json:"report_type" comment:"报告类型"`
Title string `json:"title" comment:"报告标题"`
Summary map[string]interface{} `json:"summary" comment:"汇总信息"`
Details map[string]interface{} `json:"details" comment:"详细信息"`
Charts []ChartDTO `json:"charts" comment:"图表数据"`
Tables []TableDTO `json:"tables" comment:"表格数据"`
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
}
// ChartDTO 图表DTO
type ChartDTO struct {
Type string `json:"type" comment:"图表类型"`
Title string `json:"title" comment:"图表标题"`
Data map[string]interface{} `json:"data" comment:"图表数据"`
Options map[string]interface{} `json:"options" comment:"图表选项"`
Description string `json:"description" comment:"图表描述"`
}
// TableDTO 表格DTO
type TableDTO struct {
Title string `json:"title" comment:"表格标题"`
Headers []string `json:"headers" comment:"表头"`
Rows [][]interface{} `json:"rows" comment:"表格行数据"`
Summary map[string]interface{} `json:"summary" comment:"汇总信息"`
Description string `json:"description" comment:"表格描述"`
}
// ExportDataDTO 导出数据DTO
type ExportDataDTO struct {
Format string `json:"format" comment:"导出格式"`
FileName string `json:"file_name" comment:"文件名"`
Data []map[string]interface{} `json:"data" comment:"导出数据"`
Headers []string `json:"headers" comment:"表头"`
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
DownloadURL string `json:"download_url" comment:"下载链接"`
}
// StatisticsQueryDTO 统计查询DTO
type StatisticsQueryDTO struct {
MetricType string `json:"metric_type" form:"metric_type" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
Period string `json:"period" form:"period" comment:"统计周期"`
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
Limit int `json:"limit" form:"limit" comment:"限制数量"`
Offset int `json:"offset" form:"offset" comment:"偏移量"`
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
}
// ReportGenerationDTO 报告生成DTO
type ReportGenerationDTO struct {
ReportType string `json:"report_type" comment:"报告类型"`
Title string `json:"title" comment:"报告标题"`
Period string `json:"period" comment:"统计周期"`
UserRole string `json:"user_role" comment:"用户角色"`
StartDate time.Time `json:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" comment:"结束日期"`
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
Format string `json:"format" comment:"输出格式"`
GeneratedBy string `json:"generated_by" comment:"生成者ID"`
}
// DashboardConfigDTO 仪表板配置DTO
type DashboardConfigDTO struct {
Name string `json:"name" comment:"仪表板名称"`
Description string `json:"description" comment:"仪表板描述"`
UserRole string `json:"user_role" comment:"用户角色"`
Layout string `json:"layout" comment:"布局配置"`
Widgets string `json:"widgets" comment:"组件配置"`
Settings string `json:"settings" comment:"设置配置"`
RefreshInterval int `json:"refresh_interval" comment:"刷新间隔(秒)"`
AccessLevel string `json:"access_level" comment:"访问级别"`
CreatedBy string `json:"created_by" comment:"创建者ID"`
}
// StatisticsResponseDTO 统计响应DTO
type StatisticsResponseDTO struct {
Success bool `json:"success" comment:"是否成功"`
Message string `json:"message" comment:"响应消息"`
Data interface{} `json:"data" comment:"响应数据"`
Meta map[string]interface{} `json:"meta" comment:"元数据"`
Error string `json:"error,omitempty" comment:"错误信息"`
}
// PaginationDTO 分页DTO
type PaginationDTO struct {
Page int `json:"page" comment:"当前页"`
PageSize int `json:"page_size" comment:"每页大小"`
Total int64 `json:"total" comment:"总数量"`
Pages int `json:"pages" comment:"总页数"`
HasNext bool `json:"has_next" comment:"是否有下一页"`
HasPrev bool `json:"has_prev" comment:"是否有上一页"`
}
// StatisticsListResponseDTO 统计列表响应DTO
type StatisticsListResponseDTO struct {
Success bool `json:"success" comment:"是否成功"`
Message string `json:"message" comment:"响应消息"`
Data []interface{} `json:"data" comment:"数据列表"`
Pagination PaginationDTO `json:"pagination" comment:"分页信息"`
Meta map[string]interface{} `json:"meta" comment:"元数据"`
Error string `json:"error,omitempty" comment:"错误信息"`
}

View File

@@ -0,0 +1,186 @@
package statistics
import (
"context"
"time"
)
// StatisticsApplicationService 统计应用服务接口
// 负责统计功能的业务逻辑编排和协调
type StatisticsApplicationService interface {
// ================ 指标管理 ================
// CreateMetric 创建统计指标
CreateMetric(ctx context.Context, cmd *CreateMetricCommand) (*CommandResponse, error)
// UpdateMetric 更新统计指标
UpdateMetric(ctx context.Context, cmd *UpdateMetricCommand) (*CommandResponse, error)
// DeleteMetric 删除统计指标
DeleteMetric(ctx context.Context, cmd *DeleteMetricCommand) (*CommandResponse, error)
// GetMetric 获取单个指标
GetMetric(ctx context.Context, query *GetMetricQuery) (*QueryResponse, error)
// GetMetrics 获取指标列表
GetMetrics(ctx context.Context, query *GetMetricsQuery) (*ListResponse, error)
// ================ 实时统计 ================
// GetRealtimeMetrics 获取实时指标
GetRealtimeMetrics(ctx context.Context, query *GetRealtimeMetricsQuery) (*QueryResponse, error)
// UpdateRealtimeMetric 更新实时指标
UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error
// ================ 历史统计 ================
// GetHistoricalMetrics 获取历史指标
GetHistoricalMetrics(ctx context.Context, query *GetHistoricalMetricsQuery) (*QueryResponse, error)
// AggregateMetrics 聚合指标
AggregateMetrics(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) error
// ================ 仪表板管理 ================
// CreateDashboard 创建仪表板
CreateDashboard(ctx context.Context, cmd *CreateDashboardCommand) (*CommandResponse, error)
// UpdateDashboard 更新仪表板
UpdateDashboard(ctx context.Context, cmd *UpdateDashboardCommand) (*CommandResponse, error)
// DeleteDashboard 删除仪表板
DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) (*CommandResponse, error)
// GetDashboard 获取单个仪表板
GetDashboard(ctx context.Context, query *GetDashboardQuery) (*QueryResponse, error)
// GetDashboards 获取仪表板列表
GetDashboards(ctx context.Context, query *GetDashboardsQuery) (*ListResponse, error)
// SetDefaultDashboard 设置默认仪表板
SetDefaultDashboard(ctx context.Context, cmd *SetDefaultDashboardCommand) (*CommandResponse, error)
// ActivateDashboard 激活仪表板
ActivateDashboard(ctx context.Context, cmd *ActivateDashboardCommand) (*CommandResponse, error)
// DeactivateDashboard 停用仪表板
DeactivateDashboard(ctx context.Context, cmd *DeactivateDashboardCommand) (*CommandResponse, error)
// GetDashboardData 获取仪表板数据
GetDashboardData(ctx context.Context, query *GetDashboardDataQuery) (*QueryResponse, error)
// ================ 报告管理 ================
// GenerateReport 生成报告
GenerateReport(ctx context.Context, cmd *GenerateReportCommand) (*CommandResponse, error)
// GetReport 获取单个报告
GetReport(ctx context.Context, query *GetReportQuery) (*QueryResponse, error)
// GetReports 获取报告列表
GetReports(ctx context.Context, query *GetReportsQuery) (*ListResponse, error)
// DeleteReport 删除报告
DeleteReport(ctx context.Context, reportID string) (*CommandResponse, error)
// ================ 统计分析 ================
// CalculateGrowthRate 计算增长率
CalculateGrowthRate(ctx context.Context, query *CalculateGrowthRateQuery) (*QueryResponse, error)
// CalculateTrend 计算趋势
CalculateTrend(ctx context.Context, query *CalculateTrendQuery) (*QueryResponse, error)
// CalculateCorrelation 计算相关性
CalculateCorrelation(ctx context.Context, query *CalculateCorrelationQuery) (*QueryResponse, error)
// CalculateMovingAverage 计算移动平均
CalculateMovingAverage(ctx context.Context, query *CalculateMovingAverageQuery) (*QueryResponse, error)
// CalculateSeasonality 计算季节性
CalculateSeasonality(ctx context.Context, query *CalculateSeasonalityQuery) (*QueryResponse, error)
// ================ 数据导出 ================
// ExportData 导出数据
ExportData(ctx context.Context, cmd *ExportDataCommand) (*CommandResponse, error)
// ================ 定时任务 ================
// ProcessHourlyAggregation 处理小时级聚合
ProcessHourlyAggregation(ctx context.Context, date time.Time) error
// ProcessDailyAggregation 处理日级聚合
ProcessDailyAggregation(ctx context.Context, date time.Time) error
// ProcessWeeklyAggregation 处理周级聚合
ProcessWeeklyAggregation(ctx context.Context, date time.Time) error
// ProcessMonthlyAggregation 处理月级聚合
ProcessMonthlyAggregation(ctx context.Context, date time.Time) error
// CleanupExpiredData 清理过期数据
CleanupExpiredData(ctx context.Context) error
// ================ 管理员专用方法 ================
// AdminGetSystemStatistics 管理员获取系统统计
AdminGetSystemStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
// AdminTriggerAggregation 管理员触发数据聚合
AdminTriggerAggregation(ctx context.Context, cmd *TriggerAggregationCommand) (*CommandResponse, error)
// AdminGetUserStatistics 管理员获取单个用户统计
AdminGetUserStatistics(ctx context.Context, userID string) (*QueryResponse, error)
// ================ 管理员独立域统计接口 ================
// AdminGetUserDomainStatistics 管理员获取用户域统计
AdminGetUserDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
// AdminGetApiDomainStatistics 管理员获取API域统计
AdminGetApiDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
// AdminGetConsumptionDomainStatistics 管理员获取消费域统计
AdminGetConsumptionDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
// AdminGetRechargeDomainStatistics 管理员获取充值域统计
AdminGetRechargeDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
// ================ 公开和用户统计方法 ================
// GetPublicStatistics 获取公开统计信息
GetPublicStatistics(ctx context.Context) (*QueryResponse, error)
// GetUserStatistics 获取用户统计信息
GetUserStatistics(ctx context.Context, userID string) (*QueryResponse, error)
// ================ 独立统计接口 ================
// GetApiCallsStatistics 获取API调用统计
GetApiCallsStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
// GetConsumptionStatistics 获取消费统计
GetConsumptionStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
// GetRechargeStatistics 获取充值统计
GetRechargeStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
// GetLatestProducts 获取最新产品推荐
GetLatestProducts(ctx context.Context, limit int) (*QueryResponse, error)
// ================ 管理员排行榜接口 ================
// AdminGetUserCallRanking 获取用户调用排行榜
AdminGetUserCallRanking(ctx context.Context, rankingType, period string, limit int) (*QueryResponse, error)
// AdminGetRechargeRanking 获取充值排行榜
AdminGetRechargeRanking(ctx context.Context, period string, limit int) (*QueryResponse, error)
// AdminGetApiPopularityRanking 获取API受欢迎程度排行榜
AdminGetApiPopularityRanking(ctx context.Context, period string, limit int) (*QueryResponse, error)
// AdminGetTodayCertifiedEnterprises 获取今日认证企业列表
AdminGetTodayCertifiedEnterprises(ctx context.Context, limit int) (*QueryResponse, error)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,108 @@
package commands
// SubPortalRegisterCommand 子站注册(邀请码必填)
type SubPortalRegisterCommand struct {
Phone string `json:"phone" binding:"required"`
Password string `json:"password" binding:"required"`
ConfirmPassword string `json:"confirm_password" binding:"required"`
Code string `json:"code" binding:"required"`
InviteToken string `json:"invite_token" binding:"required"`
}
// ListChildApiCallsCommand 下属 API 调用记录查询
type ListChildApiCallsCommand struct {
ParentUserID string
ChildUserID string `form:"child_user_id" binding:"required"`
Page int `form:"page"`
PageSize int `form:"page_size"`
TransactionID string `form:"transaction_id"`
ProductName string `form:"product_name"`
Status string `form:"status"`
StartTime string `form:"start_time"`
EndTime string `form:"end_time"`
}
// ListSubordinatesCommand 下属列表查询
type ListSubordinatesCommand struct {
ParentUserID string
Page int `form:"page"`
PageSize int `form:"page_size"`
Remark string `form:"remark"`
Phone string `form:"phone"`
CompanyName string `form:"company_name"`
}
// UpdateSubordinateRemarkCommand 更新下属备注
type UpdateSubordinateRemarkCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" binding:"required"`
Remark string `json:"remark"`
}
// CreateInvitationCommand 主账号创建邀请
type CreateInvitationCommand struct {
ParentUserID string
// ExpiresInHours 可选0 或不传为永久有效100年
ExpiresInHours int `json:"expires_in_hours"`
}
// AllocateToChildCommand 主账号向下属划余额
type AllocateToChildCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" binding:"required"`
Amount string `json:"amount" binding:"required"`
VerifyCode string `json:"verify_code" binding:"required,len=6"`
}
// AssignChildSubscriptionCommand 为下属代配订阅
type AssignChildSubscriptionCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" binding:"required"`
ProductID string `json:"product_id" binding:"required"`
Price string `json:"price" binding:"required"`
UIComponentPrice string `json:"ui_component_price"`
}
// ListChildAllocationsCommand 下属划拨记录查询
type ListChildAllocationsCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" form:"child_user_id" binding:"required"`
Page int `json:"page" form:"page"`
PageSize int `json:"page_size" form:"page_size"`
}
// ListChildSubscriptionsCommand 下属订阅列表查询
type ListChildSubscriptionsCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" form:"child_user_id" binding:"required"`
}
// RemoveChildSubscriptionCommand 删除下属订阅
type RemoveChildSubscriptionCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" binding:"required"`
SubscriptionID string `json:"subscription_id" binding:"required"`
}
// PurchaseChildQuotaCommand 主账号为子账号购买调用额度
type PurchaseChildQuotaCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" binding:"required"`
ProductID string `json:"product_id" binding:"required"`
CallCount int64 `json:"call_count" binding:"required,min=1"`
VerifyCode string `json:"verify_code" binding:"required,len=6"`
}
// ListChildQuotaPurchasesCommand 下属额度购买记录查询
type ListChildQuotaPurchasesCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" form:"child_user_id" binding:"required"`
Page int `json:"page" form:"page"`
PageSize int `json:"page_size" form:"page_size"`
}
// ListChildQuotaAccountsCommand 下属额度账户查询
type ListChildQuotaAccountsCommand struct {
ParentUserID string
ChildUserID string `json:"child_user_id" form:"child_user_id" binding:"required"`
}

Some files were not shown because too many files have changed in this diff Show More