项目总结

数据库设计

yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xxxx
username: xxxx
password: xxxx

# mybatis-plus
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

数据表

🎈 快速生成SQL

用户表user

id 账号 密码 用户昵称 用户头像 accessKey secretKey 用户角色 创建时间 更新时间 是否删除
id userAccount userPassword userName userAvatar accessKey secretKey userRole createTime updateTime isDelete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 用户表
create table if not exists user
(
id bigint auto_increment comment 'id' primary key,
userAccount varchar(256) not null comment '账号',
userPassword varchar(512) not null comment '密码',
userName varchar(256) null comment '用户昵称',
userAvatar varchar(1024) null comment '用户头像',
accessKey varchar(512) not null comment 'accessKey',
secretKey varchar(512) not null comment 'secretKey',
userRole varchar(256) default 'user' not null comment '用户角色:user/admin/ban',
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除'
) comment '用户' collate = utf8mb4_unicode_ci;

接口信息表interface_info

Tips: 仿照网络中请求样例进行设计

​ - status中 0-关闭(默认) 1 -打开

id 名称 描述 请求地址 请求类型 请求头 响应头 状态 创建人 创建时间 更新时间 是否删除
id name description url method requestHeader responseHeader status userId createTime updateTime isDelete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 接口信息表
create table if not exists `interface_info`
(
`id` bigint not null auto_increment comment '主键' primary key,
`name` varchar(256) not null comment '接口名称',
`description` varchar(256) null comment '接口描述',
`url` varchar(512) not null comment '接口地址',
`method` varchar(256) not null comment '请求方法',
`requestHeader` text null comment '请求头',
`responseHeader` text null comment '响应头',
`status` int default 0 not null comment '接口状态 0-关闭(默认) 1-打开',
`userId` bigint not null comment '创建人',
`createTime` datetime default CURRENT_TIMESTAMP not null comment '创建时间',
`updateTime` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
`isDeleted` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)'
) comment '接口信息表';

用户接口关系表user_interface_info

id 用户id 接口id 拥有调用次数 已调用次数 状态 创建时间 更新时间 是否删除
id userId interfaceInfoId totalNum invokeNum status createTime updateTime isDelete
1
2
3
4
5
6
7
8
9
10
11
12
13
-- 用户接口关系表
create table if not exists `user_interface_info`
(
`id` bigint not null auto_increment comment '主键' primary key,
`userId` bigint not null comment '用户id',
`interfaceInfoId` bigint not null comment '接口id',
`totalNum` int default 0 not null comment '拥有调用次数',
`invokeNum` int default 0 not null comment '已调用次数',
`status` int default 0 not null comment '0-正常(默认) 1-禁用(次数用光)',
`createTime` datetime default CURRENT_TIMESTAMP not null comment '创建时间',
`updateTime` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
`isDeleted` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)'
) comment '用户接口关系表';

快速生成CRUD

🎈 MybatisX插件

  • 选择模块名称、驼峰命名、生成位置(最后先统一生成在一个包内, 然后分别复制过去)

  • 将生成的entity、service、mapper复制到对应的位置,修改mapper.xml中的包名

  • 增加主键和逻辑删除注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * id
    */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    /**
    * 是否删除
    */
    @TableLogic
    private Integer isDelete;

模拟接口模块开发

提供接口

模块: api-interface

功能: 提供模拟的接口

提供三个模拟接口 GET接口 | POST接口(url传参) | (Restful)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 名称API
*/
@RestController
@RequestMapping("/name")
@Slf4j
public class NameController {

@GetMapping()
public String getNameByGet(String name) {
return "GET 你的名字是" + name;
}

@PostMapping("/{name}")
public String getNameByPost(@PathVariable String name) {
return "POST 你的名字是" + name;
}

@PostMapping()
public String getUsernameByPost(@RequestBody User user) {
return "POST 用户名是" + user.getUsername();
}
}

网关流量染色验证

流量染色: 指在请求流中添加标识(例如特定的 HTTP 头部或请求参数),以标记不同类型的流量。通过这种方式,流量在经过各个 微服务和组件时可以被识别和处理。

避免非法调用故在网关处增加流量染色, 必须是网关转发的请求才会进行处理

🎈 实现流程:

  • 实现全局请求拦截器判断请求是否是由网关转发而来
  • 调用对应接口

🔗 全局请求拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 全局接口请求拦截器
*/
@Component
public class GlobalInterceptor implements HandlerInterceptor {

//返回 true:放行 false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String flag = request.getHeader("Gateway-Flag");
if (flag == null || !flag.equals("gbbdxstx")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// 设置响应内容类型和编码
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// 写入响应体
response.getWriter().write("您无权限调用此接口");
return false;
}
return true; //true表示放行
}
}

🔗 注册自定义拦截器

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
/**
* 配置类,注册web层相关组件
*/
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

@Resource
private GlobalInterceptor globalInterceptor;

/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(globalInterceptor)
.addPathPatterns("/**");
}

/**
* 扩展Spring MVC框架的消息转换器
* 避免返回字符串时是乱码'?'
* @param converters
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.stream()
// 过滤出StringHttpMessageConverter类型实例
.filter(StringHttpMessageConverter.class::isInstance)
.map(c -> (StringHttpMessageConverter) c)
// 这里将转换器的默认编码设置为utf-8
.forEach(c -> c.setDefaultCharset(StandardCharsets.UTF_8));
}
}

接口调用SDK开发

模块: api-sdk

SDK: 一组工具、库、示例代码的集合,帮助开发者|接口调用者创建应用程序或者进行特定的开发任务

模块功能: 提供客户端服务,使开发者|接口调用者只需要关注调用哪个接口,传递哪些参数,就跟调用现有的方法一样, 这样开发 者| 接口调用者便不用自己编写签名认证的代码, 只要调用客户端的方法即可

调用HTTP方式: HttpClient | RestTemplate | 第三方库(OKHTTP、Hutool)

🎈​ 本项目使用的是 Hutool 工具包

🎈 实现流程:

  • 客户端开发, 封装请求头(签名认证信息), 向网关发送请求
  • 签名工具类(加密)
  • 编写配置类, 实现将从配置文件读取信息并把客户端交给Spring容器管理
  • 实现开发者引用SDK自动配置
  • 利用Maven打包, 上传到仓库中

🔗 API签名认证介绍

本质

  • 签发签名
  • 使用签名(校验签名)

用户每次调用接口都需要携带ak和sk, 只认签名, 不关注用户登录态

ak和sk类似用户名和密码, 区别是ak和sk是无状态的

  • accessKey: 调用的标识
  • secretKey: 密钥

实现

通过http request header 头传递参数, 主要包括

  • accessKey : 调用的标识

  • secretKey: 密钥 ✨ 密钥不能放到请求头中

  • 用户请求参数

  • 签名sign

    • 生成: 用户参数 + 密钥 + 签名生成算法(MD5、Hmac、Sha1) => 不可解密的值
    • 验证: 服务端用一模一样的参数和算法生成新签名, 判断与请求头中的签名是否一致
  • nonce: 一个随机数, 只能用一次, 服务端要保存用过的随机数

  • timestamp: 验证时间时间戳是否过期, 达到定时清理随机数的效果

分配accessKey和secretKey

在用户注册成功时自动分配 accessKeysecretKey

1
2
3
4
5
6
7
// 3. 分配 accessKey 和 secretKey
String accessKey = DigestUtil.md5Hex(SALT + userAccount + RandomUtil.randomNumbers(5));
String secretKey = DigestUtil.md5Hex(SALT + userAccount + RandomUtil.randomNumbers(8));
// 4. 插入数据
User user = new User();
user.setAccessKey(accessKey);
user.setSecretKey(secretKey);

作业

用户可以申请更换签名 – 有空可做

🔗 客户端开发

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
/**
* 调用第三方接口的客户端
*/
public class ApiClient {

private String accessKey;
private String secretKey;

private static final String GATEWAY_HOST = "http://localhost:8103";

public ApiClient(String accessKey, String secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
}

public String getNameByGet(String name) {
//可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("name", name);
String result = HttpRequest.get(GATEWAY_HOST + "/api/name")
.form(paramMap) // 拼接参数
.addHeaders(getHeaderMap("")) // 增加请求头
.execute().body();
System.out.println(result);
return result;
}

public String getNameByPost(String name) {
String urlString = GATEWAY_HOST + "/api/name/" + name;
String result = HttpUtil.post(urlString, "");
System.out.println(result);
return result;
}

private Map<String, String> getHeaderMap(String body) {
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("accessKey", accessKey);
// 一定不能发送给后端
//hashMap.put("secretKey", secretKey);
hashMap.put("nonce", RandomUtil.randomNumbers(4));
hashMap.put("body", URLEncoder.encode(body, StandardCharsets.UTF_8));
hashMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
hashMap.put("sign", SignUtil.getSign(body, secretKey));
return hashMap;
}

public String getUsernameByPost(User user) {
String json = JSONUtil.toJsonStr(user);
HttpResponse httpResponse = HttpRequest.post(GATEWAY_HOST + "/api/name/user")
.addHeaders(getHeaderMap(json))
.body(json) // post请求请求体
.execute();
System.out.println(httpResponse.getStatus());
String result = httpResponse.body();
System.out.println(result);
return result;
}
}

🔗 签名工具类

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 签名加密
*/
@Component
public class SignUtil {

public static String getSign(String body, String secretKey) {
Digester md5 = new Digester(DigestAlgorithm.MD5);
String content = body + "." + secretKey;
return md5.digestHex(content);
}
}

🔗 配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@ConfigurationProperties("api.client")
@Data
@ComponentScan
public class ApiClientConfig {

/**
* 标识
*/
private String accessKey;

/**
* 密钥
*/
private String secretKey;

@Bean
public ApiClient apiClient() {
return new ApiClient(accessKey, secretKey);
}
}

🔗 实现开发者引入SDK自动配置

创建resources\META-INF\spring.factories文件, 写入以下内容

1
2
# spring boot starter
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.gbbdxstx.gbbdxstxapiclientsdk.ApiClientConfig

RPC开发

作用: 使调用者像调用本地方法一样调用远程方法

🎈 RPC模型:

🔗 公共服务接口

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
/**
* 网关服务公共接口
*/
public interface DemoService {

String sayHello(String name);

/**
* 调用接口统计
*/
boolean invokeCount(long userId, long interfaceInfoId);

/**
* 根据ak,sk获取用户信息
* @param accessKey
* @return
*/
User getInvokeUser(String accessKey);

/**
* 根据请求路径和方法获取接口信息
* @param url
* @param method
* @return
*/
InterfaceInfo getInterfaceInfo(String url, String method);
}

🔗 提供者

启动类上要加@EnableDubbo注解

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
/**
* 提供服务
*/
@DubboService
public class DemoServiceImpl implements DemoService {

@Resource
private UserInterfaceInfoMapper userInterfaceInfoMapper;

@Resource
private UserMapper userMapper;

@Resource
private InterfaceInfoMapper interfaceInfoMapper;

@Override
public String sayHello(String name) {
return "Hello " + name;
}

/**
* 更新调用次数
* @param userId
* @param interfaceInfoId
* @return
*/
@Override
public boolean invokeCount(long userId, long interfaceInfoId) {
// 校验
ThrowUtils.throwIf(userId <= 0 || interfaceInfoId <= 0, ErrorCode.PARAMS_ERROR);
// 查询数据
QueryWrapper<UserInterfaceInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userId", userId);
queryWrapper.eq("interfaceInfoId", interfaceInfoId);
queryWrapper.eq("status", 0);
UserInterfaceInfo userInterfaceInfo = userInterfaceInfoMapper.selectOne(queryWrapper);
ThrowUtils.throwIf(userInterfaceInfo == null, ErrorCode.NO_AUTH_ERROR);
userInterfaceInfo.setInvokeNum(userInterfaceInfo.getInvokeNum() + 1);
// 判断是否要更新用户接口状态
if (userInterfaceInfo.getTotalNum() == userInterfaceInfo.getInvokeNum()) {
userInterfaceInfo.setStatus(1);
}
// 更新数据库 将调用次数 + 1 同时状态改变时更新状态
return userInterfaceInfoMapper.updateById(userInterfaceInfo) == 1;
}


/**
* 根据ak,sk获得用户信息
* @param accessKey
* @return
*/
@Override
public User getInvokeUser(String accessKey) {
if (StringUtils.isAnyBlank(accessKey)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("accessKey", accessKey);
return userMapper.selectOne(queryWrapper);
}

/**
* 根据路径和方法获得接口信息
* @param url
* @param method
* @return
*/
@Override
public InterfaceInfo getInterfaceInfo(String url, String method) {
if (StringUtils.isAnyBlank(url, method)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("url", url);
queryWrapper.eq("method", method);
return interfaceInfoMapper.selectOne(queryWrapper);
}

}

🔗 网关调用

启动类上要加@EnableDubbo注解

1
2
3
4
5
// 注入公共服务接口
@DubboReference
private DemoService demoService;

...调用提供的方法

网关模块开发

模块: api-gateway

目的: 利用网关实现统一处理不同项目不同请求的统一操作

功能: 实现路由转发, 黑白名单, 打印日志, 进行用户鉴权, 流量染色, 统计调用次数等功能

实现方法: 利用 Spring Cloud Gateway + Dubbo + Nacos实现网关功能

🔗 配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
cloud:
gateway:
# 路由转发: 前置匹配断言
routes:
- id: api_route
uri: http://localhost:8102
predicates:
- Path=/api/**
default-filters:
- AddResponseHeader=source, gbbdxstx
dubbo:
application:
name: dubbo-springboot-demo-consumer
logger: slf4j
qos-port: 33333
registry:
address: nacos://${nacos.address:127.0.0.1}:8848?username=nacos&password=nacos
cache-file: D:/software/nacos/.mapping.dubbo.cache

🔗 全局请求过滤器

业务逻辑:

  • 用户发送请求转发到 API 网关
  • 请求日志
  • 黑白名单
  • 用户鉴权(判断 accessKey, secretKey 是否合法)
  • 判断请求的模拟接口是否存在
  • 请求转发,调用模拟接口
  • 响应日志
  • 调用成功,次数 + 1
  • 调用失败,返回一个规范的错误码
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
/**
* 全局请求过滤器
*/
@Component
@Slf4j
public class CustomGlobalFilter implements GlobalFilter, Ordered {

@DubboReference
private DemoService demoService;

private static final List<String> IP_WHITE_LIST = Arrays.asList("0:0:0:0:0:0:0:1", "127.0.0.1");

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.请求日志
ServerHttpRequest request = exchange.getRequest();
// 获得请求到网关的完整url信息
String originUrl = request.getURI().toString();
// 获得网关转发后的路由信息
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
String urlBefore = route.getUri().toString();
String url = urlBefore + request.getPath().value();
String method = request.getMethod().toString();
log.info("请求唯一标识: {}", request.getId());
log.info("请求路径: {}", url);
log.info("请求方法: {}", method);
log.info("请求参数: {}", request.getQueryParams());
String sourceAddress = request.getLocalAddress().getHostString();
log.info("请求来源地址: {}", sourceAddress);
ServerHttpResponse response = exchange.getResponse();

// 2.黑白名单
if (!IP_WHITE_LIST.contains(sourceAddress)) {
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}

// 3. 用户鉴权(判断 ak,sk 是否合法)
HttpHeaders headers = request.getHeaders();
String accessKey = headers.getFirst("accessKey");
String nonce = headers.getFirst("nonce");
// 请求体
String encodedBody = headers.getFirst("body");
String body = URLDecoder.decode(encodedBody, StandardCharsets.UTF_8);
String timestamp = headers.getFirst("timestamp");
String sign = headers.getFirst("sign"); // 用户传过来的密钥
// 实际情况去数据库中查询是否分配给用户
User invokeUser = null;
try {
invokeUser = demoService.getInvokeUser(accessKey);
} catch (Exception e) {
log.error("getInvokeUser error", e);
}
if (invokeUser == null) {
return handleNoAuth(response);
}
// todo 随机数nonce 利用hashmap或redis存进行校验
if (Long.parseLong(nonce) > 10000L) {
return handleNoAuth(response);
}
// todo 时间和当前时间不能超过 5 分钟
long currentTime = System.currentTimeMillis() / 1000;
final long FIVE_MINUTES = 60 * 5L;
if (currentTime - Long.parseLong(timestamp) > FIVE_MINUTES) {
return handleNoAuth(response);
}
// 将分配给用户的secretKey从数据库查出来用于生成sign
String secretKey = invokeUser.getSecretKey();
String serverSign = SignUtil.getSign(body, secretKey);
if (sign == null || !sign.equals(serverSign)) {
return handleNoAuth(response);
}
// 判断请求的模拟接口是否存在
InterfaceInfo interfaceInfo = null;
try {
interfaceInfo = demoService.getInterfaceInfo(url, method);
} catch (Exception e) {
log.error("getInterfaceInfo error", e);
}
if (interfaceInfo == null) {
return handleNoAuth(response);
}

// 4. 将网关信息加入请求头中
ServerHttpRequest mutatedRequest = request.mutate()
.header("Gateway-Flag", "gbbdxstx").build();
ServerWebExchange mutatedExchange = exchange.mutate()
.request(mutatedRequest)
.build();

// 5. 请求转发, 调用模拟接口
Mono<Void> filter = chain.filter(mutatedExchange);

// 6. 响应日志 Mono是异步编程 因此上面代码调用模拟接口会在全局过滤器执行完毕后执行(易出现次数增加但调用接口失败的请求)
// 下面代码实现先调用模拟接口再执行调用成功次数加一和返回响应日志的代码
log.info("响应: {}", response.getStatusCode());
Long userId = invokeUser.getId();
Long interfaceInfoId = interfaceInfo.getId();
try {
DataBufferFactory bufferFactory = response.bufferFactory();
HttpStatus statusCode = response.getStatusCode();
if (statusCode != HttpStatus.OK) {
return chain.filter(exchange);//降级处理返回数据
}
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);

return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 7. 调用成功, 调用次数加1
try {
demoService.invokeCount(userId, interfaceInfoId);
} catch (Exception e) {
log.error("invokeCount error", e);
}
// 合并多个流集合,解决返回体分段传输
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer buff = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[buff.readableByteCount()];
buff.read(content);
DataBufferUtils.release(buff);//释放掉内存

// 构建返回日志
String result = new String(content);
List<Object> rspArgs = new ArrayList<>();
rspArgs.add(response.getStatusCode().value());
rspArgs.add(exchange.getRequest().getURI());
rspArgs.add(result);
log.info("响应结果: <-- {} {}\n{}", rspArgs.toArray());

getDelegate().getHeaders().setContentLength(result.getBytes().length);
return bufferFactory.wrap(result.getBytes());
}));
} else {
// 8. 调用失败, 返回一个标准的错误码
handleInvokeError(response);
log.error("<-- {} 响应code异常", getStatusCode());
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());

} catch (Exception e) {
log.error("网关处理响应异常" + e);
return chain.filter(exchange);
}
}

@Override
public int getOrder() {
return -1;
}

public Mono<Void> handleNoAuth(ServerHttpResponse response) {
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}

public Mono<Void> handleInvokeError(ServerHttpResponse response) {
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return response.setComplete();
}
}

管理员统计分析开发

SQL编写

1
2
# 查询热点数据
select interfaceInfoId,sum(invokeNum) as invokeNum from user_interface_info group by interfaceInfoId order by invokeNum desc limit 3;

------------------------------------------------------------------待补充

接口测试调用流程

  1. 前端将用户输入的请求参数和要测试的接口id传到后端
  2. 后端对信息进行校验
  3. 校验成功后调用模拟接口

前端问题解决

layout = 'max'但不生效

app.tst文件中获取全局默认设置

1
2
3
4
5
6
7
8
9
10
// 当每个页面首次加载时, 获取要全局保存的数据, 比如用户登录信息和全局默认设置
const state: InitialState = {
loginUser: undefined,
settings: defaultSettings,
}

// 在return中展开设置
return {
...initialState?.settings, // 展开 initialState 中的设置
}