03
1. 项目概述
本项目是一个面向开发者的 API 平台,提供 API 接口供开发者调用。用户通过注册登录,可以开通接口调用权限,并可以浏览和调用接口。每次调用都会进行统计,用户可以根据统计数据进行分析和优化。管理员可以发布接口、下线接口、接入接口,并可视化接口的调用情况和数据。本项目侧重于后端,涉及多种编程技巧和架构设计层面的知识。
2. 本期时间点
3. 本期计划
- 开发接口发布 / 下线的功能(管理员) 20 min
- 前端去浏览浏览接口、查看接口文档、申请签名(注册) 20 min
- 在线调试(用户) 20 min
- 统计用户调用接口的次数 20 min (下次一定)
- 优化系统 - API 网关 20 min (下次一定)
4. 开发接口发布和下线功能
ps.前端登录页面的手机号登录,其他按钮的删除,交给大家实现。
看看我们之前的接口管理页面(不急着启动,看看下图所示就好)。
这个页面之前是给管理员使用的,现在我们要开发一个用于发布和下线接口的功能。
本质上来说,就是改变每条接口数据的状态。在设计接口信息表时,之前已经预留了一个状态字段status
。
其中,关闭和开启分别对应接口的下线和上线。只有状态为 1 的接口才可以被用户调用,否则将无法调用。
现在我们先开发后台,开发完后台之后,开发前端的时间会更灵活一些,建议大家优先把核心功能做出来,不要去纠结那些界面。
1. 功能设计
1. 功能设计讲解
现在让我们思考一下需要开发哪些后台接口。那就是发布接口和下线接口。大致规划一下思路:
发布接口:这个接口需要执行哪些任务呢?首先需要验证接口是否存在,然后判断接口是否可调用,否则访问接口都是 404,影响用户体验。接着,如果接口可以调用,我们需要修改数据库中该接口的状态为 1,表示接口已经被发布,状态默认为 0(关闭)。
下线接口:你可以为其新增一个状态字段。例如,使用 1 表示开启,使用 2 表示下线。通过这个新字段,可以清晰地区分接口状态。当状态为 0 时,表示该接口还没有进行任何处理,看大家自己的考虑。我们这里就直接使用 0 和 1 来表示状态,不再添加额外的状态字段,大家可以按照自己的需求进行设计。对于下线接口,校验接口是否存在也是和发布接口类似的,但是下线接口无需判断接口是否可调用。
另外,还需注意的一点是仅管理员可操作这两个接口,这点需要特别注意,防止用户越权操作。以上就是这两个接口的基本设计。
接下来, 我们将根据这个设计开始编写代码。整体来说,比较简单。对于业务逻辑等方面,大家可以自行思考。因为所有的业务和系统都是灵活的。我们开发的这个 API 接口开放平台只是一个简单的标准而已。但是具体的业务细节还是需要根据实际情况来进行调整。
2. 功能设计总结
后台接口:
发布接口(仅管理员可操作)
- 校验该接口是否存在
- 判断该接口是否可以调用
- 修改接口数据库中的状态字段为 1
下线接口(仅管理员可操作)
- 校验该接口是否存在
- 修改接口数据库中的状态字段为 0
2. 后端项目开发
1. 创建发布和下线接口
把 yuapi-client-sdk 项目和 yuapi-interface 项目,放到 yuapi-backend 项目中,方便操作。
或者直接拉取🐟的 项目。
找到 InterfaceInfoController.java,顺便修改注释为接口管理
。
往下滑,复制更新接口。
粘贴两个在此处。
改成发布和下线。
然后**这两个接口接收什么参数呢?**其实只需接收接口的 id 即可。比如,我想要发布 id 唯一的接口,只需传递这个 id 作为参数即可,这样就可以清楚地表示要对哪个接口进行发布操作。
这里我们新建一个通用的 request:
复制 common 目录下的 DeleteRequest.java,粘贴到 common 目录下,并重命名为IdRequest
。
IdRequest
就是封装了 id 这个参数,没有别的意思,它将一个基本类型封装成一个对象,这样便于我们进行 json 参数传递。
有同学提议:不封装,直接加参数也可以的,但是得去掉 @RequestBody 注解。
🐟建议:还是封装一下比较好,因为这样的话可以使所有的 post 接口保持统一。
回到InterfaceInfoController.java
,把这两个接口的接收参数都改一下。
这两个接口只能管理员能调用,我们要给接口打上注解。
这个注解是怎么实现的呢?这是🐟自己写的权限校验切面注解,它对应的实现方法在这里。
这个功能的原理非常简单:
首先获取当前登录用户的信息,然后判断用户是否具有管理员权限。如果没有权限,就会直接抛出权限异常;如果有权限,就会继续执行后续操作。这种方式被称为 AOP 切面的基本应用之一。对于学习 Spring 的同学来说,这个功能一定要掌握好,它能够帮助你更好地理解和应用 AOP 的概念。
2. 发布接口业务逻辑
我们开始编写发布接口的业务逻辑,根据功能设计进行编写代码。
先校验该接口是否存在。
/**
* 发布
*
* @param idRequest
* @param request
* @return
*/
// 声明该方法为一个HTTP POST请求处理方法,处理路径是"/online",用于发布接口
@PostMapping("/online")
// 该接口仅管理员可用
@AuthCheck(mustRole = "admin")
public BaseResponse<Boolean> onlineInterfaceInfo(@RequestBody IdRequest idRequest,
HttpServletRequest request) {
// 如果id为null或者id小于等于0
if (idRequest == null || idRequest.getId() <= 0) {
// 抛出业务异常,表示请求参数错误
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 1.校验该接口是否存在
// 获取idRequest对象的id属性值
long id = idRequest.getId();
// 根据id查询接口信息数据
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
// 如果查询结果为空
if (oldInterfaceInfo == null) {
// 抛出业务异常,表示未找到数据
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 2.判断该接口是否可以调用
}
接着判断该接口是否可以调用,怎么判断呢?上次我们开发了一个客户端的 SDK,直接使用它就可以了;
复制在 yuapi-interface 项目中的依赖。
粘贴到 yuapi-backend 项目中。
去 application.yml 编写客户端的配置。
然后在 InterfaceInfoController.java 引入客户端的实例。
继续编写发布接口的判断该接口是否可以调用。
/**
* 发布
*
* @param idRequest
* @param request
* @return
*/
@PostMapping("/online")
@AuthCheck(mustRole = "admin")
public BaseResponse<Boolean> onlineInterfaceInfo(@RequestBody IdRequest idRequest,
HttpServletRequest request) {
if (idRequest == null || idRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 1.校验该接口是否存在
long id = idRequest.getId();
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
if (oldInterfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 2.判断该接口是否可以调用
// 创建一个User对象(这里先模拟一下,搞个假数据)
com.yupi.yuapiclientsdk.model.User user = new com.yupi.yuapiclientsdk.model.User();
// 设置user对象的username属性为"test"
user.setUsername("test");
// 通过yuApiClient的getUsernameByPost方法传入user对象,并将返回的username赋值给username变量
String username = yuApiClient.getUsernameByPost(user);
// 如果username为空或空白字符串
if (StringUtils.isBlank(username)) {
// 抛出系统错误的业务异常,表示系统内部异常,并附带错误信息"接口验证失败"
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "接口验证失败");
}
// 创建一个InterfaceInfo对象
InterfaceInfo interfaceInfo = new InterfaceInfo();
// 设置interfaceInfo的id属性为id
interfaceInfo.setId(id);
// 3.修改接口数据库中的状态字段为 1
interfaceInfo.setStatus();
}
还有一个小问题需要注意,我们的接口名称都是固定的,而不是通过地址来映射到相应的接口,所以只能通过方法名调用接口。先记录下这个问题,后续开发过程中解决它,本期先走通整个流程(下次一定)。
这里我们的状态应该是 1,建议大家可以用枚举值来表示。
复制 PostGenderEnum.java(帖子性别枚举),粘贴到 enums 包下,并重命名为InterfaceInfoStatusEnum
(接口信息状态枚举)。
修改一下。
回到 InterfaceInfoController.java 继续编写代码。
/**
* 发布
*
* @param idRequest
* @param request
* @return
*/
@PostMapping("/online")
@AuthCheck(mustRole = "admin")
public BaseResponse<Boolean> onlineInterfaceInfo(@RequestBody IdRequest idRequest,
HttpServletRequest request) {
if (idRequest == null || idRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 1.校验该接口是否存在
long id = idRequest.getId();
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
if (oldInterfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 2.判断该接口是否可以调用
com.yupi.yuapiclientsdk.model.User user = new com.yupi.yuapiclientsdk.model.User();
user.setUsername("test");
String username = yuApiClient.getUserNameByPost(user);
if (StringUtils.isBlank(username)) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "接口验证失败");
}
InterfaceInfo interfaceInfo = new InterfaceInfo();
interfaceInfo.setId(id);
// 3.修改接口数据库中的状态字段为上线
interfaceInfo.setStatus(InterfaceInfoStatusEnum.ONLINE.getValue());
// 调用interfaceInfoService的updateById方法,传入interfaceInfo对象,并将返回的结果赋值给result变量
boolean result = interfaceInfoService.updateById(interfaceInfo);
// 返回一个成功的响应,响应体中携带result值
return ResultUtils.success(result);
}
这里的 request 对象没有用到,先留着,说不定后面会用到。
3. 下线接口业务逻辑
接下来我们写一下下线接口的业务逻辑,直接把发布接口的业务逻辑复制粘贴到下线接口中。
然后同样的逻辑判断 id 是否存在、判断这个接口是否存在,判断接口是否可以调用就不需要了,把上线的状态改为下线的状态。
接口已经完成且没有问题,接下来,启动后端项目(记得先启动 redis),无需进行测试,因为这是一个相对简单的功能。
3. 前端项目开发
1. 增加发布和下线按钮
接下来写一下前端,先把所有管理员操作的页面封装到一起;
在 page 目录下新建 Admin 目录,把 InterfaceInfo 目录放进去。
然后去 routes.ts 修改组件路径(这样路由和对应的组件路径一样,会规范一点)。
开始在前端页面的操作区增加发布和下线的按钮。
找到对应的页面 InterfaceInfo 目录下的 index.tsx,复制删除按钮。
粘贴到此处进行修改。
在终端输入yarn run dev
启动前端,访问 http://localhost:8000。
登录后,查看新增的按钮。
💡 是否可以同时展示发布和下线这两个按钮呢?
最好不要同时展示,如果一个接口已经发布了,我们只展示下线按钮会更好一些。
所以这里要加一个判断,在这个渲染函数中,能拿到当前这条记录的对象,它是一个 record,类型就是我们的接口信息,所以我们可以直接根据 record 的 status 来判断。
回到前端页面,刷新一下,因为我们所有的接口都是未发布,所以现在能看见发布按钮。
修改下线按钮和删除按钮的颜色,使用 Ant Design Pro 给我们提供的一个文字按钮组件。
回到前端页面,鼠标放到删除按钮,就会变色。
来实现发布和下线按钮的功能。大家还记得怎么调用后端接口的函数?
现在新加了两个接口,前端应该怎么办?
利用 openapi 生成即可,前端不需要写任何接口调用的方法,在终端执行yarn run openapi
。
然后直接复制删除节点的方法,粘贴到删除节点上面。
修改成发布节点。
/**
* 发布接口
*
* @param record
*/
const handleOnline = async (record: API.IdRequest) => {
// 显示正在发布的加载提示
const hide = message.loading('发布中');
// 如果接口数据为空,直接返回true
if (!record) return true;
try {
// 调用发布接口的POST请求方法
await onlineInterfaceInfoUsingPOST({
// 传递接口的id参数
id: record.id
});
hide();
// 显示操作成功的提示信息
message.success('操作成功');
// 重新加载数据
actionRef.current?.reload();
// 返回true表示发布成功
return true;
} catch (error: any) {
hide();
// 显示操作失败的错误提示信息
message.error('操作失败,' + error.message);
// 返回false表示发布失败
return false;
}
};
复制发布节点的方法,粘贴到发布节点下面,然后修改成下线节点。
/**
* 下线接口
*
* @param record
*/
const handleOffline = async (record: API.IdRequest) => {
// 显示正在下线的加载提示
const hide = message.loading('发布中');
// 如果接口数据为空,直接返回true
if (!record) return true;
try {
// 调用下线接口的POST请求方法
await offlineInterfaceInfoUsingPOST({
// 传递接口的id参数
id: record.id
});
hide();
// 显示操作成功的提示信息
message.success('操作成功');
// 重新加载数据
actionRef.current?.reload();
// 返回true表示下线成功
return true;
} catch (error: any) {
hide();
// 显示操作失败的错误提示信息
message.error('操作失败,' + error.message);
// 返回false表示下线失败
return false;
}
};
把发布和下线按钮改成调用对应的函数。
idea 另开一个窗口,打开 yuapi-interface 项目运行。
yuapi-interface 项目如果没有运行,点击发布按钮会弹出未连接。
yuapi-interface 项目是模拟接口项目,我们开发的主要是后台管理系统和用户前台,但模拟接口也要启动,可见之前设计的流程图。
回到前端页面试一下,点击发布按钮,提示操作成功
,状态变为开启,并显示了下线按钮。
点击下线按钮,提示操作成功
,状态变为关闭,并显示了发布按钮。
2. 解决按钮同时显示的问题
这时你发现,诶,我们之前不是写了不能同时显示发布和下线按钮吗?
虽然说刷新之后显示正常,但这还是太怪了,有两种方法:
- 设置不同的 key
- 发布和下线的按钮都有 key 属性,默认是 config,设置成不一样的即可,原因是,在 React 中,如果列表中的元素需要进行增删操作,需要为每个元素指定唯一的 key。这是因为 React 使用 key 来识别列表中的每个元素,以便在更新列表时能够准确地判断新旧元素的对应关系,从而提高渲染性能。
- 给发布按钮和下线按钮设置不同的 key 是解决重复渲染问题的一种方法。通过设置不同的 key 值,React 在进行新旧对比时会识别出这是两个不同的元素,从而确保它们能够正确地渲染和更新。
所以,可以通过给发布按钮和下线按钮设置不同的 key 值来解决按钮重复渲染的问题,确保 React 能够正确地进行虚拟 DOM 的对比和更新。
- 修改条件渲染逻辑
代码中是通过条件来渲染按钮,重新修正代码:
- 首先判断状态是否为 0,如果是 0,则渲染发布按钮;
- 如果状态为 1,则渲染下线按钮;其他状态的情况下,不进行任何渲染。
这样可以根据状态变化正确渲染对应的按钮,并且不会出现重复按钮的问题。
使用以上两种方法都能解决渲染问题。
然后我们点击发布按钮,页面显示下线按钮,发布按钮也被隐藏。
点击下线按钮,显示发布按钮,不会显示多个下线按钮,下线按钮被隐藏了。
搞定咯(~ ̄▽ ̄)~
5. 前端开发浏览接口页
1. 创建浏览接口页
现在要新建一个给用户看的页面,之前我们是没有主页的。
删掉 Admin.tsx,在 pages 目录下新建Index
目录。
把 Welcome.tsx 拖到 Index 目录下,并修改名为index.tsx
。
去 route.ts 修改路由,取消掉之前欢迎页的注释,然后把它放到最上面。
修改路径、名称、组件路径。
访问 http://localhost:8000,主页就展示出来,它默认提供了这些东西:了解 umi、了解 ant design...
我们不需要这些,改成我们自己的浏览接口页。
2. 修改浏览接口页
回到主页,删除多余的内容,留下 PageContainer。
- PageContainer:是 Ant Design Pro 中提供的一个组件,用于快速构建页面的容器。它提供了一些常用的布局和功能,例如面包屑导航、页面标题、操作区域等,可以帮助我们快速搭建页面的基本结构。
import { PageContainer } from '@ant-design/pro-components';
import React from 'react';
const Index: React.FC = () => {
return (
<PageContainer>
</PageContainer>
);
};
export default Index;
通过 title 设置页面标题,在 PageContainer 随便写一些内容。
import { PageContainer } from '@ant-design/pro-components';
import React from 'react';
const Index: React.FC = () => {
return (
<PageContainer title="在线接口开发平台">
阿巴巴
</PageContainer>
);
};
export default Index;
回到前端页面查看修改情况。
直接让浏览接口页像管理页一样,将所有的接口以列表形式展示给用户;
去 ant design 组件库
找一下列表(list)组件。
选一个精简一点的,然后点击显示代码按钮。
找到示例代码。
<List
className="demo-loadmore-list"
loading={initLoading}
itemLayout="horizontal"
loadMore={loadMore}
dataSource={list}
renderItem={(item) => (
<List.Item
actions={[<a key="list-loadmore-edit">edit</a>, <a key="list-loadmore-more">more</a>]}
>
<Skeleton avatar title={false} loading={item.loading} active>
<List.Item.Meta
avatar={<Avatar src={item.picture.large} />}
title={<a href="https://ant.design">{item.name?.last}</a>}
description="Ant Design, a design language for background applications, is refined by Ant UED Team"
/>
<div>content</div>
</Skeleton>
</List.Item>
)}
/>
放到这里。
这里还有什么页面的加载状态(loading),去ant design 组件库
复制。
const [initLoading, setInitLoading] = useState(true);
const [loading, setLoading] = useState(false);
const [data, setData] = useState<DataType[]>([]);
const [list, setList] = useState<DataType[]>([]);
粘贴到主页中。
然后进行修改。
import { PageContainer } from '@ant-design/pro-components';
import React, { useEffect, useState } from 'react';
import { List, message } from 'antd';
import { listInterfaceInfoByPageUsingGET } from '@/services/yuapi-backend/interfaceInfoController';
/**
* 主页
* @constructor
*/
const Index: React.FC = () => {
// 使用 useState 和泛型来定义组件内的状态
// 加载状态
const [loading, setLoading] = useState(false);
// 列表数据
const [list, setList] = useState<API.InterfaceInfo[]>([]);
// 总数
const [total, setTotal] = useState<number>(0);
// 定义异步加载数据的函数
const loadData = async (current = 1, pageSize = 5) => {
// 开始加载数据,设置 loading 状态为 true
setLoading(true);
try {
// 调用接口获取数据
const res = await listInterfaceInfoByPageUsingGET({
current,
pageSize,
});
// 将请求返回的数据设置到列表数据状态中
setList(res?.data?.records ?? []);
// 将请求返回的总数设置到总数状态中
setTotal(res?.data?.total ?? 0);
// 捕获请求失败的错误信息
} catch (error: any) {
// 请求失败时提示错误信息
message.error('请求失败,' + error.message);
}
// 数据加载成功或失败后,设置 loading 状态为 false
setLoading(false);
};
useEffect(() => {
// 页面加载完成后调用加载数据的函数
loadData();
}, []);
return (
// 使用 antd 的 PageContainer 组件作为页面容器
<PageContainer title="在线接口开放平台">
<List
className="my-list"
// 设置 loading 属性,表示数据是否正在加载中
loading={loading}
itemLayout="horizontal"
// 将列表数据作为数据源传递给 List 组件
dataSource={list}
// 渲染每个列表项
renderItem={(item) => (
<List.Item actions={[<a key={"list-loadmore-edit"}>查看</a>]}>
<List.Item.Meta
// href等会要改成接口文档的链接
title={<a href={"https://ant.design"}>{item.name}</a>}
description={item.description}
/>
</List.Item>
);
}}
// 分页配置
pagination={{
// 自定义显示总数
// eslint-disable-next-line @typescript-eslint/no-shadow
showTotal(total: number) {
return '总数:' + total;
},
// 每页显示条数
pageSize: 5,
// 总数,从状态中获取
total,
// 切换页面触发的回调函数
onChange(page, pageSize) {
// 加载对应页面的数据
loadData(page, pageSize);
},
}}
/>
</PageContainer>
);
};
export default Index;
请注意,修改后的index.tsx
中有两个将页数设置为数字 5 的设置,也就是多次使用重复的数字。这里需要记住的是,当我们在代码中使用魔法值(即凭空出现的数值)时,如果我们发现需要在多个地方修改这个数值,建议将其提取为一个常量。
**为什么要提取为常量呢?**因为使用常量可以提高代码的可维护性和可读性。通过将魔法值提取为常量,然后就可以修改常量的值来一次性修改多处使用该值的地方,而不必逐个搜索和修改每个出现的地方。
修改好后,回到前端页面,查看浏览接口页面的效果展示。
点击页数,查看数据是否正常显示。
6. 前端开发接口文档页
1. 创建浏览接口文档页
复制 Index 目录,粘贴到 page 目录下,并重命名为InterfaceInfo
。
去 route.ts 添加路由,动态路由可以查看官网 —— UmiJS,给这个路由新增两个参数:
- 让这个路由可以接收动态参数,在点击
查看
之后可以跳到对应的接口页面,通过 id 来区分不同的接口。 - path: '/interface_info/:id':定义了路由的路径,其中 :id 是一个参数占位符,表示在实际路径中可以传递一个具体的ID值作为参数。
- 让这个页面在菜单栏中隐藏,查看接口不需要放在菜单栏上。
- hideInMenu: true:表示该路由在菜单中是否隐藏,如果设置为 true,则该路由不会在菜单中显示。
现在我们要做的事情是点击查看
之后,读取到这条数据的 id,然后跳到对应的页面。
先去修改超链接。
回到前端页面,点击查看
,可以看见地址栏上的 url 变了,它已经跳转到查看接口文档页面。
访问 http://localhost:8000,点击列表项,也能正常跳转。
2. 修改浏览接口文档页
现在来改一下查看单个接口文档的页面,需要从 url 参数中拿到这个 id,然后才知道要加载哪个接口信息。
**怎么拿到动态路由的参数呢?**查看官网 —— UmiJS。
这里就使用 userMatch 来获取一下,进入查看接口文档页,进行修改。
import { PageContainer } from '@ant-design/pro-components';
import React, { useEffect, useState } from 'react';
import { useMatch } from 'react-router';
/**
* 主页
* @constructor
*/
const Index: React.FC = () => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<API.InterfaceInfo>();
// 使用useMatch钩子将当前URL与指定的路径模式/interface_info/:id进行匹配,
// 并将匹配结果赋值给match变量
const match = useMatch('/interface_info/:id');
// 使用JSON.stringify将match变量的值转换为JSON字符串,并通过alert函数进行弹窗显示
alert(JSON.stringify(match));
const loadData = async (current = 1, pageSize = 5) => {
// setLoading(true);
// }
// try {
// const res = await getInterfaceInfoByIdUsingGET(id);
// setData(res.data);
// } catch (error: any) {
// message.error('请求失败,' + error.message);
// }
// setLoading(false);
};
useEffect(() => {
loadData();
}, []);
return (
<PageContainer title="查看接口文档">
</PageContainer>
);
};
export default Index;
点击查看
。
弹出提示框,并打印出了 match 参数。
之前的 userMatch 可以获取整个页面路径的详细信息,但我们只需要拿动态路由的参数。
这里官网还提供了一个叫做 useParams 的钩子函数,通过使用 useParams,我们可以轻松地获取动态路由中的参数值,而无需关心整个页面路径的其他细节。
继续修改查看接口文档页面。
import { getInterfaceInfoByIdUsingGET } from '@/services/yuapi-backend/interfaceInfoController';
import { PageContainer } from '@ant-design/pro-components';
import { message } from 'antd';
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router';
/**
* 主页
* @constructor
*/
const Index: React.FC = () => {
// 定义状态和钩子函数
const [loading, setLoading] = useState(false);
const [data, setData] = useState<API.InterfaceInfo>();
// 使用 useParams 钩子函数获取动态路由参数
const params = useParams();
const loadData = async () => {
// 检查动态路由参数是否存在
if (!params.id) {
message.error('参数不存在');
return;
}
setLoading(true);
try {
// 发起请求获取接口信息,接受一个包含 id 参数的对象作为参数
const res = await getInterfaceInfoByIdUsingGET({
id: Number(params.id),
});
// 将获取到的接口信息设置到 data 状态中
setData(res.data);
} catch (error: any) {
// 请求失败处理
message.error('请求失败,' + error.message);
}
// 请求完成,设置 loading 状态为 false,表示请求结束,可以停止加载状态的显示
setLoading(false);
};
useEffect(() => {
loadData();
}, []);
return (
<PageContainer title="查看接口文档">
{
// 将 data 对象转换为 JSON 字符串
JSON.stringify(data)
}
</PageContainer>
);
};
export default Index;
修改后,回到前端页面,点击查看
。
现在这个接口信息就获取到了。
3. 美化页面
接下来对查看接口文档页面进行美化,用卡片进行包裹。
回到前端页面查看,好看了一点🐶。
我们把里面的内容换成列表的形式,去 官网 找一下描述列表组件;
复制示例代码。
<Descriptions title="User Info">
<Descriptions.Item label="UserName">Zhou Maomao</Descriptions.Item>
<Descriptions.Item label="Telephone">1810000000</Descriptions.Item>
<Descriptions.Item label="Live">Hangzhou, Zhejiang</Descriptions.Item>
<Descriptions.Item label="Remark">empty</Descriptions.Item>
<Descriptions.Item label="Address">
No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China
</Descriptions.Item>
</Descriptions>
粘贴到查看接口文档页面。
做一个判断,如果接口存在就展示列表,否则显示接口不存在;
title={data.name}
将接口名称作为标题传递给 组件。
接下来填充列表内的数值,光标放在 InterfaceInfo 上,按[Ctrl+鼠标左键]。
看一下接口信息的类型。
根据接口信息,来修改列表,userId 就不需要了,用户不关心是谁发布的,因为我们这个不是共建平台。
<Descriptions title={data.name}>
<Descriptions.Item label="接口状态">{data.status}</Descriptions.Item>
<Descriptions.Item label="描述">{data.description}</Descriptions.Item>
<Descriptions.Item label="请求地址">{data.url}</Descriptions.Item>
<Descriptions.Item label="请求方法">{data.method}</Descriptions.Item>
<Descriptions.Item label="请求头">{data.requestHeader}</Descriptions.Item>
<Descriptions.Item label="响应头">{data.responseHeader}</Descriptions.Item>
<Descriptions.Item label="创建时间">{data.createTime}</Descriptions.Item>
<Descriptions.Item label="更新时间">{data.updateTime}</Descriptions.Item>
</Descriptions>
回到前端页面,点击查看
。
显示接口信息。
再换一个。
接口信息显示正常。
不过这个列表展示还是有点太紧了,我们用参数来控制一下;
使用 column,设置每行展示一条。
回到前端页面查看效果展示。
我们这个状态还是显示数字,设置 1 显示 '开启',0 显示 '关闭'。
回到前端页面查看效果展示。
7. 后端开发申请签名
1. 开发申请签名
现在用户已经能看到这个接口了,也能看到这个接口文档,接下来就要在线调用;
在线调用前必须要分配一个签名,上次我们自己造了一个签名和密钥,看一下数据库。
现在我们可以给每个新注册的用户自动分配一个签名和密钥,去修改一下注册流程:
回到后端 yuapi-backend 项目,找到 UserServiceImpl.java 中的 userRegister。
在插入数据前增加分配 ak、sk,把插入数据变成第四步。
怎么给它随机分配?只要用加密算法生成即可。
然后再把得到的值设置给用户,现在有个问题,我们之前的 user 没有这两个字段,ak、sk 是后来改表加进去的,所以要给 user 扩充点参数。
找到 User.java,补充 ak、sk。
在 UserMapper.xml 也添加上 ak、sk。
现在就可以把得到的值设置给用户,回到 UserServiceImpl.java 中的 userRegister。
重启后端项目。
访问 http://localhost:7529/api/doc.html,然后注册一下。
去数据库里看一下。
注册时自动分配 ak、sk 就搞定了。
2. 创建真实数据
现在用户也有签名了,就可以在线来调用这个接口,回到前端页面创建一个真实一点的数据,之前都是假数据。
鼠标右键选择检查
→网络
,等会看一下请求响应。
填写接口信息,这里的接口就用 yuapi-interface 项目中的 getUserNameByPost,填写完后点击提交
。
ps.这里的请求方法框可以做成下拉框。
上面的 url 可以去后端随机复制一个接口地址,点击 Endpoints 可以查看到接口地址;
选中接口,点击鼠标右键→Generate Request in HTTP Client。
生成客户端请求,然后复制粘贴到前端页面的 url 框中即可。
点击提交后,查看请求,它把双引号内文本进行了转译。
去数据库查看数据,把这条数据放到最上面。
先将第一条数据改为 23。
然后把这条数据 id 修改为 1。
回到前端页面,查看效果。
查看接口文档展示效果,点击查看
。
ps.创建时间、更新时间修改格式,交给大家实现,伙伴匹配系统已经带大家改过了。
3. 补充参数
这里发现接口信息里面没有请求参数,去后端补充一下。
更新下 db.sql 的字段。
右键 interface_info 表,选择 Modify Table。
添加请求参数字段。
interface_info 表就新增了请求参数字段。
把所有和接口信息相关的地方补充上请求参数。
重启后端项目。
回到前端,执行 openapi,重新生成。
在查看接口文档页补充请求参数。
修改列的定义。
把请求头和响应头的 textarea 改为 jsonCode。
回到前端页面,点击修改
。
填写请求参数,我们怎么知道接收什么参数?找到这个接口。
它接收的是 User,光标放到 User 上,按[Ctrl+鼠标左键]查看。
那参数就很简单了,但是这里有个问题,我们这里请求参数难道只写 username 吗?
是不是还有一个类型,所以这里还要约定一个类型,类型的约定交给大家实现。
💡 在线调用中有一个关键点,那就是确定前端向后端发送请求时所需的一些信息,例如请求参数的类型。在开始开发之前,建议大家先不要写代码,而是先思考一下。比如,如果在 Java 中使用了 String 类型,那前端对应的请求参数应该是什么类型呢?这里不用自己定义,因为我们都是用 JSON 来作为请求参数的类型,直接使用 JSON 类型即可。
可以搜一下 JSON 的基本类型。
请求参数的类型(直接用 json 类型,更灵活):
[
{"name": "username", "type": "string"}
]
如果大家在企业中写文档的话,把它写到注释里,或者说写一个例子、实例;
可能会收获同事的感谢🐶
粘贴到请求参数框中。
操作成功,也自动格式化了**(解决了修改按钮 bug 可跳过 01:32:16-01:39:53)**。
ps.如果操作失败,提示 id 为 null,可见上期教程:六、前端项目开发 → 2.实现新建功能 的 修改操作演示。
💡 **由操作错误引出的问题:**当出现像这样的参数传递错误时,我们应该进行一些反思和考虑。对于这种请求头、响应头等信息,我们可以在表单的设计上下功夫。可以将表单改造成每个请求参数单独一行让用户输入,每个请求头也单独一行,然后将这个 JavaScript 对象转换为 JSON 格式,再传递给后台,这样就不会出现换行符等问题了,这有点类似于之前提到的 SQLFather 项目中的表单设计。通过这种方式,我们能够更清晰地管理请求参数和请求头,确保它们的准确性和一致性。
8. 开发在线调用
1. 前端增加调用按钮
模拟假接口的数据就造好了,现在我们来在线调用,在查看接口文档右上角添加调用按钮。
这里可以仿造 Swagger 文档,只不过我们已经有信息了;
只需要多加一个输入框,用来输入请求参数,还有一个发送按钮。
在查看接口文档页的描述列表下新加一个卡片。
官网 找一个现成表单。
复制表单代码。
<Form
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input.Password />
</Form.Item>
<Form.Item name="remember" valuePropName="checked" wrapperCol={{ offset: 8, span: 16 }}>
<Checkbox>Remember me</Checkbox>
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
粘贴到卡片里。
删除表单多余的内容。
<Card>
<Form
name="invoke"
onFinish={onFinish}
>
<Form.Item
label="Username"
name="username"
>
<TextArea />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
</Card>
这里还有一个onFinish
方法,它在用户点击提交按钮后被调用,用于向后台发送请求。将官网上的 onFinish 方法复制过来。
const onFinish = (values: any) => {
console.log('Success:', values);
};
粘贴到这里。
然后我们来修改表单。
<Card>
{/* 创建一个表单,表单名称为"invoke",布局方式为垂直布局,当表单提交时调用onFinish方法 */}
<Form name="invoke" layout="vertical" onFinish={onFinish}>
{/* 创建一个表单项,用于输入请求参数,表单项名称为"userRequestParams" */}
<Form.Item label="请求参数" name="userRequestParams">
<Input.TextArea />
</Form.Item>
{/* 创建一个包裹项,设置其宽度占据 16 个栅格列 */}
<Form.Item wrapperCol={{ span: 16 }}>
{/* 创建调用按钮*/}
<Button type="primary" htmlType="submit">
调用
</Button>
</Form.Item>
</Form>
</Card>
回到前端页面查看效果展示,点击查看
。
在请求参数框中,就可以输入发送给后台的请求了。
这里我有一个小的拓展点,因为我们这个接口它有可能会有很多种不同的请求头。比如,post 请求,它就是这么发送的请求内容,在这里你可以以这种形式输入(如下图所示)。
但如果是 get 请求呢,你会发现它这个地方是给你用了这种的前端,就是每一个属性它得提取出来了作为参数。
2. 调用流程讲解
接下来要实际地让后端去调用这个接口,所以我们要开发一个在线调用的后端:
让我们思考一下如何在后端处理这个在线调用的问题。首先,我们要考虑的是用户的请求是不固定的,每个接口的请求参数和请求头可能都不同。现在的问题是,我们应该如何将这些请求传递给真实的第三方接口呢?
我们目前有三个项目:前端项目(刚刚写的点击调用)、接口平台的后端项目、以及提供给开发者的模拟接口项目。那么现在的问题是,前端点击调用后,是直接请求接口平台后端,再由后端调用模拟接口;还是前端绕过后端,直接调用模拟接口呢?
让我们思考一下这两种方案的优缺点。在设计方案时,我们不应该固定地认为某种方案是最好的,而是要思考每种方案的优点和缺点,然后选择最适合的方案。
实际上,在企业项目中,选择第二种方式是不太可能的。原因在于,如果模拟接口可以直接被调用,那么存在安全风险。通常情况下,前端虽然可以直接调用模拟接口,但我们不会将模拟接口暴露给外部,而是将其隐藏起来。用户或开发者在调用时可能根本不知道模拟接口的地址。假设,模拟接口的地址是 aaa.com/api,后端地址是 bbb.com/api,而 aaa.com/api 并不对用户开放,用户根本不知道它的存在。
为了更规范、更安全,以及方便进行统计,建议使用后端调用的方式。这种方式更加规范和安全,还可以隐藏接口地址。如果直接将模拟接口完全开放给用户,那么后续的网关和计费等工作可能会徒劳无功。因为对方可以直接请求到你的模拟接口。当然,你可能还需要为模拟接口提供一些特殊保障,所以推荐使用第一种方式。
💡 如果是本人测试自己提交的接口是不是第二种也可以?
当然,第二种方式并不是完全行不通的。它的优点在于简单直接,但是使用第二种方式的前提是你必须确保它的安全性。只要你能确保安全性,比如项目只是你个人使用,那么直接使用第二种方式肯定更加方便,所以要根据具体情况来决定选择哪种方式。
我们这里要实现的是第一种方式,即前端在调用接口时,首先将要调用的接口以及请求参数传递给后端,然后后端作为一个中转角色,再向模拟接口发送请求。除了中转功能,后端可能还需要进行一些判断,例如判断前端的测试频率是否过高,或者判断前端是否有权限进行该接口的测试。因此,使用后端作为中转会更加方便。因此,我们选择第一种方式实现。
前端要做的事情,就是把所有它要调用的接口 id 、请求参数传给后端,后端负责调用。
3. 调用流程
- 前端将用户输入的请求参数和要测试的接口 id 发给平台后端
- (在调用前可以做一些校验)
- 平台后端去调用模拟接口
4. 后端开发在线调用
我们首先要确保整个流程能够正常运行,先不做特殊的校验。直接调用接口时,我们可以方便地使用我们之前开发好的客户端,这样调用接口变得非常简单便捷。
4.1 开发测试接口
我们来测试一下,开发一个测试接口:
复制 InterfaceInfoController.java 的下线接口,粘贴到下线接口下面
为了给这个接口新封装一个参数,我们给这个请求参数创建一个对象;
复制 InterfaceInfoUpdateRequest.java,粘贴到 interfaceInfo 目录下,并修改名为InterfaceInfoInvokeRequest
。
然后修改一下参数。
package com.yupi.project.model.dto.interfaceInfo;
import lombok.Data;
import java.io.Serializable;
/**
* 接口调用请求
*
* @TableName product
*/
@Data
public class InterfaceInfoInvokeRequest implements Serializable {
/**
* 主键
*/
private Long id;
/**
* 用户请求参数
*/
private String userRequestParams;
private static final long serialVersionUID = 1L;
}
把刚刚复制的下线接口改为测试接口。
/**
* 测试调用
*
* @param interfaceInfoInvokeRequest
* @param request
* @return
*/
@PostMapping("/invoke")
// 这里给它新封装一个参数InterfaceInfoInvokeRequest
// 返回结果把对象发出去就好了,因为不确定接口的返回值到底是什么
public BaseResponse<Object> invokeInterfaceInfo(@RequestBody InterfaceInfoInvokeRequest interfaceInfoInvokeRequest,
HttpServletRequest request) {
// 检查请求对象是否为空或者接口id是否小于等于0
if (interfaceInfoInvokeRequest == null || interfaceInfoInvokeRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 获取接口id
long id = interfaceInfoInvokeRequest.getId();
// 获取用户请求参数
String userRequestParams = interfaceInfoInvokeRequest.getUserRequestParams();
// 判断是否存在
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
if (oldInterfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 检查接口状态是否为下线状态
if (oldInterfaceInfo.getStatus() == InterfaceInfoStatusEnum.OFFLINE.getValue()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "接口已关闭");
}
}
在用户进行测试调用时,我们需要告知后端用户的签名信息,这样我们才能判断用户是否具有调用接口的权限。
在这里有三种考虑方式,具体取决于你对用户体验的考虑:
- 第一种方式:要求用户必须具有接口权限才能进行调用。
- 第二种方式:即使用户没有权限,也允许其进行调用,以便体验接口功能。如果选择进行体验,建议为用户分配临时的签名,类似于测试环境,给予一定数量的调用次数。这可能需要新增两个字段,例如在数据库中添加一个测试次数字段,稍微复杂一些。
- 第三种方式:可以直接为每个用户提供几十次调用机会,这样就简单了。假设你本来给用户提供了 1 万次调用次数,现在你可以直接给每个用户送 50 次。这样做起来更方便,这种方式非常巧妙。
那我们这里的话就直接用他自己的账号了。
/**
* 测试调用
*
* @param interfaceInfoInvokeRequest
* @param request
* @return
*/
@PostMapping("/invoke")
public BaseResponse<Object> invokeInterfaceInfo(@RequestBody InterfaceInfoInvokeRequest interfaceInfoInvokeRequest,
HttpServletRequest request) {
if (interfaceInfoInvokeRequest == null || interfaceInfoInvokeRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
long id = interfaceInfoInvokeRequest.getId();
String userRequestParams = interfaceInfoInvokeRequest.getUserRequestParams();
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
if (oldInterfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
if (oldInterfaceInfo.getStatus() == InterfaceInfoStatusEnum.OFFLINE.getValue()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "接口已关闭");
}
// 获取当前登录用户的ak和sk,这样相当于用户自己的这个身份去调用,
// 也不会担心它刷接口,因为知道是谁刷了这个接口,会比较安全
User loginUser = userService.getLoginUser(request);
String accessKey = loginUser.getAccessKey();
String secretKey = loginUser.getSecretKey();
// 我们只需要进行测试调用,所以我们需要解析传递过来的参数。
Gson gson = new Gson();
// 将用户请求参数转换为com.yupi.yuapiclientsdk.model.User对象
com.yupi.yuapiclientsdk.model.User user = gson.fromJson(userRequestParams, com.yupi.yuapiclientsdk.model.User.class);
// 调用YuApiClient的getUsernameByPost方法,传入用户对象,获取用户名
String usernameByPost = yuApiClient.getUserNameByPost(user);
// 返回成功响应,并包含调用结果
return ResultUtils.success(usernameByPost);
}
这里发现有个问题,为什么 ak、sk没有用到?
因为 YuApiClient 已经在配置里写死了。
现在我们要根据用户自己的 ak、sk 调用,所以这个地方就不能复用 YuApiClient 了。
这里要新建一个 client,要不然始终用的是管理员的账户、密码来测试,这样肯定不对,也会存在刷量的风险。
欧了~ 现在先重启后端。
回到前端执行 openapi,重新生成文档。
4.2 前端修改调用请求
接下来,我们需要在这个调用的位置添加一个真实的请求后台的功能。
我们可以在用户点击提交表单的时候触发这个功能,也就是我们在 onFinish 方法中进行处理。
修改后的 onFinish:
const onFinish = (values: any) => {
// 检查是否存在接口id
if (!params.id) {
message.error('接口不存在');
return;
}
try {
// 发起接口调用请求,传入一个对象作为参数,这个对象包含了id和values的属性,
// 其中,id 是从 params 中获取的,而 values 是函数的参数
invokeInterfaceInfoUsingPOST({
id: params.id,
...values,
});
message.success('请求成功');
} catch (error: any) {
message.error('操作失败,' + error.message);
}
};
拿到返回值之后,要把这个结果回显出来,需要一个变量来存储,还有调用时的加载状态,来定义新的变量。
继续完善 onFinish 方法。
页面还需要添加展示结果的地方。
来到前端页面,先把接口发布。
回到浏览接口页,点击查看
。
发一个请求,点击调用
,测试一下,测试成功。
美化一下,加个标题和分割线。
回到前端查看效果。
欧了~
那现在我们为什么能调用成功呢?
因为登录的用户恰好是我们之前在开发模拟接口时所创建的用户。在这里,密码并不是从数据库中获取的。在实际情况中,我们应该从数据库中获取密钥,并检查该密钥是否存在于数据库中。但是目前这部分代码逻辑还没有进行修改。因此,如果我们更换用户,只要其密钥不是"abcdefgh",就会导致错误。大家要注意这一点,当然后面我们会一起修改这部分代码逻辑。
9. 优化点
- 判断该接口是否可以调用时由固定方法改为根据测试地址来调用
- 用户测试接口固定方法名改为根据测试地址来调用
- 模拟接口改为从数据库校验 akey
10. 扩展点
- 用户可以申请更换签名(如果用户的签名泄露,用户有权申请更换签名)。
- 先跑通整个接口流程,后续可以针对不同的请求头或者接口类型来设计界面和表单,给用户更好的体验(可以参考 swagger、postman、knife4j)。