网关聚合 Knife4J 文档


网关聚合 Knife4J 文档

原创 已于 2025-09-28 12:00:07 修改 · 粉丝可见 · 98 阅读 · 0 · 0 GEO检测 · 编辑
文章链接:https://blog.csdn.net/hacker_51/article/details/152074797

目录

[TOC]

搭建并配置网关

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

yml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 8080

spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
username: nacos
password: nacos
gateway:
routes:
- id: News # 路由规则id,自定义,唯一
uri: lb://NewsModule # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/news/** # 这里是以请求路径作为判断规则
- id: User # 路由规则id,自定义,唯一
uri: lb://UserModule # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/user/** # 这里是以请求路径作为判断规则

启动网关

访问测试: http://localhost:8080/news/getBackInfo

编写Swagger配置资源

创建 网关模块

在网关服务引入 Knife4j 依赖

1
2
3
4
5
6
<!-- knife4j Aggregate documents -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>

注意 这里 Gateway 服务的 Knife4j 依赖与先前 user 服务的 Knife4j 不是同一个依赖

为网关服务添加配置项

application.ymlapplication.properties 文件中添加以下配置(加入 /api/**):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
server:
port: 8080
spring:
cloud:
nacos:
# nacos 服务地址
server-addr: @nacos.server@
discovery:
# 注册组
group: @nacos.discovery.group@
namespace: @profiles.active@
username: @nacos.user@
password: @nacos.pwd@
config:
# 配置组
group: @nacos.config.group@
namespace: @profiles.active@
username: @nacos.user@
password: @nacos.pwd@
gateway:
# 打印请求日志(自定义)
requestLog: true
discovery:
locator:
lowerCaseServiceId: true
enabled: false
routes:
- id: News # 路由规则id,自定义,唯一
uri: lb://NewsModule # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path= /NewsModule/** # 匹配路径,这里用处是 和 网关聚合 swagger 区分服务路径作用
filters:
- StripPrefix=1
- id: User # 路由规则id,自定义,唯一
uri: lb://UserModule # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path= /UserModule/** # 匹配路径,这里用处是 和 网关聚合 swagger 区分服务路径作用
filters:
- StripPrefix=1 # 去掉swagger需要区分的多余前缀,方便我们请求路径

# Knife4j 配置
knife4j:
gateway:
enabled: true
strategy: discover
discover:
enabled: true
version: openapi3
excluded-services: # 排除Knife4j网关服务的模块
- GateWay
-
# springdoc-openapi项目配置
springdoc:
api-docs:
path: /v3/api-docs # 接口文档路径 应该与 其它工程模块的接口文档路径一致
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha

其它工程模块的yml配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# springdoc-openapi项目配置
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs
group-configs:
- group: 'user-server'
paths-to-match: '/**'
packages-to-scan: com.angindem.controller
# knife4j的增强配置,不需要增强可以不配
knife4j:
enable: true
setting:
language: zh_cn

启动网关服务

成功聚合swagger。

注意: 聚合的 swagger 的请求路径会有多余的模块区分前缀,我们实际请求路径需要去掉多余前缀,所以我们配置gateway网关的时候,需要用到

filters:
- StripPrefix=1

去除swagger区分路径前缀。

统一swagger布局模块,yml配置

新建Swagger模块

编写yml配置参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
 
/**
* @author Angindem
* @description This is ${NAME} Class
* @since 2025/9/28 - 11:51
*/
@Component
@ConfigurationProperties("swagger")
public class SwaggerProperties
{
/**
* 是否开启swagger
*/
private Boolean enabled;

/**
* 标题
**/
private String title = "";

/**
* 描述
**/
private String description = "";

/**
* 版本
**/
private String version = "";

/**
* 许可证
**/
private License license = new License();

/**
* 服务条款URL
**/
private String termsOfServiceUrl = "";

/**
* 联系人信息
*/
private Contact contact = new Contact();

public Boolean getEnabled()
{
return enabled;
}

public void setEnabled(Boolean enabled)
{
this.enabled = enabled;
}

public String getTitle()
{
return title;
}

public void setTitle(String title)
{
this.title = title;
}

public String getDescription()
{
return description;
}

public void setDescription(String description)
{
this.description = description;
}

public String getVersion()
{
return version;
}

public void setVersion(String version)
{
this.version = version;
}

public String getTermsOfServiceUrl()
{
return termsOfServiceUrl;
}

public void setTermsOfServiceUrl(String termsOfServiceUrl)
{
this.termsOfServiceUrl = termsOfServiceUrl;
}

public Contact getContact()
{
return contact;
}

public void setContact(Contact contact)
{
this.contact = contact;
}

public License getLicense() {
return license;
}

public void setLicense(License license) {
this.license = license;
}
}

编写swagger配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
 
//@AutoConfiguration(before = SpringDocConfiguration.class)
@Configuration
@EnableKnife4j
@EnableConfigurationProperties(SwaggerProperties.class)
@ConditionalOnProperty(name = "swagger.enabled", matchIfMissing = true)
public class SwaggerAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public SwaggerProperties swaggerProperties() {
return new SwaggerProperties();
}

@Bean
public GroupedOpenApi api() {
return GroupedOpenApi.builder()
.group("default")
.pathsToMatch("/**")
.addOpenApiCustomizer(this.openApiCustomizer()) // 修正方法名拼写
.build();
}

@Bean
@ConditionalOnMissingBean(OpenAPI.class)
public OpenAPI openApi(SwaggerProperties swaggerProperties) {
OpenAPI openApi = new OpenAPI();
boolean devDebugFlag = UserFun.checkExistLoadBalancer();
String title = swaggerProperties.getTitle();
if(devDebugFlag){
String address = UserFun.getServerIp();
title = title + " (开发/测试环境,服务所在IP:"+address+")";
}
Info info = new Info();
info.setDescription(swaggerProperties.getDescription());
info.setContact(swaggerProperties.getContact());
info.setLicense(swaggerProperties.getLicense());
info.setTitle(title);
info.setVersion(swaggerProperties.getVersion());
info.setTermsOfService(swaggerProperties.getTermsOfServiceUrl());
// 文档基本信息
openApi.info(info);
return openApi;
}

/**
* 对已经生成好的 OpenApi 进行自定义操作
*/
@Bean
public OpenApiCustomizer openApiCustomizer() { // 方法名也要保持一致
return openApi -> {
//添加全局响应状态码
openApi.getPaths().values().forEach(pathItem -> pathItem.readOperations().forEach(operation -> {
Arrays.stream(CodeEnum.values()).forEach(codeEnum -> {
if(!codeEnum.equals(CodeEnum.success)){
operation.getResponses().addApiResponse(String.valueOf(codeEnum.getCode()), new ApiResponse().description(codeEnum.getMsg()));
}
});
}));
};
}
}

配置所需的参数

全局统一HTTP响应码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 
/**
* @author Angindem
* @description 全局统一HTTP响应码
* @since 2025/9/28 - 11:53
*/
public enum CodeEnum {

success(200,"操作成功"),
logicCheck(202,"逻辑检查不通过"),
paramError(400,"参数错误"),
authentication(401,"未登录(需要身份验证)"),
noPermission(403,"没有权限,访问受限"),
sms(418,"自定义状态码:用于短信发送"),
sysError(500,"系统错误"),
fail(501,"操作失败"),
repeat(502,"重复提交"),
busy(503,"系统繁忙,请稍后再试")
;

private int code;
private String msg;

CodeEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
网络服务工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
 
/**
* @author Angindem
* @description 此类用于存放无数据库操作的通用方法
* @since 2025/9/28 - 11:54
*/
public class UserFun {

//中文(大陆)计数单位
static final String[] unitCN = new String[]{"京","","","","万兆","","","","兆","","","","万亿","","","","亿","","","","万","","","",""};

public String ggg(Method method){
return method.getClass().getName() + "." + method.getName();
}
/**
* 新建Cookie对象(无加密,根路径)
* @最后修改人 方安伦
* @修改时间 2014-12-12 下午3:12:38
* @param name
* @param value
* @return
*/
public static Cookie newCookie(String name, String value){
Cookie randomCookie = new Cookie();
randomCookie.setPath("/");
return randomCookie;
}

public static int compareDatesDay(Date date1,Date date2){
int days = 0;
try {
days = (int) ((date2.getTime() - date1.getTime()) / (1000*3600*24));
} catch (Exception e) {
e.printStackTrace();
}
return days;
}

/**
* 获取一定位数的随机字符串(A-z0-9)
* @最后修改人 方安伦
* @修改时间 2014-12-22 下午3:58:20
* @param len
* @return
*/
public static String getRandomString(int len){
//定义可选择的字符
char[] chars = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h',
'i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H',
'I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'};
Random random = new Random();
StringBuffer buffer = new StringBuffer();
for(int i=0;i<len;i++){
buffer.append(chars[random.nextInt(chars.length)]);
}
return buffer.toString();
}

/**
* 获取通过代理的用户的真实IP地址
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}

/**
* 把一个double数值转换为带有中文单位的字符串
* @author 雷日初
* @date 2020年1月17日 上午09:56:52
* @param num 待转换的double数值
* @param digit 添加计量单位后要保留的最长小数位数(注:小于一万的数值,原小数部分将全部保留)
* 如:10000.1234转换得“1万”,9999.1234转换得“9999.1234”
* @return 带中文“兆/亿/万”计量单位的字符串,在不超过指定保留的最长小数位数的情况下,再截去结尾连续为0的小数位
*/
public static String formartNumByCnUnit(double num, int digit){
if(digit<0){
digit = 0;
}
String numberStr = (new BigDecimal(String.valueOf(num))).toPlainString();
String ends = "";
char[] splits = null;
if(numberStr.indexOf(".")>-1){
splits = (numberStr.substring(0,numberStr.indexOf("."))).toCharArray();
char[] ends0 = (numberStr.substring(numberStr.indexOf(".")+1, numberStr.length())).toCharArray();
for(int i=ends0.length-1;i>=0;i--){
if(ends0[i] != '0'){
for(int j=0;j<=i;j++){
ends += ends0[j];
}
break;
}
}
}else{
splits = numberStr.toCharArray();
}
StringBuffer retStr = new StringBuffer();
String unit = "";
for(int i=0;i<splits.length;i++){
if(splits.length>4 && (unitCN.length-splits.length+i)%4==0 && unit.length()==0){
unit = unitCN[unitCN.length-splits.length+i];
retStr.append(splits[i]);
String others = "";
for(int j=i+1;j<splits.length && j-i<=digit+1;j++){
others += splits[j];
}
if(others.length()>0){
int oth = Integer.parseInt(others);
if(oth>0){
if(others.length()>digit){
double tmps = (Math.round(oth*Math.pow(10,-1)))*Math.pow(10,-digit);
others = ((tmps + "").split("\\.")).length>1?((tmps + "").split("\\."))[1]:"";
others = others.length()>=digit ? others.substring(0, digit) : others;
if(tmps >= 1){
int tmpInt = Integer.parseInt(retStr.toString()) + 1;
if(tmpInt==10000){
retStr = new StringBuffer("1");
unit = unitCN[unitCN.length-splits.length+i-4];
}else{
retStr = new StringBuffer(tmpInt+"");
}
}
}
while(others.endsWith("0")){
others = others.substring(0, others.length()-1);
}
others = others.length() == 0 ? "" : "." + others;
}else{
others = "";
}
}
retStr.append(others + unit);
break;
}else{
retStr.append(splits[i]);
}
}
if(unit.length()==0 && ends.length()>0){
retStr.append("." + ends);
}
return retStr.toString();
}

/**
* @description 从listA里删除listB里有的数据
* @author 方安伦
* @date 2021/5/27 10:32
* @param listA
* @param listB
* @return List<String>
**/
public static List<String> listrem(List<String> listA, List<String> listB){
for (Iterator<String> itA = listA.iterator(); itA.hasNext();){
String temp = itA.next();
// itA.next() 只能在外层循环里面调用1次
for (int i = 0; i < listB.size(); i++){
if (temp.equals(listB.get(i))){
itA.remove();
break;
}
}
}
return listA;
}

public static int getRandomInt(int min,int max){
return (int)(min+Math.random()*(max-min+1));
}

public static String getUUID32(){
String uuid = UUID.randomUUID().toString(); //转化为String对象
uuid = uuid.replace("-", "");//因为UUID本身为32位只是生成时多了“-”,所以将它们去除就可
return uuid;
}

public static String getStringNoNullTrim(String str) {
return str != null && !"null".equals(str.trim()) ? str.trim() : "";
}

/**
* @description 根据异常对象,取得异常堆栈信息
* @author 方安伦
* @date 2021/11/22 17:14
* @param e
* @return String
**/
public static String getStackTraceForHtml(Throwable e, int linenum) {
String returnStr = "";
int lineCount = 0;
int moreInt = 0;
int causeLength = 0;
StringBuffer buf = new StringBuffer();
StackTraceElement[] elements = e.getStackTrace();
Throwable causeThrowable = e.getCause();
StackTraceElement[] elementsCause = null;
if (causeThrowable != null) {
elementsCause = causeThrowable.getStackTrace();
causeLength = elementsCause.length;
}

if (lineCount >= linenum && linenum != -1) {
if (moreInt == 0) {
moreInt = elements.length + causeLength;
buf.append("<br>&nbsp;&nbsp;&nbsp;&nbsp;... " + moreInt + " more");
}
} else {
buf.append(e.getClass().getName() + ":" + getStringNoNullTrim(e.getMessage()));
++lineCount;
}

int i;
String opsition;
for(i = 0; i < elements.length; ++i) {
if (lineCount >= linenum && linenum != -1) {
if (moreInt == 0) {
moreInt = elements.length - i + causeLength;
buf.append("<br>&nbsp;&nbsp;&nbsp;&nbsp;... " + moreInt + " more");
}
} else {
opsition = "Unknown Source";
if (elements[i].getFileName() != null) {
opsition = elements[i].getFileName() + ":" + elements[i].getLineNumber();
}

buf.append("<br>&nbsp;&nbsp;&nbsp;&nbsp;at " + elements[i].getClassName() + "." + elements[i].getMethodName() + "(" + opsition + ")");
++lineCount;
}
}

if (causeThrowable != null) {
if (lineCount >= linenum && linenum != -1) {
if (moreInt == 0) {
moreInt = causeLength;
buf.append("<br>&nbsp;&nbsp;&nbsp;&nbsp;... " + causeLength + " more");
}
} else {
buf.append("<br>Caused by:" + causeThrowable.getClass().getName() + ":" + getStringNoNullTrim(causeThrowable.getMessage()) + "<br>");
lineCount += 2;
}
}

if (causeThrowable != null) {
for(i = 0; i < causeLength; ++i) {
if (lineCount >= linenum && linenum != -1) {
if (moreInt == 0) {
moreInt = causeLength - i;
buf.append("<br>&nbsp;&nbsp;&nbsp;&nbsp;... " + moreInt + " more");
}
} else {
opsition = "Unknown Source";
if (elementsCause[i].getFileName() != null) {
opsition = elementsCause[i].getFileName() + ":" + elementsCause[i].getLineNumber();
}

buf.append("<br>&nbsp;&nbsp;&nbsp;&nbsp;at " + elementsCause[i].getClassName() + "." + elementsCause[i].getMethodName() + "(" + opsition + ")");
++lineCount;
}
}
}

returnStr = buf.toString();
returnStr = returnStr.trim();
return returnStr;
}

/**
* @description 根据属性,调用get方法
* @author 方安伦
* @date 2021/12/10 14:30
**/
public static Object doGetMethod(Object ob , String name)throws Exception{
Method[] m = ob.getClass().getMethods();
for(int i = 0;i < m.length;i++){
if(("get"+name).toLowerCase().equals(m[i].getName().toLowerCase())){
return m[i].invoke(ob);
}
}
return null;
}

/**
* 通过判断是否存在指定类,识别当前运行环境是否为本地开发调试环境
* @return boolean
*/
public static boolean checkExistLoadBalancer(){
try {
Class.forName("com.detech.dems.common.loadbalance.core.CustomSpringCloudLoadBalancer");
//此时没有报异常,表明类CustomSpringCloudLoadBalancer是存在的,说明此时是本地开发调试环境
return true;
} catch (ClassNotFoundException e) {
//异常则说明类CustomSpringCloudLoadBalancer不存在,说明此时不是本地开发调试环境
}
return false;
}

/**
* 通过判断是否存在指定类,识别当前服务是否为网关服务
* @return boolean
*/
public static boolean checkExistGatewayApplication(){
try {
Class.forName("com.detech.dems.gateway.GatewayApplication");
//此时没有报异常,表明类GatewayApplication是存在的,说明此时是网关服务
return true;
} catch (ClassNotFoundException e) {
//异常则说明类GatewayApplication不存在,说明此时不是网关服务
}
return false;
}

/**
* 字符串转布尔值
*/
public static final boolean strToBoolean(String flag) {
if("0".equals(flag)){
return false;
}
if("1".equals(flag)){
return true;
}
if("Y".equals(flag)){
return true;
}
if("N".equals(flag)){
return false;
}
return StringUtils.isEmpty(flag);
}

/**
* 获取本机IP
* @return
*/
public static String getLocalIp() throws UnknownHostException {
String host = "127.0.0.1";
try {
// 如需自定义ip可修改此处
String address = InetAddress.getLocalHost().getHostAddress();
if (address != null) {
host = address;
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
return host;
}

public static String getServerIp() {
String ip = "127.0.0.1";
try{
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface ni = interfaces.nextElement();
String name = ni.getName();
// 跳过Docker虚拟网桥及虚拟接口
if (name.toLowerCase().startsWith("br-") ||
name.toLowerCase().startsWith("docker") ||
name.toLowerCase().startsWith("veth")) {
continue;
}
Enumeration<InetAddress> addresses = ni.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if (!addr.isLoopbackAddress() && addr instanceof Inet4Address) {
return addr.getHostAddress();
}
}
}
}catch (SocketException e){
e.printStackTrace();
}
return ip;
}
}

其它模块pom引用Swagger

yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
swagger:
# 是否开启swagger
enabled: true
# 标题
title: '标题:dems微服务框架_xxx接口文档'
# 描述
description: '描述:微服务框架, 具体包括XXX,XXX模块...'
# 版本
version: '1.0.0'
# 作者信息
contact:
name: Angindem
email: 123456789@qq.com
url: https://blog.csdn.net/hacker_51?type=blog

觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭

微信二维码

wechat pay

支付宝二维码

ali pay

网关聚合 Knife4J 文档
http://blog.angindem.cn/2025/09/28/Angindem-CSDN博客/193_193/
作者
Angindem
发布于
2025年9月28日
许可协议