03

1. 项目概述

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

2. 本期时间点

53256944e5834540b71a4fc1e00607be

3. 本期计划

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

4. 开发接口发布和下线功能

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

看看我们之前的接口管理页面(不急着启动,看看下图所示就好)。

2665fc522425418caed41bec7d463a35

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

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

854f2912c36342789fcf63a59b9221f8

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

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

1. 功能设计

1. 功能设计讲解

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

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

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

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

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

2. 功能设计总结

后台接口:

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

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

下线接口(仅管理员可操作)

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

2. 后端项目开发

1. 创建发布和下线接口

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

f8913ae37b574d75b8d4ed27d50ed8ea

或者直接拉取🐟的 项目open in new window

ed80fd6c21fe48c69bb8604d906c6ebf

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

3b22796f00b04fce9e489012b27760b2

往下滑,复制更新接口。

891f6069183e4f039c184dd2019ecb7a

粘贴两个在此处。

d210b067aebd4d7bbf829940bf00203f

改成发布和下线。

7456dd623f914479982d1db29c4df806

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

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

复制 common 目录下的 DeleteRequest.java,粘贴到 common 目录下,并重命名为IdRequest

a9eacd800cb04b3eac6f2a93ed9f9396

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

有同学提议:不封装,直接加参数也可以的,但是得去掉 @RequestBody 注解。

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

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

703dad8767d6468fb8103ebfbf661589

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

a0f1be4a04c3437d9b0aeb231946a9eb

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

f26c23dcafcb41478ee10f33658d58fc

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

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

2. 发布接口业务逻辑

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

99bdf4828b5c443f9646ce36f3036a86

先校验该接口是否存在。

/**
 * 发布
 *
 * @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 项目中的依赖。

c0e908963b174f338355ed79844a0019

粘贴到 yuapi-backend 项目中。

b5f502ec0c64415a8cf8da3ca9ee2efb

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

c27d9aec43284678a94f8b2b56bf8a39

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

c0644de1b2614249b118f037e01842fc

继续编写发布接口的判断该接口是否可以调用。

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

}

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

e85f7dfc11ae47cd910ab02e37d7e731

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

8cafea4994704a99849afbef17ac8ceb

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

61ffa2615a2249eca27e3d923584f83d

修改一下。

2cde490782424659b562cde712703503

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

975203e4a5ee420f97746b380e5d5003

3. 下线接口业务逻辑

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

13f7c970db184282a806535ef2c87bff

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

aff6c9aa7f86499295d0d4dc2ac7b6dc

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

c1a674138908449f9dee4d3c973aa296

3. 前端项目开发

1. 增加发布和下线按钮

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

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

4987a5ba2bcc4ec0a6755320dec71857

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

8b2150696ce049319e75e7b2135474fa

开始在前端页面的操作区增加发布和下线的按钮。

89c0a29c0a7641b5a94068a284ea40e3

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

129170ac9f474694bf3767df7d4c8307

粘贴到此处进行修改。

7171ea0ff8ab4415aa2cc747cf3f05c5

在终端输入yarn run dev启动前端,访问 http://localhost:8000。open in new window

150a4b8923bd4503b4880ed1d1f7934e

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

0860cd186a4141cb8d9e3603af52b67f

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

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

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

67d9e546fb924e2095c787e528f56d82

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

c8cc4aa603c04ea6b575a2b3235ca2ec

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

5f933822221948d494298b883d7f64ca

回到前端页面,鼠标放到删除按钮,就会变色。

fad4ef73631a4ef197e843816cf644a0

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

现在新加了两个接口,前端应该怎么办?

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

50630b8b11204084b5ffd27aabb86b5e

然后直接复制删除节点的方法,粘贴到删除节点上面。

4d54768ab9044aa89e2cf10e13bce747

修改成发布节点。

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

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

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

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

5c295e3919b741b4b23258917294125d

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

78686de0ff5740ff98d84f31c8f18839

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

662be554091a4dcaa2d6862bff05feea

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

46e8a55296004b53a9f21000bd6d8e58

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

dd0fd912b0d94b58a04c875c2097c389

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

2780c0ca41e94b94a63468cf0eb5325e

2. 解决按钮同时显示的问题

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

虽然说刷新之后显示正常,但这还是太怪了,有两种方法:

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

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

0a9dff23a76843a4bdf011f68de69f25
  1. 修改条件渲染逻辑

代码中是通过条件来渲染按钮,重新修正代码:

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

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

b528fbccde354834b975216130e0656e

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

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

d13aa3605b534522867b95fa7aaf195e

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

76428c87ef5a4638ab66b107b93010ad

搞定咯(~ ̄▽ ̄)~

5. 前端开发浏览接口页

1. 创建浏览接口页

现在要新建一个给用户看的页面,之前我们是没有主页的。

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

24ae9e335e6747d0b3eaad3e5134b70f

把 Welcome.tsx 拖到 Index 目录下,并修改名为index.tsx

e783015dd37844b5bb44734c7c42b884

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

84626b690c474de9b16e4e2b8e06ea88

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

c5a2bd6dfe5f40aa84712cefafc19fd3

访问 http://localhost:8000,主页就展示出来,它默认提供了这些东西:了解open in new window umi、了解 ant design...

903c576a299a4a53baf5f0ee97d1bfbb

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

2. 修改浏览接口页

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

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

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

e57eaec04db74353b10b1a806d985b54

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

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

09ff43fc784a493ea0d796ea9ebaffd6

选一个精简一点的,然后点击显示代码按钮。

43461bda968840adb28ea00eea514a3a

找到示例代码。

46394f6f2b39415da221d014df5dd269
<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>
  )}
/>

放到这里。

eb4b15c8a07f49daa1954908f9337c59

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

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

粘贴到主页中。

fc2144cafd8d4576a57fbdcbadd33f5c

然后进行修改。

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

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

a77537de97b64b37b42cc91ab44eb64e

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

63a0b031f6c34270b8023b5af5f4b791

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

c5a8d53902504f9d9a1812aff9206f81

6. 前端开发接口文档页

1. 创建浏览接口文档页

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

155eab91b6c046aabd95896ff747650e

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

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

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

先去修改超链接。

1512639d33b7486ea137fdb28e9f33b9

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

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

105b5d92dd4c4b2bb944a65277f7bd6a

2. 修改浏览接口文档页

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

**怎么拿到动态路由的参数呢?**查看官网 —— UmiJSopen in new window

6e74e9073bd04eba8477bd771db4bfad

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

点击查看

09eee348f6f747fcb72dad489bb6f8db

弹出提示框,并打印出了 match 参数。

ff8146b09a4f437b9a9b5e9c24a1d235

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

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

13bc74bb51384ae8971d20225de22061

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

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;

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

603c0db50d684474b90c7c899e3e8d80

现在这个接口信息就获取到了。

17a89007ff4c44848fb6d51e93c35b28

3. 美化页面

接下来对查看接口文档页面进行美化,用卡片进行包裹。

fefbfd90a77145cdb2d55966b5ebc0f6

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

92471280ba3442dd9825bba7a30c97da

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

12e8527646304bfdb956dcc64234b35e1f8bd6c3318a4353813b16ab1b33846d

复制示例代码。

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

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

fb5bd2223def4fa8b8444de57f35ba72

做一个判断,如果接口存在就展示列表,否则显示接口不存在;

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

9337c72e2e6c4f0587a90664a1faa512

接下来填充列表内的数值,光标放在 InterfaceInfo 上,按[Ctrl+鼠标左键]。

dcb0821058824d31b3a0207215678df0

看一下接口信息的类型。

0becd87170384a2dabea38fc93ebaf6d

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

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

回到前端页面,点击查看

186eb3d25c55462d852dc245ee9792c8

显示接口信息。

cae289dd8a1f449da061997481527841

再换一个。

f8aeefed033745558014dbef7183635e

接口信息显示正常。

9edf56f5ae1548779489f0bd0b8789c1

不过这个列表展示还是有点太紧了,我们用参数来控制一下;

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

b34701b55914401ca1363961b1381dd0

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

eb2e0a0eecde467c8100151134254bee

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

0b27849054ee4a3d91e602d9b899485d

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

295151cfc13f48d691f37436b1b26256

7. 后端开发申请签名

1. 开发申请签名

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

在线调用前必须要分配一个签名,上次我们自己造了一个签名和密钥,看一下数据库。

71c2b70da0ed43efa3e421bc90ba34bb

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

回到后端 yuapi-backend 项目,找到 UserServiceImpl.java 中的 userRegister。

aec94bb565b1408a949e9d0b8a2486cf

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

怎么给它随机分配?只要用加密算法生成即可。

0be1b01d820c4453b16484663db8cbaf

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

找到 User.java,补充 ak、sk。

5f77fe2b7c0c4adeb0645321e1250b0b

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

446693d2107542a6a35c5396afefc565

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

de101a4d00d141418d53bc2cc6e28390

重启后端项目。

8a8a2bd4637c423a81f22f1cd2affc57

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

e63e86bdd98e445eac3d061829839891

去数据库里看一下。

e403b2d4333b4df894a679c99bbb3d93

注册时自动分配 ak、sk 就搞定了。

2. 创建真实数据

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

450594c7512c44ccac38b5a95b47d4b6

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

6b228f2bd28247479e8df442003bc9a0

填写接口信息,这里的接口就用 yuapi-interface 项目中的 getUserNameByPost,填写完后点击提交

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

25bca7aa5f314161bdb219e154108733

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

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

f79ae2e606eb4774ae807682baabb1bb

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

849ac4c39b6a4c21806a621e2c295d78

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

9d384981b44441aeb5d9e0f5cee6730c

去数据库查看数据,把这条数据放到最上面。

d36d1ac2fcdc4af3b27a535b9354f9e1

先将第一条数据改为 23。

bb9fd0c23f2542e594b0d90a3c13b564

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

d0e13bd321e44267a28ae3fd98df43c6

回到前端页面,查看效果。

9b07df1be736487287e990e3e97d5fe2

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

93f59862c782445e9a3239a9ec3ba2a1

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

3. 补充参数

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

a994216212f94bf58db67e0dbca0522f

更新下 db.sql 的字段。

a4d439608f37475d876fe11e95346a43

右键 interface_info 表,选择 Modify Table。

90e539bd2f674fd497f9dbe3ea0796b1

添加请求参数字段。

b870af493e204d6c9ed91da6136dd01c

interface_info 表就新增了请求参数字段。

4d9dbfd75677417db1e173fc86dd3ab5

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

02fef98016dc4d83b0d0b23f90cb19c5

重启后端项目。

725b9072d54641b4bd8dfa6dcfb10725

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

6487c088b73a42d38806f3e30909d58e

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

3cc017296ace43ef9516bd0fc5a7a78b

修改列的定义。

4f5a0c52f2b643f184cefee221ab1d48

把请求头和响应头的 textarea 改为 jsonCode。

993d6592ecbb4fb98b3270738f411ebc

回到前端页面,点击修改

5ecb9774cd654b4eab0b97c6c815a5e8

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

4711bf5068aa4fe7b44949ca23ea7f40

它接收的是 User,光标放到 User 上,按[Ctrl+鼠标左键]查看。

6a0ec989cbd0497496627d03c98cf2c7

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

是不是还有一个类型,所以这里还要约定一个类型,类型的约定交给大家实现。


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

可以搜一下 JSON 的基本类型。

9029511d228b44d0853c14de47e71641

请求参数的类型(直接用 json 类型,更灵活):

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

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

可能会收获同事的感谢🐶

2a324066e6094305a3367c49b21fb290

粘贴到请求参数框中。

9bd3e2032b5d4385ba798b51ac2bcfb9

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

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

a6a19dc49210496d8e357927b51c5e1a

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

2483e305ced5400fa51bd3ee1c2a0ad7

8. 开发在线调用

1. 前端增加调用按钮

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

340f8a15f24242708154eb707034625f

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

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

eeef081b71934bd1858030bde761ca30

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

9bb629b256d64acebf4a7611181f93d5

官网open in new window 找一个现成表单。

51272c75ebd547049d257456d0d9841b

复制表单代码。

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

粘贴到卡片里。

66103dbe440b46859e1aafa7ba3d8d08

删除表单多余的内容。

2e42d00395254d86b4fe30dec95a8a63
<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 方法复制过来。

ee8e5256969a4a2c9dd2d10eee6d2e00
const onFinish = (values: any) => {
  console.log('Success:', values);
};

粘贴到这里。

a4edae094b684abc8e1756ccde08edd2

然后我们来修改表单。

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

回到前端页面查看效果展示,点击查看

a08cc60bb9ba490b8b81b7b116eaa89f

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

7a178abdede045ddb429e0286606f579

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

bda713f327124420ab25c97ff1283301

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

dbff0be0e3e24cd2818b3faf296f9945

2. 调用流程讲解

接下来要实际地让后端去调用这个接口,所以我们要开发一个在线调用的后端:

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

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

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

984a3b3a61774c93bca34ded979c6d73

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

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

453240c4ac354bf68fa12f8c6040d1df

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

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


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

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

3. 调用流程

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

4. 后端开发在线调用

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

4.1 开发测试接口

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

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

e1ff6c3c2da849e3a46797b44efafe1c

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

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

7089dca58ce440a8af9adfe05ea0f590

然后修改一下参数。

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没有用到?

ccce2e171ae4415f83fdf0c3443311b0

因为 YuApiClient 已经在配置里写死了。

f04459675cc444b594c90625199ca4d2

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

6dc3eb0a77194f6893d3188c7648119f

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

15879489516143119e5c195520ca4f50

欧了~ 现在先重启后端。

86eeb5d79b3543f393f01a0484ed352e

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

c3bd5abe230e4cb69290a7272c3e3a86

4.2 前端修改调用请求

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

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

e262b129ecea4a79a648f4659a7469f9

修改后的 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);
  }
};

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

474b8a3559594b6796237aa8bb72c679

继续完善 onFinish 方法。

8139c76550c24deaaea8d40b7a5ce8ec

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

096bde5720cd433aa2e685e85c294537

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

00dc10aa84244585bbc510cb89d98688

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

62c3f2c8ecb544c2bc2d8c6ea15b2ffc

发一个请求,点击调用,测试一下,测试成功。

322e96f4845742e1920f37a82da1d9a4

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

6471ff119c92481b930628923eae3680

回到前端查看效果。

a3abbf7ec4e44c0bb947ebe94d4bd0f6

欧了~

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

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

11e6d6ba239e49f9a9eaa7ae0ffb536b

9. 优化点

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

10. 扩展点

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