项目笔记(三)

一、项目概述

本项目是一个面向开发者的 API 平台,提供 API 接口供开发者调用。用户通过注册登录,可以开通接口调用权限,并可以浏览和调用接口。每次调用都会进行统计,用户可以根据统计数据进行分析和优化。管理员可以发布接口、下线接口、接入接口,并可视化接口的调用情况和数据。本项目侧重于后端,涉及多种编程技巧和架构设计层面的知识。

二、本期时间点

QG60IT2m4PKw8pmu-4946e1e5

三、本期计划(时间点 04:36-09:07)

  1. 开发接口发布 / 下线的功能(管理员) 20 min
  2. 前端去浏览浏览接口、查看接口文档、申请签名(注册) 20 min
  3. 在线调试(用户) 20 min
  4. 统计用户调用接口的次数 20 min (下次一定)
  5. 优化系统 - API 网关 20 min (下次一定)

四、开发接口发布和下线功能(时间点 09:07-37:54)

ps.前端登录页面的手机号登录,其他按钮的删除,交给大家实现。

看看我们之前的接口管理页面(不急着启动,看看下图所示就好)。ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=

l1xzCVtM6qXyBr29-94f23973

这个页面之前是给管理员使用的,现在我们要开发一个用于发布和下线接口的功能。

本质上来说,就是改变每条接口数据的状态。在设计接口信息表时,之前已经预留了一个状态字段status。j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=

pyAcgAJ5IZqTyFmI-8a07f391

其中,关闭和开启分别对应接口的下线和上线。只有状态为 1 的接口才可以被用户调用,否则将无法调用。

现在我们先开发后台,开发完后台之后,开发前端的时间会更灵活一些,建议大家优先把核心功能做出来,不要去纠结那些界面。sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=

1. 功能设计(时间点 10:34-12:50)

1.1 功能设计讲解

现在让我们思考一下需要开发哪些后台接口。那就是发布接口和下线接口。大致规划一下思路:nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=

发布接口:这个接口需要执行哪些任务呢?首先需要验证接口是否存在,然后判断接口是否可调用,否则访问接口都是 404,影响用户体验。接着,如果接口可以调用,我们需要修改数据库中该接口的状态为 1,表示接口已经被发布,状态默认为 0(关闭)。

下线接口:你可以为其新增一个状态字段。例如,使用 1 表示开启,使用 2 表示下线。通过这个新字段,可以清晰地区分接口状态。当状态为 0 时,表示该接口还没有进行任何处理,看大家自己的考虑。我们这里就直接使用 0 和 1 来表示状态,不再添加额外的状态字段,大家可以按照自己的需求进行设计。对于下线接口,校验接口是否存在也是和发布接口类似的,但是下线接口无需判断接口是否可调用。

另外,还需注意的一点是仅管理员可操作这两个接口,这点需要特别注意,防止用户越权操作。以上就是这两个接口的基本设计。y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

接下来, 我们将根据这个设计开始编写代码。整体来说,比较简单。对于业务逻辑等方面,大家可以自行思考。因为所有的业务和系统都是灵活的。我们开发的这个 API 接口开放平台只是一个简单的标准而已。但是具体的业务细节还是需要根据实际情况来进行调整。

1.2 功能设计总结

**后台接口:**MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=

发布接口(仅管理员可操作)

  1. 校验该接口是否存在
  2. 判断该接口是否可以调用
  3. 修改接口数据库中的状态字段为 1

**下线接口(仅管理员可操作)**MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

  1. 校验该接口是否存在
  2. 修改接口数据库中的状态字段为 0

2. 后端项目开发(时间点 12:50-29:22)

2.1 创建发布和下线接口(时间点 12:50-16:47)

把 yuapi-client-sdk 项目和 yuapi-interface 项目,放到 yuapi-backend 项目中,方便操作。

RMo2mMn3fTlMlAqI-4483de45

或者直接拉取🐟的 项目。QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=

kVbsYFSROMfJDVLe-e50c1afe

找到 InterfaceInfoController.java,顺便修改注释为接口管理

53pTRJwg4c4cpddW-23006463

往下滑,复制更新接口。

kY30FUdFocAR4hws-d8cb4262

粘贴两个在此处。sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=

VxUQroWfJla8b3uO-041dacc4

改成发布和下线。

YNln1vh5vMRIii05-c7edd5f9

然后**这两个接口接收什么参数呢?**其实只需接收接口的 id 即可。比如,我想要发布 id 唯一的接口,只需传递这个 id 作为参数即可,这样就可以清楚地表示要对哪个接口进行发布操作。

这里我们新建一个通用的 request:

复制 common 目录下的 DeleteRequest.java,粘贴到 common 目录下,并重命名为IdRequest。dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=

Bn8pcz7KGH26OKQJ-19484b16

IdRequest就是封装了 id 这个参数,没有别的意思,它将一个基本类型封装成一个对象,这样便于我们进行 json 参数传递。

有同学提议:不封装,直接加参数也可以的,但是得去掉 @RequestBody 注解。j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=

🐟建议:还是封装一下比较好,因为这样的话可以使所有的 post 接口保持统一。

回到InterfaceInfoController.java,把这两个接口的接收参数都改一下。

uSRhQlZMXIiv82e8-a6600abd

这两个接口只能管理员能调用,我们要给接口打上注解。

EXPCJ2Saz2rgOnVz-a5b43a14

这个注解是怎么实现的呢?这是🐟自己写的权限校验切面注解,它对应的实现方法在这里。8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

7fnIQLB55ylzEuZe-63fc2dd5

这个功能的原理非常简单:

首先获取当前登录用户的信息,然后判断用户是否具有管理员权限。如果没有权限,就会直接抛出权限异常;如果有权限,就会继续执行后续操作。这种方式被称为 AOP 切面的基本应用之一。对于学习 Spring 的同学来说,这个功能一定要掌握好,它能够帮助你更好地理解和应用 AOP 的概念。nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=

2.2 发布接口业务逻辑(时间点 16:47-28:55)

我们开始编写发布接口的业务逻辑,根据功能设计进行编写代码。

05VudQRopM8wst2D-466440a2

先校验该接口是否存在。

/** * 发布 * * @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,直接使用它就可以了;j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=

复制在 yuapi-interface 项目中的依赖。

AHXAgExyU4agkNBx-c62c541a

粘贴到 yuapi-backend 项目中。j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

bcupyJbUhNXuKcmm-43a148a5

去 application.yml 编写客户端的配置。

2iCmMxkF9YD5n1f5-04ff6df6

然后在 InterfaceInfoController.java 引入客户端的实例。

lUPnhTezhcnr1xow-578e927b

继续编写发布接口的判断该接口是否可以调用。8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=

/** * 发布 * * @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(); }

还有一个小问题需要注意,我们的接口名称都是固定的,而不是通过地址来映射到相应的接口,所以只能通过方法名调用接口。先记录下这个问题,后续开发过程中解决它,本期先走通整个流程(下次一定)。ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=

lZxuEkhQVBCILxLs-b5795e17

这里我们的状态应该是 1,建议大家可以用枚举值来表示。sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

GM6TbnZHmgaW7VwT-341b0c52

复制 PostGenderEnum.java(帖子性别枚举),粘贴到 enums 包下,并重命名为InterfaceInfoStatusEnum(接口信息状态枚举)。

EQLOktnOJ2UMhCgM-ff4fe4e1

修改一下。

JnuyWalanHNv8HJf-fcff56ab

回到 InterfaceInfoController.java 继续编写代码。I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=

/** * 发布 * * @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 对象没有用到,先留着,说不定后面会用到。

yGGUBbsvDIzHE0yW-6c209a69

2.3 下线接口业务逻辑(时间点 28:55-29:22)

接下来我们写一下下线接口的业务逻辑,直接把发布接口的业务逻辑复制粘贴到下线接口中。

GJjMjAfJ91Yht0mm-6885ad51

然后同样的逻辑判断 id 是否存在、判断这个接口是否存在,判断接口是否可以调用就不需要了,把上线的状态改为下线的状态。

8zLQP2Vn3FiJ3EFW-bc2c2cf8

接口已经完成且没有问题,接下来,启动后端项目(记得先启动 redis),无需进行测试,因为这是一个相对简单的功能。j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

zZ8W5D5nKbNh95Ad-551cd2a2

3. 前端项目开发(时间点 29:22-37:54)

3.1 增加发布和下线按钮(时间点 29:22-37:54)

接下来写一下前端,先把所有管理员操作的页面封装到一起;

在 page 目录下新建 Admin 目录,把 InterfaceInfo 目录放进去。

MGUKQvc1Q8WkdXo1-3a400655

然后去 routes.ts 修改组件路径(这样路由和对应的组件路径一样,会规范一点)。

oiskDMWPFIp8A3Wb-acb1ed8c

开始在前端页面的操作区增加发布和下线的按钮。I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

w0y7XELmQQbDt4gp-f13a95b6

找到对应的页面 InterfaceInfo 目录下的 index.tsx,复制删除按钮。

Z9TLrEWBc0D2BMcO-71756f51

粘贴到此处进行修改。

g99vgYcqfIliDJno-26d94939

在终端输入yarn run dev启动前端,访问 http://localhost:8000。I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=

ZujVostOkbHR9yvd-551f1fb3

登录后,查看新增的按钮。

yCDhk5VUzje5NDtV-79b4eeb0

💡 是否可以同时展示发布和下线这两个按钮呢?

最好不要同时展示,如果一个接口已经发布了,我们只展示下线按钮会更好一些。

所以这里要加一个判断,在这个渲染函数中,能拿到当前这条记录的对象,它是一个 record,类型就是我们的接口信息,所以我们可以直接根据 record 的 status 来判断。QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=

hrdFoNWZuXuWB9V3-20fce6f4

回到前端页面,刷新一下,因为我们所有的接口都是未发布,所以现在能看见发布按钮。

NDOhTHYv1zmHRMg3-41fad644

修改下线按钮和删除按钮的颜色,使用 Ant Design Pro 给我们提供的一个文字按钮组件。

3L33PsHzwb9C8WQ8-e57ea77b

回到前端页面,鼠标放到删除按钮,就会变色。dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=

qfZLzEOHg9bSd7pr-ef73b1e5

来实现发布和下线按钮的功能。大家还记得怎么调用后端接口的函数?

现在新加了两个接口,前端应该怎么办?ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=

利用 openapi 生成即可,前端不需要写任何接口调用的方法,在终端执行yarn run openapi

ltzsBgnoZy0MeJPB-5a274e94

然后直接复制删除节点的方法,粘贴到删除节点上面。ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=

yH7mRhYUeeZSVsC7-5adb5a60

修改成发布节点。

/** * 发布接口 * * @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; } };
KRsPUzmw6StuBAAu-e1e6b03e

复制发布节点的方法,粘贴到发布节点下面,然后修改成下线节点。

p6OfiVzmG5oaV5VJ-6a1b3f3a
/** * 下线接口 * * @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; } };

把发布和下线按钮改成调用对应的函数。

Fc91E3yF7Ivb3sS8-5adc983f

idea 另开一个窗口,打开 yuapi-interface 项目运行。

rbSVU2nMcPBCVAAW-93c36216

yuapi-interface 项目如果没有运行,点击发布按钮会弹出未连接。sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

INyNYxrvagOedohT-79b00574

yuapi-interface 项目是模拟接口项目,我们开发的主要是后台管理系统和用户前台,但模拟接口也要启动,可见之前设计的流程图。MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

PHs0DY7gmvlr1qn0-e7ffc99c

回到前端页面试一下,点击发布按钮,提示操作成功,状态变为开启,并显示了下线按钮。MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=

mRaSDN7kMUbHWwLT-6f41aeb3

点击下线按钮,提示操作成功,状态变为关闭,并显示了发布按钮。

AZSDNN9xfR1Eon2E-93a86b03

3.2 解决按钮同时显示的问题

这时你发现,诶,我们之前不是写了不能同时显示发布和下线按钮吗?

虽然说刷新之后显示正常,但这还是太怪了,有两种方法:MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=

  1. 设置不同的 key
  • 发布和下线的按钮都有 key 属性,默认是 config,设置成不一样的即可,原因是,在 React 中,如果列表中的元素需要进行增删操作,需要为每个元素指定唯一的 key。这是因为 React 使用 key 来识别列表中的每个元素,以便在更新列表时能够准确地判断新旧元素的对应关系,从而提高渲染性能。
  • 给发布按钮和下线按钮设置不同的 key 是解决重复渲染问题的一种方法。通过设置不同的 key 值,React 在进行新旧对比时会识别出这是两个不同的元素,从而确保它们能够正确地渲染和更新。

所以,可以通过给发布按钮和下线按钮设置不同的 key 值来解决按钮重复渲染的问题,确保 React 能够正确地进行虚拟 DOM 的对比和更新。QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=

Ptpm1QECZWLm3eTl-68b96134
  1. 修改条件渲染逻辑

代码中是通过条件来渲染按钮,重新修正代码:sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

  • 首先判断状态是否为 0,如果是 0,则渲染发布按钮;
  • 如果状态为 1,则渲染下线按钮;其他状态的情况下,不进行任何渲染。

这样可以根据状态变化正确渲染对应的按钮,并且不会出现重复按钮的问题。

Xq1tkKxBzSdAGOTN-a4f7810b

使用以上两种方法都能解决渲染问题。

然后我们点击发布按钮,页面显示下线按钮,发布按钮也被隐藏。

PX7NVzkJHnLOKcdY-5f3fd0d9

点击下线按钮,显示发布按钮,不会显示多个下线按钮,下线按钮被隐藏了。

eXo9KHp3XubV7iza-769b1d82

搞定咯(~ ̄▽ ̄)~QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=

五、前端开发浏览接口页(时间点 37:54-54:51)

1. 创建浏览接口页(时间点 37:54-40:26)

现在要新建一个给用户看的页面,之前我们是没有主页的。sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

删掉 Admin.tsx,在 pages 目录下新建Index目录。

3YifLVIV3YrlLyWs-288b295d

把 Welcome.tsx 拖到 Index 目录下,并修改名为index.tsx。sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

PJlWgxCR8SXlYNcp-96ad321b

去 route.ts 修改路由,取消掉之前欢迎页的注释,然后把它放到最上面。

9Qv9e5yViiSVuwrk-4320d3ba

修改路径、名称、组件路径。

Ee8xEvOY6sbuGl9b-7351906c

访问 http://localhost:8000,主页就展示出来,它默认提供了这些东西:了解 umi、了解 ant design...8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=

DS12VAdRyRqCOSva-536c2cdd

我们不需要这些,改成我们自己的浏览接口页。

2. 修改浏览接口页(时间点 40:26-54:51)

回到主页,删除多余的内容,留下 PageContainer。

  • PageContainer:是 Ant Design Pro 中提供的一个组件,用于快速构建页面的容器。它提供了一些常用的布局和功能,例如面包屑导航、页面标题、操作区域等,可以帮助我们快速搭建页面的基本结构。
YshpgO0X3gl8I1GB-6b878cbf
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;

回到前端页面查看修改情况。

sykaKQ8qigvvg23V-8b6de20e

直接让浏览接口页像管理页一样,将所有的接口以列表形式展示给用户;j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=

ant design 组件库找一下列表(list)组件。

S09YkfyJFnP8BIKK-b68a027f

选一个精简一点的,然后点击显示代码按钮。nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=

VxVxbPgF47KoCigc-f8072dd0

找到示例代码。

2Pglb8rvMFpPDxwY-24edf2eb
<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> )} />

放到这里。

NuFi9UXec76SYPWI-15962282

这里还有什么页面的加载状态(loading),去ant design 组件库复制。

hk3Rv8EIvUsb2pvC-b8673628
const [initLoading, setInitLoading] = useState(true); const [loading, setLoading] = useState(false); const [data, setData] = useState<DataType[]>([]); const [list, setList] = useState<DataType[]>([]);

粘贴到主页中。

ajq9395bDUTJiHG6-282398ec

然后进行修改。MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=

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 的设置,也就是多次使用重复的数字。这里需要记住的是,当我们在代码中使用魔法值(即凭空出现的数值)时,如果我们发现需要在多个地方修改这个数值,建议将其提取为一个常量。

**为什么要提取为常量呢?**因为使用常量可以提高代码的可维护性和可读性。通过将魔法值提取为常量,然后就可以修改常量的值来一次性修改多处使用该值的地方,而不必逐个搜索和修改每个出现的地方。nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=

HBtgvmizf5VLs5Ko-bc0ffbbf

修改好后,回到前端页面,查看浏览接口页面的效果展示。

h7DS6lep7WzR4aDj-0339d9fd

点击页数,查看数据是否正常显示。

0Zy8P9MTLIyluB5u-3b1aa04a

六、前端开发接口文档页(时间点 54:51-01:10:30)

1. 创建浏览接口文档页(时间点 54:51-01:00:29)

复制 Index 目录,粘贴到 page 目录下,并重命名为InterfaceInfo

jnl2FINCi8g8mGKj-1c67e606

去 route.ts 添加路由,动态路由可以查看官网 —— UmiJS,给这个路由新增两个参数:

  • 让这个路由可以接收动态参数,在点击查看之后可以跳到对应的接口页面,通过 id 来区分不同的接口。
    • path: '/interface_info/:id':定义了路由的路径,其中 :id 是一个参数占位符,表示在实际路径中可以传递一个具体的ID值作为参数。
  • 让这个页面在菜单栏中隐藏,查看接口不需要放在菜单栏上。
    • hideInMenu: true:表示该路由在菜单中是否隐藏,如果设置为 true,则该路由不会在菜单中显示。
Gi2QRySE53069ni9-ce4a472c

现在我们要做的事情是点击查看之后,读取到这条数据的 id,然后跳到对应的页面。

先去修改超链接。

KuLRHsjLg7aOMoQv-fc7cc1e1

回到前端页面,点击查看,可以看见地址栏上的 url 变了,它已经跳转到查看接口文档页面。

9MbMItTiRcezvPUS-e7e3be9c访问 http://localhost:8000,点击列表项,也能正常跳转。

pLW68E6OWjigwRtt-a67f4824

2. 修改浏览接口文档页(时间点 01:00:29-01:04:51)

现在来改一下查看单个接口文档的页面,需要从 url 参数中拿到这个 id,然后才知道要加载哪个接口信息。

**怎么拿到动态路由的参数呢?**查看官网 —— UmiJS。8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=

OW3kpOxhktVyFQoZ-bea3d98d

这里就使用 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;

点击查看

UBnt8lgyQykJhmA0-309946b6

弹出提示框,并打印出了 match 参数。dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=

7dCCYUqQN0zfurpV-86841d6f

之前的 userMatch 可以获取整个页面路径的详细信息,但我们只需要拿动态路由的参数。

这里官网还提供了一个叫做 useParams 的钩子函数,通过使用 useParams,我们可以轻松地获取动态路由中的参数值,而无需关心整个页面路径的其他细节。sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=

hrVW7eAtsAfTTFEC-bc52e162

继续修改查看接口文档页面。

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;

修改后,回到前端页面,点击查看

KwJ47jsNj4r7SL4I-ba75cb91

现在这个接口信息就获取到了。QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=

aBCdtSrjxtnjXlPp-ddbbad22

3. 美化页面(时间点 01:04:51-01:10:30)

接下来对查看接口文档页面进行美化,用卡片进行包裹。ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

xlJF6lDEfJ7Faqgy-45fdf10a

回到前端页面查看,好看了一点🐶。

vrbda9kaRbIzNc01-629e631b

我们把里面的内容换成列表的形式,去 官网 找一下描述列表组件;

这里就用最基本的展示,点击 q2ONBMbYJnBWzgP0-975e3935显示代码按钮。

vvg5rGgAe1ZIPKN5-5b4c862c

复制示例代码。

Ts1XQlLWUmpBWo6G-f78b3caa
<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>

粘贴到查看接口文档页面。

enR2Sjc558wf87Vj-cf9e8bb2

做一个判断,如果接口存在就展示列表,否则显示接口不存在;sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=

title={data.name}将接口名称作为标题传递给 组件。

FzNRbE9IJtIyLvNZ-d68b2ad5

接下来填充列表内的数值,光标放在 InterfaceInfo 上,按[Ctrl+鼠标左键]。8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=

NKrKwGtkgFhs10oS-ff067ac7

看一下接口信息的类型。

ZfWQSjxTmKH7FdP3-08b2de2e

根据接口信息,来修改列表,userId 就不需要了,用户不关心是谁发布的,因为我们这个不是共建平台。

FRtbg54rjQV3fMU6-19e6b905
<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>

回到前端页面,点击查看

8HhK6B6wRAJKxzEi-18b9c7dc

显示接口信息。y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=

JEQojhrx6TFNs482-f5996f20

再换一个。

NYYMNFTwPAV5xms1-515c56b0

接口信息显示正常。

LKMx9vKFFJgQoL6I-37aa6553

不过这个列表展示还是有点太紧了,我们用参数来控制一下;QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=

使用 column,设置每行展示一条。

a3eE3a9mrfbLyyHL-af6dbe43

回到前端页面查看效果展示。nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=

YLsguQABUKk8ebwg-6dc0ffa3

我们这个状态还是显示数字,设置 1 显示 '开启',0 显示 '关闭'。

0azhbfK1KQz0saxC-614adc73

回到前端页面查看效果展示。

JWt8XzrwvFFjRf2Z-48d7245f

七、后端开发申请签名(时间点 01:10:30-01:28:38)

1. 开发申请签名(时间点 01:10:30-01:19:29)

现在用户已经能看到这个接口了,也能看到这个接口文档,接下来就要在线调用;

在线调用前必须要分配一个签名,上次我们自己造了一个签名和密钥,看一下数据库。QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=

oPVJ99rR5LQaYlrK-c7d9fa8c

现在我们可以给每个新注册的用户自动分配一个签名和密钥,去修改一下注册流程:

回到后端 yuapi-backend 项目,找到 UserServiceImpl.java 中的 userRegister。I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=

Swf34zO4mD1vWif7-aa30c8b3

在插入数据前增加分配 ak、sk,把插入数据变成第四步。

怎么给它随机分配?只要用加密算法生成即可。y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=

BE4whJ1ChyufbBae-4e2f8fea

然后再把得到的值设置给用户,现在有个问题,我们之前的 user 没有这两个字段,ak、sk 是后来改表加进去的,所以要给 user 扩充点参数。

找到 User.java,补充 ak、sk。ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

k97jkxRJoT2qlPd0-35eb900e

在 UserMapper.xml 也添加上 ak、sk。

3Nz47DMg8QPR497H-48fbceaf

现在就可以把得到的值设置给用户,回到 UserServiceImpl.java 中的 userRegister。

XMlhnfiCvmoYVYnM-e4644db5

重启后端项目。MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=

jgrYg2IWAsCFWMIf-c8c949a4

访问 http://localhost:7529/api/doc.html,然后注册一下。

eCkUTqY7Wk5hhX5k-e455eb7f

去数据库里看一下。

gIya5ZWL8fCElJlR-37b80c84

注册时自动分配 ak、sk 就搞定了。MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

2. 创建真实数据(时间点 01:19:29-01:23:58)

现在用户也有签名了,就可以在线来调用这个接口,回到前端页面创建一个真实一点的数据,之前都是假数据。

w7WDumFRUXnNMm8g-e4734b03

鼠标右键选择检查网络,等会看一下请求响应。

SZcWDacZHh93ErL6-614760d4

填写接口信息,这里的接口就用 yuapi-interface 项目中的 getUserNameByPost,填写完后点击提交。dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=

ps.这里的请求方法框可以做成下拉框。

jv8B5H3XQnOoZX7u-a6466403

上面的 url 可以去后端随机复制一个接口地址,点击 Endpoints 可以查看到接口地址;

选中接口,点击鼠标右键→Generate Request in HTTP Client。

HDu1t7ofCRljIs2J-e81c230b

生成客户端请求,然后复制粘贴到前端页面的 url 框中即可。

CnQwuPHzYqJyFKya-6356f038

点击提交后,查看请求,它把双引号内文本进行了转译。

C6DZKsX4SFqjyKEM-314d2b44

去数据库查看数据,把这条数据放到最上面。QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=

ol3rb3h1fo4ExsAh-8a4981c3

先将第一条数据改为 23。

Ru4a8wQSODTViAp7-024b26c1

然后把这条数据 id 修改为 1。

vJMiTVN6DKzXs0Cw-9ca47f6c

回到前端页面,查看效果。y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=

kWMRW8D76gtyGc40-8021f95b

查看接口文档展示效果,点击查看

Toi86BQOzcbl3d4Z-c7b01af3 swC6PwncG8Rnn7RR-8817c45b

ps.创建时间、更新时间修改格式,交给大家实现,伙伴匹配系统已经带大家改过了。

3. 补充参数(时间点 01:24:04-01:28:38)

这里发现接口信息里面没有请求参数,去后端补充一下。

swC6PwncG8Rnn7RR-8817c45b

更新下 db.sql 的字段。ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=

ZvscjwmvCLbZYAFq-ce31a728

右键 interface_info 表,选择 Modify Table。

FjnqZ0dOwalj5We1-0a7f5b12

添加请求参数字段。

GbjRxtf5Q17QlhVh-eec06793

interface_info 表就新增了请求参数字段。dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

DKnryelQBuRw8OiM-3a69e81b

把所有和接口信息相关的地方补充上请求参数。

sj5nvgi1P7LWOrmu-041bf3a0 dAAdv01rgHUAx0kH-27561baa ywMlXlGOYdVVZmCh-b70cfd42 F3VbP7OnBeQ1VGtE-fc72431a

重启后端项目。

Ps6ObWqoyEWTvhYN-cf836e2d

回到前端,执行 openapi,重新生成。8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=

BHBbFCCL71CIlBap-d3d6b448

在查看接口文档页补充请求参数。

tqzCbzwAo1ZzBFXu-4952516a

修改列的定义。

uByjtT6x5NaLBlch-94a18953

把请求头和响应头的 textarea 改为 jsonCode。y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=

6HhkgfD6hVeEhk5E-d5c6b2a5

回到前端页面,点击修改

9ucg5FtqE6UV0ADh-5dfb2fb1

填写请求参数,我们怎么知道接收什么参数?找到这个接口。

8O5p8COKqDDxJvIt-9795a184

它接收的是 User,光标放到 User 上,按[Ctrl+鼠标左键]查看。I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=

vjpAD1H2KpkwdDie-630b58fd

那参数就很简单了,但是这里有个问题,我们这里请求参数难道只写 username 吗?

是不是还有一个类型,所以这里还要约定一个类型,类型的约定交给大家实现。MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=


💡 在线调用中有一个关键点,那就是确定前端向后端发送请求时所需的一些信息,例如请求参数的类型。在开始开发之前,建议大家先不要写代码,而是先思考一下。比如,如果在 Java 中使用了 String 类型,那前端对应的请求参数应该是什么类型呢?这里不用自己定义,因为我们都是用 JSON 来作为请求参数的类型,直接使用 JSON 类型即可。

可以搜一下 JSON 的基本类型。y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=

XelRcYhsu7BIr4DB-2c1e2c77

请求参数的类型(直接用 json 类型,更灵活):nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=

[ {"name": "username", "type": "string"} ]

如果大家在企业中写文档的话,把它写到注释里,或者说写一个例子、实例;

可能会收获同事的感谢🐶MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

RbZK8oLZDvwcX5jL-303bf77b

粘贴到请求参数框中。

pyWbB1NZ5f6cprOU-7304cc4b

操作成功,也自动格式化了**(解决了修改按钮 bug 可跳过 01:32:16-01:39:53)**。

ps.如果操作失败,提示 id 为 null,可见上期教程:六、前端项目开发 → 2.实现新建功能修改操作演示

NYbuC8r0fPKDFb0b-2bd60b7c

💡 **由操作错误引出的问题:**当出现像这样的参数传递错误时,我们应该进行一些反思和考虑。对于这种请求头、响应头等信息,我们可以在表单的设计上下功夫。可以将表单改造成每个请求参数单独一行让用户输入,每个请求头也单独一行,然后将这个 JavaScript 对象转换为 JSON 格式,再传递给后台,这样就不会出现换行符等问题了,这有点类似于之前提到的 SQLFather 项目中的表单设计。通过这种方式,我们能够更清晰地管理请求参数和请求头,确保它们的准确性和一致性。

fHLQ96kgkakVxPn5-c4dd1210

八、开发在线调用(时间点 01:40:20-02:15:07)

1. 前端增加调用按钮(时间点 01:40:20-01:47:30)

模拟假接口的数据就造好了,现在我们来在线调用,在查看接口文档右上角添加调用按钮。sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

gmMUtFP8Ksmo6jE9-164a11d7

这里可以仿造 Swagger 文档,只不过我们已经有信息了;

只需要多加一个输入框,用来输入请求参数,还有一个发送按钮。ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=ZQrfE/sCxk5l2HPzrk0ji4jR2Ph837ekIgMbdJHkyJI=

YamNTOseZWuTzuck-5d3a3ad4

在查看接口文档页的描述列表下新加一个卡片。

57CfLCn7Pua5tiOj-8b304810

官网 找一个现成表单。

euPPsYjp2pKmjKjj-d60e36f5

复制表单代码。MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=

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

粘贴到卡片里。

RvNHpPArw3e7LLp3-c6271349

删除表单多余的内容。

m6zo26xIGjYmWgJh-c3863bc1
<Card>  <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>  </Card>

这里还有一个onFinish方法,它在用户点击提交按钮后被调用,用于向后台发送请求。将官网上的 onFinish 方法复制过来。

ETs8KrE8B6tCJun8-d802ceaf
const any) => { console.log('Success:', values); };

粘贴到这里。

1m965Pc0iM8b9pOu-dc4001d3

然后我们来修改表单。nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

VGk3yzj5B7u5Cs8y-c829275e
<Card> {/* 创建一个表单,表单名称为"invoke",布局方式为垂直布局,当表单提交时调用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>  </Card>

回到前端页面查看效果展示,点击查看。QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

3beabrPlY9ooYZSo-1c11c8b7

在请求参数框中,就可以输入发送给后台的请求了。

hJjNC9dIssLXhm7M-282da5a1

这里我有一个小的拓展点,因为我们这个接口它有可能会有很多种不同的请求头。比如,post 请求,它就是这么发送的请求内容,在这里你可以以这种形式输入(如下图所示)。

xY0QDeqL9lB5vjXH-60457e48

但如果是 get 请求呢,你会发现它这个地方是给你用了这种的前端,就是每一个属性它得提取出来了作为参数。dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=

OyNF4BXbMGtHp21y-07221cff

2. 调用流程讲解(时间点 01:47:30-01:54:12)

接下来要实际地让后端去调用这个接口,所以我们要开发一个在线调用的后端:I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=

让我们思考一下如何在后端处理这个在线调用的问题。首先,我们要考虑的是用户的请求是不固定的,每个接口的请求参数和请求头可能都不同。现在的问题是,我们应该如何将这些请求传递给真实的第三方接口呢?

我们目前有三个项目:前端项目(刚刚写的点击调用)、接口平台的后端项目、以及提供给开发者的模拟接口项目。那么现在的问题是,前端点击调用后,是直接请求接口平台后端,再由后端调用模拟接口;还是前端绕过后端,直接调用模拟接口呢?

让我们思考一下这两种方案的优缺点。在设计方案时,我们不应该固定地认为某种方案是最好的,而是要思考每种方案的优点和缺点,然后选择最适合的方案。j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=

Ry2H11WYnCTRPzpD-aa82b44b

实际上,在企业项目中,选择第二种方式是不太可能的。原因在于,如果模拟接口可以直接被调用,那么存在安全风险。通常情况下,前端虽然可以直接调用模拟接口,但我们不会将模拟接口暴露给外部,而是将其隐藏起来。用户或开发者在调用时可能根本不知道模拟接口的地址。假设,模拟接口的地址是 aaa.com/api,后端地址是 bbb.com/api,而 aaa.com/api 并不对用户开放,用户根本不知道它的存在。

为了更规范、更安全,以及方便进行统计,建议使用后端调用的方式。这种方式更加规范和安全,还可以隐藏接口地址。如果直接将模拟接口完全开放给用户,那么后续的网关和计费等工作可能会徒劳无功。因为对方可以直接请求到你的模拟接口。当然,你可能还需要为模拟接口提供一些特殊保障,所以推荐使用第一种方式。nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=

QTjcHKyIU5WGqUaL-f61274e2

💡 **如果是本人测试自己提交的接口是不是第二种也可以?**dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

当然,第二种方式并不是完全行不通的。它的优点在于简单直接,但是使用第二种方式的前提是你必须确保它的安全性。只要你能确保安全性,比如项目只是你个人使用,那么直接使用第二种方式肯定更加方便,所以要根据具体情况来决定选择哪种方式。


我们这里要实现的是第一种方式,即前端在调用接口时,首先将要调用的接口以及请求参数传递给后端,然后后端作为一个中转角色,再向模拟接口发送请求。除了中转功能,后端可能还需要进行一些判断,例如判断前端的测试频率是否过高,或者判断前端是否有权限进行该接口的测试。因此,使用后端作为中转会更加方便。因此,我们选择第一种方式实现。8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=

前端要做的事情,就是把所有它要调用的接口 id 、请求参数传给后端,后端负责调用。

3. 调用流程(时间点 01:54:12-01:54:46)

  1. 前端将用户输入的请求参数和要测试的接口 id 发给平台后端
  2. (在调用前可以做一些校验)
  3. 平台后端去调用模拟接口

4. 后端开发在线调用(时间点 01:54:46-02:15:07)

我们首先要确保整个流程能够正常运行,先不做特殊的校验。直接调用接口时,我们可以方便地使用我们之前开发好的客户端,这样调用接口变得非常简单便捷。

4.1 开发测试接口(时间点 01:54:46-02:04:54)

我们来测试一下,开发一个测试接口:

复制 InterfaceInfoController.java 的下线接口,粘贴到下线接口下面。

cD3l5mt0UheMIIgW-e836a1f1

为了给这个接口新封装一个参数,我们给这个请求参数创建一个对象;

复制 InterfaceInfoUpdateRequest.java,粘贴到 interfaceInfo 目录下,并修改名为InterfaceInfoInvokeRequest

vv8hqRshZWTwPJt6-a6604982

然后修改一下参数。

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

把刚刚复制的下线接口改为测试接口。j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=MHQxthdjzFhIWjS4Nn+sBZ3SJfic4DfvRwDIcKLFTjo=

/** * 测试调用 * * @param interfaceInfoInvokeRequest * @param request * @return */ @PostMapping("/invoke") // 这里给它新封装一个参数InterfaceInfoInvokeRequest // 返回结果把对象发出去就好了,因为不确定接口的返回值到底是什么 public BaseResponse 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, "接口已关闭"); } }

在用户进行测试调用时,我们需要告知后端用户的签名信息,这样我们才能判断用户是否具有调用接口的权限。

在这里有三种考虑方式,具体取决于你对用户体验的考虑:dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=

  • 第一种方式:要求用户必须具有接口权限才能进行调用。
  • 第二种方式:即使用户没有权限,也允许其进行调用,以便体验接口功能。如果选择进行体验,建议为用户分配临时的签名,类似于测试环境,给予一定数量的调用次数。这可能需要新增两个字段,例如在数据库中添加一个测试次数字段,稍微复杂一些。
  • 第三种方式:可以直接为每个用户提供几十次调用机会,这样就简单了。假设你本来给用户提供了 1 万次调用次数,现在你可以直接给每个用户送 50 次。这样做起来更方便,这种方式非常巧妙。

那我们这里的话就直接用他自己的账号了。

/** * 测试调用 * * @param interfaceInfoInvokeRequest * @param request * @return */ @PostMapping("/invoke") public BaseResponse 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没有用到?

XLrdswc7L12wDMYD-0c6697a8

因为 YuApiClient 已经在配置里写死了。y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

MfbHXonL6H7vy9GQ-d1191b50

现在我们要根据用户自己的 ak、sk 调用,所以这个地方就不能复用 YuApiClient 了。

W9yRoCo9QJyPd00O-fcee8356

这里要新建一个 client,要不然始终用的是管理员的账户、密码来测试,这样肯定不对,也会存在刷量的风险。

vY6nWnobjU6oKWLo-6a13e82b

欧了~ 现在先重启后端。I/mv7JqPu0KdBgBsR4rkFWPOT/qJ5Yd7ZLlmeKodCQ4=dlIMZh5jzCLjaFePUcXqKAODVjw+KvlmqOtoVYi2eV8=

NmoYmJFBz0X2PXOo-08b62774

回到前端执行 openapi,重新生成文档。

cuhoSk0lW1OuHSY6-88ac3fc7

4.2 前端修改调用请求(时间点 02:04:54-02:15:07)

接下来,我们需要在这个调用的位置添加一个真实的请求后台的功能。

我们可以在用户点击提交表单的时候触发这个功能,也就是我们在 onFinish 方法中进行处理。y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=sZkzRi3MTD+un8ei2gjakRryTSmKof47t4VavacNfrE=

sQP3FzLG4v5OLMP2-fbd7fb78

修改后的 onFinish:

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

拿到返回值之后,要把这个结果回显出来,需要一个变量来存储,还有调用时的加载状态,来定义新的变量。

1i2FLYtVeNhkFYXU-43cd6052

继续完善 onFinish 方法。j3nSzs+NO5SqQKOeNTdarf4VPxB+8MbZdVOU86YsPf4=y0KKJH83GHzBF93m63zbbBynhjavwWswKIbe+8lmuns=

Zppwf9MC9WAEHeTv-fe076212

页面还需要添加展示结果的地方。

6RAvyXVcK3EZcOKZ-85664f7e

来到前端页面,先把接口发布。

J1lPzuKjCh1PN0uq-6e9ef1c9 Z3OcMP1dAeLkUSYO-b1ef529c

回到浏览接口页,点击查看

2DpPVM4D1g7bIWSt-6c4dc3f2

发一个请求,点击调用,测试一下,测试成功。8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=

ALBJoONtxcjMGJns-61814c4c

美化一下,加个标题和分割线。

7gTf6b1kH5fSx75q-37ef4c20

回到前端查看效果。

jAitj3zUEiGUabFL-ccae40c8

欧了~8t5+VJlIX2Lzofl38AYV5bnrc6jCeMh9kFLnSW4W2fY=QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=

那现在我们为什么能调用成功呢?

因为登录的用户恰好是我们之前在开发模拟接口时所创建的用户。在这里,密码并不是从数据库中获取的。在实际情况中,我们应该从数据库中获取密钥,并检查该密钥是否存在于数据库中。但是目前这部分代码逻辑还没有进行修改。因此,如果我们更换用户,只要其密钥不是"abcdefgh",就会导致错误。大家要注意这一点,当然后面我们会一起修改这部分代码逻辑。

knvdrEo60QPX5TDv-4bca6228

九、优化点

  1. 判断该接口是否可以调用时由固定方法改为根据测试地址来调用
  2. 用户测试接口固定方法名改为根据测试地址来调用
  3. 模拟接口改为从数据库校验 akey

十、扩展点

  1. 用户可以申请更换签名(如果用户的签名泄露,用户有权申请更换签名)。
  2. 先跑通整个接口流程,后续可以针对不同的请求头或者接口类型来设计界面和表单,给用户更好的体验(可以参考 swagger、postman、knife4j)。

QFU7mhNWUEojH1P/mbZkTNJwHPK/zMECeOY+OMwV/3U=nxygd+ImutUjNSlTu1uMISbMTMWPTt4PJRfqBmRyZn8=


项目分区导航:⬅️ 09-项目笔记(七) - API 开放项目项目教程 - 编程导航教程 | 10-项目笔记(三) | ➡️ 11-项目笔记(二)