01

1. 项目概述

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

2. 项目介绍

一个提供 API 接口供开发者调用的平台:

管理员可以发布接口,同时统计分析各接口的调用情况,用户可以注册登录并开通接口调用权限,浏览接口以及在线进行调试,并可以使用 SDK 轻松地在代码中调用接口。该项目前端简单,后端丰富,涵盖编程技巧和架构设计等多个技术领域。

主页(浏览接口):

80606af6cb664723926e365ff9523da6

接口管理:

071c9093476f4e9b9dc9c6b41eb0aa77

在线调试:

587c588791714000b1fda0500c0fa30a

使用自己开发的客户端 SDK,一行代码调用接口:

64adf334152843aeae4367473078cbda

首先是它足够新颖,不同于大家在学校时做的管理系统、商城项目等,开放平台通常是知名企业(产品)才会建设和提供的。开放平台类的项目不要说现成的教程了,连相关的文章都少的可怜!

如下图,基本都是几年前的了:

236f3b19e70f4489a192db4d0de7a8cc

💡 为什么做这个项目?

  1. 该项目足够新颖,与大家在学校所做的管理系统、商城项目等有所不同。
  2. 开放平台类的项目通常只由知名企业或产品提供,因此该项目的建设和提供也很特别。
  3. 相比于其他技术领域,开放平台类的项目缺乏相关的教程和文章,相当匮乏。

开放平台项目涉及多个系统的交互,包含了 API 签名认证、网关、RPC、分布式等必学知识,很适合提升后端同学的系统设计和架构能力,是很多网课所不能提供的。

b87ff5429af34fb7913a91b52a209d12

3. 前置知识

  1. 本项目更侧重后端,如果你学习过后端开发技术(比如 Java Web),希望做一个区别于管理系统的、有亮点的、写在简历上加分的项目,并提升自己的编程和架构设计能力,那么非常欢迎来学习!
  2. 如果你是前端,也可以通过这个项目学习到快速开发前端项目的技巧,但是最好学习过 Vue 或 React 框架。

4. 项目源码

  1. API 开放平台 - [前端代码open in new window]
  2. API 开放平台 - [后端代码open in new window]

5. 项目准备工具

前端开发:

  1. nodejs 安装教程open in new window
  2. WebStorm 安装教程open in new window [vscode 安装教程open in new window or vscode 安装教程open in new window] (选其一即可)

后端开发:

  1. idea 安装教程open in new window
  2. Java 安装教程open in new window
  3. maven 安装教程open in new window

其他工具:

  1. mysql 安装教程open in new window
  2. redis 安装教程open in new window
  3. yarn 下载open in new window
  4. yarn 安装教程open in new window
  5. git 安装教程open in new window

注意:

  1. nodejs 要下载大于 16.14,小于 18 的版本
  2. idea要下载 2020.1以上的版本,才能下载 MyBatisX 插件

推荐的可能不是最优的,大家可以根据自己的需求去百度查找相关教程<( ̄︶ ̄)↗[GO!]

6. 项目相关学习

学习路线:

  1. ooxx导航 - 前端学习路线open in new window
  2. ooxx导航 - Java 学习路线open in new window
  3. ooxx导航 - 数据库学习路线open in new window
  4. ooxx导航 - redis 学习路线open in new window
  5. ooxx导航 - git&github 学习路线open in new window

7. 本期时间点

经过7场直播,总时长近20小时。

631a8cfff8ee48909266b7c6619f72c1

1. 预计计划

  1. 项目介绍、业务流程、项目计划、需求分析
  2. 数据库表设计
  3. 前后端项目初始化(包含 Ant Design Pro 框架最新版本的使用)
  4. 前后端代码自动生成(强烈推荐,提高 1000% 的开发效率)
  5. 登录页、接口信息页开发

8. 项目业务流程

1. 流程概述

5个子系统:

  1. 模拟接口系统:提供各种模拟接口供开发者使用和测试,例如,提供一个随机头像生成接口。
  2. 后台管理系统:管理员可以发布接口、设置接口的调用数量、设定是否下线接口等功能,以及查看用户使用接口的情况,例如使用次数,错误调用等。
  3. 用户前台系统:提供一个访问界面,供开发者浏览所有的接口,可以购买或开通接口,并获得一定量的调用次数。
  4. API 网关系统:负责接口的流量控制,计费统计,安全防护等功能,提供一致的接口服务质量,和简化 API 的管理工作。
  5. 第三方调用 SDK 系统:提供一个简化的工具包,使得开发者可以更方便地调用接口,例如提供预封装的 HTTP 请求方法、接口调用示例等。

需要考虑的关键问题和挑战:

  1. 接口设计:需要设计清晰易用的 API 接口,并且提供详细的接口文档,以方便开发者使用。
  2. 性能和可用性:平台需要承载大量的接口请求,因此需要考虑到性能和可用性问题。例如,设计高效的数据存储和检索策略,确保 API 网关的高性能等。
  3. 安全:平台需要防止各种安全攻击,例如 DDOS 攻击,也需要保护用户的隐私和数据安全。
  4. 计费和流量控制:需要设计合理的计费策略和流量控制机制,以确保平台的稳定运行和收入来源。
  5. 易用性和用户体验:需要为开发者提供简单易用的接口调用工具和友好的用户界面,提供优质的用户体验。
e81bb81ef6684b72bd3441d7e1f9c6bb

2. 详细说明

需要注意的问题及知识:

❗ **注意:**在实现接口调用时,如果要作为一个开放平台,不要轻易免费开放接口,因为这会极大地增加平台被攻击的风险,导致服务器瞬间被 DDOS 攻击瘫痪。

ps.不了解的朋友看看🐟被 ddos 的视频🐶

💡 在调用这个接口时需要考虑以下问题:

  1. 需要考虑访问权限的问题:用户是否可以随意访问数据库和接口。
  2. 需要添加计费功能,统计用户调用次数,并考虑限流或流量保护措施。
  3. 需要考虑如何有效地管理用户。(例如,如果用户欠费或者是陌生人调用接口,我们需要及时发现并采取措施)

🪔 有没有听说过 API 网关?

API 网关的主要作用是为多个接口提供保护,并集中进行计费、健全日志等逻辑处理。类似于我们去火车站坐高铁一样,无论是去哪个站台坐哪个火车,都需要先通过检票口进行集中的检票。

与我们程序中的 AOP 不同,API 网关是一个独立的服务,需要单独开通。

🪔 什么是 SDK?

SDK 是软件开发工具包的缩写,是一种为软件开发者提供支持的一系列工具、接口和规范的集合。举个例子,比如腾讯云提供了一些接口,比如创建 VPC 等,如果开发者直接向腾讯云服务器发请求,需要输入密钥、做签名认证等操作,非常繁琐。因此,在构建第三方 API 平台时,一般都会提供一套 SDK,让使用者能够轻松地调用接口,无需自己编写和封装 HTTP 请求。可以把 SDK 理解为 Java 语法中的工具包,使用者只需要最少量的代码即可调用接口,如果不理解 SDK,需要加强 Java 语法的学习。


首先,管理员可以发布接口并将其存储到数据库中,设置接口的调用数量和上下线接口状态。例如,发布一个免费的随机头像接口,它将有一个地址(比如:yupi.icu/img)并保存到数据库中,这里需要一个后台管理系统。

现在,我们的用户是开发人员,他们如何使用这个接口呢?为此,我们需要提供一个访问界面,我们称其为用户前台。用户通过前台浏览所有的接口,当找到所需接口时,可以购买或开通接口,获取一定量的调用次数,例如一万次或一千次等。开通接口后,开发者可以调用接口的 API,系统从数据库中获取相关数据并进行调用;同时,管理员可以在后台进行相关的操作(如下图所示)。

6f55cb41e39246a3a6e60a7067e82bf6

举个例子,腾讯云提供各类开放接口,其中有他们自己开发的或者是与第三方合作开发的。而在该项目中,由于目前没有第三方,所以我们需要自己提供这些接口。因此,在该项目中,将会带领大家开发模拟接口——这是需要开发的第三个子系统,我们可以随意开发一些类似 helloworld 的小接口,然后将整个流程打通。

后续当你开发额外的接口时,可以自己开发并提供给用户。如果你使用了第三方接口平台,你需要向对方提供接口文档,就像腾讯云的 API,其中有很多功能接口,每个接口都需要提供相应的文档,否则用户将不知道如何使用这些接口。这种情况在使用第三方接口时很常见,有时用户看文档时感到困惑,觉得文档难懂或不够清晰。为解决这个问题,我们最好提供在线调试功能,让用户能够立即知道如何使用接口。此外,为了方便用户,我们可以提供一些示例代码,让他们可以直接复制粘贴到自己的项目中。因此,我们的项目中应该提供接口文档和在线调用功能(如下图所示)。

eadb65cc73524fcf9b55a6f4abd58f9e

在作为第三方接口平台的时候,我们需要考虑到防止攻击、统计调用次数、计费流量保护等方面的问题。然而,如果我们需要给用户提供大量的接口,单独为每个接口开发计费统计、流量保护等功能可能会成为一项繁琐的工作。因此,我们可以考虑使用一些通用的解决方案,例如使用 API 网关来管理接口,统一进行流量控制、计费统计等操作。这样可以减少重复开发的工作量,提高开发效率。

目前,实现 API 网关的方式有多种,例如 Kong、Nginx、Gateway 和 Spring Cloud 的 Gateway 扩展等。企业中也可能会使用自建 API 网关。此外,Kong 实际上是对 OpenResty 的一种封装,同时也是对 Nginx 服务器的上层扩展,因此,熟练掌握 Nginx 服务器的使用将有助于理解 API 网关的实现。总的来说,熟悉 Nginx 服务器的开发人员应该更容易实现 API 网关。如果大家对这些技术还不太了解,那这个项目将会帮助大家拓宽视野。这些技术都是架构层面或微服务层面的技术,在企业实战层面是必不可少的。

这个项目需要包含多个子系统,但光有这些还不足以完成整个项目,因为我们还需要为其他开发者提供一个第三方接口调用平台。在这个平台中,我们将提供一个下载示例代码的 SDK,以便其他开发者可以更方便地使用我们的接口。因此,我们还需要开发一个小系统,即第三方接口调用的 SDK。

目前,我们已明确了该项目五个主要平台,分别是模拟接口、后台管理系统、用户前台、API 网关和第三方调用 SDK。而这五个平台会被拆分成五个项目,并且每个项目还会被拆分成相应的子系统。具体来说,我们需要开发五个系统:模拟接口系统、后台管理系统、用户前台系统、API 网关系统和第三方调用 SDK 系统。

7eb5485978df40228df23b39f94a303d

9. 项目技术选型

前端:

  • React 18
  • Ant Design Pro 5.x 脚手架
  • Ant Design & Procomponents 组件库
  • Umi 4 前端框架
  • OpenAPI 前端代码生成

后端:

  • Java Spring Boot
  • MySQL 数据库
  • MyBatis-Plus 及 MyBatis X 自动生成
  • API 签名认证(Http 调用)
  • Spring Boot Starter(SDK 开发)
  • Dubbo 分布式(RPC、Nacos)
  • Swagger + Knife4j 接口文档生成
  • Spring Cloud Gateway 微服务网关
  • Hutool、Apache Common Utils、Gson 等工具库

10. 需求分析

背景:

  1. 前端开发需要用到后台接口
  2. 使用现成的系统的功能 - 免费API接口平台open in new window

做一个 API 接口平台:

  1. 管理员可以对接口信息进行增删改查
  2. 用户可以访问前台,查看接口信息

其他要求:

  1. 防止攻击(安全性)
  2. 不能随便调用(限制、开通)
  3. 统计调用次数
  4. 计费
  5. 流量保护
  6. API 接入

11. 本期计划

  1. 项目脚手架搭建(初始化项目) -- 10 分钟前端,5 - 10 分钟后端
  2. 管理员可以对接口信息进行增删改查
  3. 用户可以访问前台,查看接口信息

12. 前端项目初始化

1. 搭建项目脚手架

1. 点击开始使用

Ant Design Pro:官方文档open in new window

进入 Ant Design Pro 网站,点击开始使用

ed380d6b035b4f1f845771dfeaca4558

2. 复制初始化命令

复制初始化命令。

6af37b18f530411d8951b3c8f43b79ea
▼bash

复制代码# 使用npm
npm i @ant-design/pro-cli -g

pro create myapp

3. 在本地打开项目目录

在 D 盘按[Ctrl+Shift+N]新建文件夹,位置和项目名可自拟。

进入创建好的项目目录,点击左上方目录栏。

1c74722762a14e6cb3225aa20ad62a95

输入cmd后,点击[Enter]回车,进入到当前目录的终端。

ba3bd506560d40149a4be39405e1059d

或使用**PowerShell**(在 cmd 使用 npm 卡顿,换成 PowerShell 试试):

在项目目录中空白处,按 [Shift+鼠标右键] → 选择在此处打开Powershell

36d5826d7c0f4cd3b6cf5db3e595796f

4. 使用初始化命令

打开Powershell

7f215ed0332c405195dfe3b619966d4c

将初始化命令粘贴到此处,回车,全局安装脚手架。

331caf131abb4550b7f7afe61fbfbabd

5. 创建项目

使用pro create yuapi-frontend创建项目。

ps.yuapi-frontend是项目名称,可自行更改。

选择最新版本umi@4

0d82e45d7fca41d2bab6b63758554634

2. 使用前端开发工具

1. 打开项目

开发前端使用webstormvscode

ps.🐟用webstorm,电脑内存太少,再装webstorm要炸,我这边就使用vscode

进入vscode

c01902d644bf4e119660f009c6c7c941

点击左上角 文件 → 打开文件夹**。**

2b3e4e5932004176aba3f0afef0c2786

选择创建好的项目目录。

99e3442d42ab4141b3b8a2244cbcd2b2

以下是刚刚生成的项目(如下图所示):

f617a94601e94df4aeb4ea94bd85dd0b

确认环境是否匹配:

右键目录栏空白处,选择在集成终端中打开。

fbbecb24ac69495bbaa40ce41ce6ec83

输入命令,查看版本。

aaada1ddbac7483ca9ed3db18a73b15c
▼bash

复制代码# 查看nodejs版本
node -v

# 查看npm版本
npm -v

# 查看yarn版本
yarn -v

ps. nodejs 建议下载大于 16.14,小于 18 的版本。

2. 安装依赖包

打开开发工具下的终端,输入yarnyarn install

ps.依赖包没有安装,启动不了项目。

webstorm

点击最下方的Trtminal(终端)。

65662bfaf9464c8f9b14e16ed12b5ad9

在终端输入yarn

cb5613eff9344a268237edff6f3a0acf

就可以在项目目录中看到生成好的依赖包。

acaaa7ffd5964f1ca50fa68698c68499

在终端输入yarn

53c619c25e9d4aac949e78a1cc079769

就可以在项目目录中看到生成好的依赖包。

4f7258064f324b2f9e9545d02d851756

3. 项目试运行

找到package.json中的dev,以本地开发的方式运行。

ps.package.json:管理项目依赖、脚本的工具。

f86d02f17d2a403a8b31cd8af3791876

💭 这里的dev命令start:dev命令一样都是启动命令。

start:dev命令中的MOCK=none,是把自动模拟的数据关闭;

dev命令是调用自己定义的后端;

start命令是以模拟数据的方式运行项目。


webstorm

点击绿色启动按钮。

838118de7cd8487ba25fa787b5aed4b5

配置好了之后点击Run

54bc6553b807473ba6625c0c95dad7b3

或在终端使用yarn run dev启动。

95235bdae2de46608d79f1fa02536fe8

vscode

在终端使用yarn run dev启动。

9742a271294d48cda3d25f8ddd12c571

根据提示,访问 http://localhost:8000,预览一下:open in new window

d01071fb138c4585befc0067b145ee9c

按照提示,输入账号和密码。

▼bash

复制代码# admin会多一个管理页页面
用户名:admin  或者 user
密码:ant.design

dev模式下,由于本地没有后端服务器,因此会提示登录失败。

因为缺少数据,无法运行ㄟ( ▔, ▔ )ㄏ

b25768ebfb0541059ee0ef4b7504cfe3

找到package.json中的start,以模拟数据的方式运行。

webstorm

点击绿色启动按钮。

0b6254d3186642fd902af99e69d6ce96

配置好了之后点击Run

59dc5d9261574051b1b8064ae01bbab7

vscode

在终端使用yarn run start启动。

5eadb70bd31f49c98d511ff9bd8ec40d

访问 http://localhost:8000,再次输入用户名和密码,登录成功。open in new window

a4704112da4345b0a6f540817c759b81

3. 项目瘦身

bd1a0fcdd4f6461d820f16343a9fde7f

1. 移除国际化

Ant Design Pro 集成了很多的功能。比如:国际化。

国际化的代码在 目录路径: /src/locales 下:

b717157d083a48e0b14cfb9595f244eb

国际化介绍:

它给我们集成了多种语言:中文、英文...

因为这个项目主要在国内访问,国内都没多少用户浏览,更别说国外了;

如果要使用国际化,还要去配置不同语言,很麻烦。直接移除🔨


package.json找到i18n-remove,这里提供了移除国际化的脚本。

f148a563fe6547a780dcf7764e064402

执行i18n-remove,报错了。

5e2babccd5f445cb9d075c85f2d6c284

遇到报错怎么办?

  1. 百度
  2. 问原作者 or 官方团队 👉 官方的开源地址open in new window
e13889abb0a9425ea4320da674c62eb1

然后看刚刚报错的地方,找几个报错信息,多搜索几次,能找到为止。

复制pro i18n-remove --locale=zh-CN --write报错信息。

1b88cc25724e41319a0109e3be00e9a4

点击Issues,将报错信息粘贴至搜索框搜索,回车。

1f0a842cd59a44c482b1ae5979d67474

搜索到了 (≧∀≦)ゞ

8e43cf5f26824773a4fcaecd37c5555b

往下滑,一模一样的报错,报错路上不是一个人hhhhh

34ec80aad8ce431484880ac9134919c7

再往下滑,有人提供解决方法open in new window(如下图所示)。

135970288f994aaf8159c51e459b51d8
▼bash

复制代码1.执行
yarn add eslint-config-prettier --dev yarn add eslint-plugin-unicorn --dev


2.然后修改node_modules/@umijs/lint/dist/config/eslint/index.js文件注释
// es2022: true可以解决问题

在终端执行:

▼bash

复制代码yarn add eslint-config-prettier --dev yarn add eslint-plugin-unicorn --dev
ec00aea0f6ff43139b25ba98d9e20558

注释掉项目目录:node_modules/@umijs/lint/dist/config/eslint/index.js文件中的es2022: true(一定要注释掉,不然还会报错👻)

55ee6f8d6e0246bdb9fcd8fa59fd848c

一定要注释掉,不然还会报错。

51060bced01040b282846eb9dd58c30d

再次执行i18n-remove,可以看见它帮我们删除了一些代码。

6a03a75b78494045a3d2ef19eb9d2790

但是locales目录不会被删掉,我们手动将其删除。

f36b8270ee5b4ca4b9b464e9552dc0a4

在终端使用yarn run start启动项目,看看能不能正常运行。

0badd736ae934b4b9546ca9db95d4081

登录之后发现原本左侧的菜单栏没了。

a87294665e0647cc9e68223e285d746a

目录路径:config/route.ts的路由加name

f7407a6ae59d4f908e6466e2b428de69

给路由加上name

488d1a6912b0484fac7e61bfc77a897a

再次启动项目,输入账号密码后,菜单栏显示出来了。

d12be7a910d746b5b6667158db9f0bcc

2. 移除测试工具

6fe548dde2874a08ae25bf0822fc89a9

13. 后端项目初始化

1. 使用后端项目模板

1. 下载模板

使用🐟整的万用后端项目模板 👉 springboot-init 项目open in new window

a344bbde84974072b62d986b6aa0b490

下载并解压至项目目录。

ceb809c64d534c2f8010496aa345b1a2

并修改文件夹名为yuapi-backend

c57570d9fc814fc5ae3c1209226886b8

模板功能:

1de973dba144467fbc22a27a6840a5d9

2. 加载依赖

开发后端使用**idea**

idea打开yuapi-backend

67659901758d44eaad7d9444199641f9

选择左上角File→settings

86e14f6022ec42a991d43a65fa7c8640

修改 maven 的设置,我这里默认会变成在 C 盘。

fd66c5a68df748e693210eaa006b1bb6

然后项目就开始加载依赖,最下方,有个蓝条咻咻地在动,等它加载完。

点击pom.xml,看看有没有报错。

9137f334d419491d8d4a65b66ad926c2

有报错就点击右侧菜单栏的 maven。

dc6a99e7bbe24e33a6439639f2107b2874775836991249d0a7e76bb9bfea1357e03a26a127e14c839c87d6e1c508c331

如果还有报错,找到报错的位置,没有版本号就添加版本号,有版本号就换一个。

5e71cb57735c484e831c9b5c7ea74c82

怎么知道这个依赖有什么版本呢?随便加一个?(达咩.jpg)

百度搜索 maven repositoryopen in new window,这个网站能找到所有 java 的包。

a5f6d01936594fb0862647ce108ba2cb

把报错的位置复制粘贴到搜索框,回车,然后点击搜索到的内容。

996ac57bcfc040da8a814b7f7055133c

这里有很多版本,选一个风险少,人数多,版本不会太新,或者太旧的;

点击选择的版本号。

c3f8c4d0323d4156af353e6af9e478ff

复制依赖。然后粘贴至pom.xml,替换掉之前报错的。

ps.希望不会和其他依赖的起冲突,先给 xdm 试试水 保佑.jpg

f2da86e34994403eb8a07799a2f9d387
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-maven-plugin -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <version>2.7.2</version>
</dependency>
b83573e1fadd402bab872143ac43753576b60504b628411dae797f48b8b3fa3f

没有报错,问题解决<( ̄︶ ̄)↗[GO!]

361a243f25784f78ac60812e2d2df051

3. 下载 markdown 插件

idea 下载插件有种方式:

  1. 通过 idea 软件提供的方式下载
  2. 外部安装导入 - 直接安装

这里我们使用第一种方法。

点击左上角菜单栏上的 File → settings → plugins。

bcb1b5af58504212b8bb1f13bb2da250

在搜索框输入markdown,点击[Enter]回车(步骤如下图所示)。

36875cc4e7f740c48c89ecdba6abf3ca

安装这个插件之后,打开 .md 文件就可以支持 md 的视图查看和编辑内容。一般用于写 README.md 文件。

点击后端模板中的 README.md,这里是🐟写的使用教程,右上角有三个图标:

b6eee9f9b0f741f3ba37a9891cf79548f48478c59de84ccb8372e0c350dbc5ae6241096cb5fe44509408e33aa1f8c27688e2557a077d4b11b29a613c472d262ae6752b20923145e3be99c629132d94ff3612c611e8284e829627be8f72a44c3c

4. 修改项目名称

进入pom.xml

选中springboot-init然后按住[Ctrl+Shift+R]全局搜索。

d352e95c0d69487e9c99451917f76d5f

springboot-init全部替换成yuapi-backend

7ec068ceb7664a39bad3b8ac9b95f3b6

把描述改成谷牛接口

a53be4d43cb84912935bcfacddd29d7c77da9e39fa384e109728dca16cb4c61842c19710fc6d4ca4ac8c59e4361a3aaf

项目名称更新成功。

2dfc399b4a8d48d6a1ccb5e0f0888ade

5. 修改配置

1. idea连接本地数据库

首先点击右侧菜单栏的Database

960793c2edcb443e98536923136561df86c0c7a3e4ba4baf9b435c056a9e9c394edab2fa754b4773823166186aac3e83

填写 mysql 用户名和密码。

7895bf85e72b42a1ba4d558efb571374

连接成功。

6f518bd9bd5e492e9c576c1af7a418cb

这个模板提供了示例 — 初始化的数据库脚本,找到ddl.sql

上方会弹出是否使用 Mysql 语法,点击Use Mysql,这个文件就会以 Mysql 语法格式显示。

52432f6028c94b778a780c21a14bc006
-- 创建库
create database if not exists my_db;

-- 切换库
use my_db;

-- 用户表
create table if not exists user
(
  id           bigint auto_increment comment 'id' primary key,
  userName     varchar(256)                           null comment '用户昵称',
  userAccount  varchar(256)                           not null comment '账号',
  userAvatar   varchar(1024)                          null comment '用户头像',
  gender       tinyint                                null comment '性别',
  userRole     varchar(256) default 'user'            not null comment '用户角色:user / admin',
  userPassword varchar(512)                           not null comment '密码',
  createTime   datetime     default CURRENT_TIMESTAMP not null comment '创建时间',
  updateTime   datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
  isDelete     tinyint      default 0                 not null comment '是否删除',
  constraint uni_userAccount
  unique (userAccount)
) comment '用户';

-- 帖子表
create table if not exists post
(
  id            bigint auto_increment comment 'id' primary key,
  age           int comment '年龄',
  gender        tinyint  default 0                 not null comment '性别(0-男, 1-女)',
  education     varchar(512)                       null comment '学历',
  place         varchar(512)                       null comment '地点',
  job           varchar(512)                       null comment '职业',
  contact       varchar(512)                       null comment '联系方式',
  loveExp       varchar(512)                       null comment '感情经历',
  content       text                               null comment '内容(个人介绍)',
  photo         varchar(1024)                      null comment '照片地址',
  reviewStatus  int      default 0                 not null comment '状态(0-待审核, 1-通过, 2-拒绝)',
  reviewMessage varchar(512)                       null comment '审核信息',
  viewNum       int                                not null default 0 comment '浏览数',
  thumbNum      int                                not null default 0 comment '点赞数',
  userId        bigint                             not null comment '创建用户 id',
  createTime    datetime default CURRENT_TIMESTAMP not null comment '创建时间',
  updateTime    datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
  isDelete      tinyint  default 0                 not null comment '是否删除'
) comment '帖子';

这个模板默认的数据库名为:my_db,改成自己的;

按[Ctrl+Shift+R]全局搜索,把数据库名全部替换成yuapi

8a01bbf68b4a4bad869ffcae01951be8c86ae569ce2848a0840d764a0706bdb998a876b72f004facbadc513eb564b626

右侧就多了一个创建好的yuapi数据库。

d72892b877ae4c0aada68e0c5a1ff228

找到application.yml,修改数据库配置为自己的。

be9325c4952c46c4ab52d033c3b5920f
2. 修改 redis 配置

application.yml中还用到了redis session

如果没有用到 redis,可以把store-type改成none→ 即存储会话不用 redis 存储。

我本地有 redis,所以就不改为 none 了🐶

24893572993a4c8598958a07e323ca78

注意:不改 none 的小伙伴,一定要本地先启动 redis;不然,报错警告👻。

在本地安装的 redis 目录下找到 redis-server.exe,双击启动,放那别关掉。

b4001d82f8904214a7faf380a90481d7

6. 项目试运行

fd0e4c225d71445688dd432e81f17bf6

下方控制台会提示已经在7529端口启动成功了。

55d03b1bb4b04d4b8a48019ec090fd19

这个端口号是在application.yml配置中设置的,可自行更改。

017f0e5dd0a24188b02708c557027388

访问 http://localhost:7529/api/doc.html,打开现成的接口文档;

不需要写前端就能在线调试接口,这里有很多现成的接口。

0c8c02098b5749a9aa68b02fbe42f37a

部分接口介绍:

使用userRegister接口注册账户。

df99323ce74a4c4189178c7886ed18de

注册成功。

▼json

复制代码{
  "checkPassword": "12345678",
  "userAccount": "yupi",
  "userPassword": "12345678"
}

userLogin接口中使用刚刚注册的账号进行登录。

74b5c19a5f2a44b2937e032481f4a1b2

登录成功。

▼json

复制代码{
  "userAccount": "yupi",
  "userPassword": "12345678"
}

使用listUser接口获取所有用户。

1f0446fe96b94b42bd8f245fc9a2274f

使用getLoginUser接口获取当前登录的用户。

7a238effa9ad487fadf5ca6b64276c35

使用listPost接口获取帖子信息。

9aaf995a76b44e47a2ab8e5a7c16c513

现在还没有权限,因为是获取全量数据 — 指全部的数据 list 接口,只有管理员有这个接口的权限。

2. 数据库表设计

任何系统实际上都可以抽象成管理系统。那么对于我们的系统,最基本的功能就是接口管理。

接口信息表(interface_info)
字段说明类型
id用户 id(主键)bigint
name名称varchar(256)
description描述varchar(256)
url接口地址varchar(512)
requestHeader请求头text
responseHeader响应头text
status接口状态(0-关闭,1-开启)int
method请求类型varchar(256)
userId创建人bigint
createTime创建时间datetime
updateTime更新时间datetime
isDelete是否删除(0-未删, 1-已删)tinyint
-- 接口信息
create table if not exists yuapi.`interface_info`
(
  `id` bigint not null auto_increment comment '主键' primary key,
  `name` varchar(256) not null comment '名称',
  `description` varchar(256) null comment '描述',
  `url` varchar(512) not null comment '接口地址',
  `requestHeader` text null comment '请求头',
  `responseHeader` text null comment '响应头',
  `status` int default 0 not null comment '接口状态(0-关闭,1-开启)',
  `method` varchar(256) not null comment '请求类型',
  `userId` bigint not null comment '创建人',
  `createTime` datetime default CURRENT_TIMESTAMP not null comment '创建时间',
  `updateTime` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
  `isDelete` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)'
) comment '接口信息';

1. SQL之父使用

SQL 之父open in new window 简单介绍:

在开发新项目时,我们通常需要编写 SQL 语句来创建数据库表并手动添加假数据进行测试。然而,随着项目的增多和表格的复杂度,手动编写和添加数据的成本越来越高,也越来越浪费时间。

SQL 之父可以帮助我们自动生成 SQL 语句和模拟数据,从而显著提高开发效率,让我们更专注于业务逻辑的实现。

👋🏻 做新项目用新表、模拟数据,可以用 SQL 之父,这里还能转换成 Java 代码。

62c26a2ad12942759c689bfea16f8465

我们要创建一个接口信息表,在SQL之父填写对应信息。

400ab58b1d9e452b8a1b928eb4c3c90e

修改通用字段,字段采用驼峰式,让前后端一致。

c9cb4fd4bc624f0d8fdf5511bda9e225

除了这四个字段之外,还需要一个接口名称字段name

接口还需要哪些字段?

建议:如果不确定接口要有什么信息,就去看一下 Swagger 接口文档里都有啥。

bf45634b124b467a946b77f1e47ed57d

这些都可以作为一些字段信息。

目前需要的字段:

ccef991f1f7b4c0ebde0a9291109611c

还需要哪些字段呢?可以去看看请求的信息;

鼠标右键 → 选择检查(或按F12),点击网络,按F5刷新,查看请求的信息。

656e46717003448692a2e97956d90fba

这些请求头,响应头,你都要了解他们是做什么的,都是库表设计要写进去的。

ps.例如:cookie、Content-Type、Referer、Host、请求方法、状态代码等等。

38956fe9108c47dcb969d2e1d68dd4bb

新增字段:

6be93b6b467b4a148bc1457b60dcb039

回到SQL之父填写对应字段。

dd905509a85c4084a82c626f0a85a0d7

可点击左侧 > 按钮设置对应字段。

3ecf29c6aa1f4b38af69699975623512

设置字段(设置完后,点击一键生成):

d8f28956437a4827b3a2e9ac44a0a0f3

此时右侧会出现建表语句和插入语句;上方是建表语句,下方是插入语句(模拟数据)。

这里程序有个小 bug,把 CURRENT_TIMESTAMP 两边的单引号去掉。

ps.不然,建表报错警告👻

6a972a868a804bb68d3c507334736099

点击复制全部

f7ba3d098c9f41cebd20f582ea117271
-- 接口信息
create table if not exists yuapi.`interface_info`
(
`id` bigint not null auto_increment comment '主键' primary key,
`name` varchar(256) not null comment '名称',
`description` varchar(256) null comment '描述',
`url` varchar(512) not null comment '接口地址',
`requestHeader` text null comment '请求头',
`responseHeader` text null comment '响应头',
`status` int default 0 not null comment '接口状态(0-关闭,1-开启)',
`method` varchar(256) not null comment '请求类型',
`userId` bigint not null comment '创建人',
`createTime` datetime default CURRENT_TIMESTAMP not null comment '创建时间',
`updateTime` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
`isDelete` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)'
) comment '接口信息';

insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('刘笑愚', '吴琪', 'www.wesley-trantow.com', '万鸿煊', '王越彬', 0, '黄雪松', 26826);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('潘展鹏', '叶晓啸', 'www.courtney-kassulke.net', '戴鸿煊', '袁荣轩', 0, '谢立诚', 434);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('覃天宇', '冯昊强', 'www.johnie-harris.name', '武博文', '戴思聪', 0, '毛昊焱', 2334);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('谭黎昕', '傅哲瀚', 'www.josette-adams.org', '覃振家', '吕风华', 0, '孔鹭洋', 57553);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('魏嘉熙', '沈思聪', 'www.wyatt-nader.org', '韩嘉懿', '熊思源', 0, '姚立轩', 0);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('龚修洁', '宋锦程', 'www.jerlene-grimes.io', '廖哲瀚', '张建辉', 0, '林天宇', 34618);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('蒋鑫磊', '谭明辉', 'www.micki-dicki.name', '唐雪松', '沈鹏飞', 0, '罗烨华', 27);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('钱雪松', '吕鹏', 'www.andy-russel.org', '范烨伟', '邵黎昕', 0, '苏笑愚', 5460331959);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('严晟睿', '唐晟睿', 'www.nadine-bradtke.name', '郑峻熙', '冯琪', 0, '秦烨华', 90477376);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('段浩轩', '潘文博', 'www.terrence-konopelski.co', '韩雨泽', '袁志强', 0, '陈博文', 9453614492);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('黎健雄', '龙泽洋', 'www.rodney-douglas.io', '冯晟睿', '韩明哲', 0, '蒋弘文', 70703);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('邵绍齐', '范振家', 'www.wilbur-reinger.name', '邹绍辉', '叶哲瀚', 0, '姚子轩', 24180);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('顾博文', '张瑾瑜', 'www.kittie-kautzer.name', '龙修杰', '万弘文', 0, '潘弘文', 334268466);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('唐明辉', '郝耀杰', 'www.ed-barton.org', '蒋晓啸', '段钰轩', 0, '程明', 6);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('陶泽洋', '龙语堂', 'www.shane-braun.io', '杜驰', '徐笑愚', 0, '熊展鹏', 8);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('孔炎彬', '龚昊天', 'www.rolf-wiegand.net', '赵明辉', '覃昊天', 0, '白天宇', 7453201);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('陈梓晨', '高志泽', 'www.norah-goldner.org', '何鹤轩', '郝鹏', 0, '李煜城', 791);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('白思', '汪懿轩', 'www.hugo-bradtke.co', '于立轩', '毛楷瑞', 0, '罗俊驰', 3219);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('李琪', '谭健雄', 'www.gerry-dicki.biz', '龙果', '吴晟睿', 0, '马昊焱', 61151986);
insert into yuapi.`interface_info` (`name`, `description`, `url`, `requestHeader`, `responseHeader`, `status`, `method`, `userId`) values ('吴思', '吴志泽', 'www.kareem-feest.io', '汪黎昕', '赵瑾瑜', 0, '邱致远', 6);

2. 创建接口信息表

sql目录新建db.sql

鼠标右键sql目录选择New

c33b7eeee7b5466b881a43f79d9ea4b1

输入文件名,点击[Enter]回车。

94b876eb209d4b8aa2f36744428feda7

把刚刚复制的SQL语句粘贴至db.sql内。

上方会弹出是否使用 Mysql 语法,点击Use Mysql,这个文件就会以 Mysql 语法格式显示。

31dce2fa05434d30a0adacc850c0e2f8

在最上方添加use yuapi;语句 — 指定往哪个数据库里插入。

38802782433f47f6831941dbb356be17072cb017cf194dbfa9e0ba495f5a6867b4369be04986412eba4449c1b2dda2d3

右侧就多了一个创建好的interface_info表。

9e303ac1dd9046a98ec509ec31878760

点击interface_info表,查看表中的内容,此时已经有了现成的模拟数据。

67aa10ac8ff24a718f3a5eaf0de8ca0e

3. mybatisX 插件生成代码

表已经有了,现在就可以生成增删改查代码了。

1. mybatisX 插件安装

点击左上角菜单栏上的 File → settings → plugins。

9c9f7f6c56224ba3b92675c8cffc2101

在搜索框输入 MyBatisX,点击回车(步骤如下图所示)。

f87524f5be064de5be1201cca895a9da

2. mybatisX 插件使用

右键interface_info表,选择MybatisX-Generator

cae957e585a5451998d7ee0587f3bcbb

模块路径选择当前路径即可。

c4630bb23aa041e09986d39a6244dcef

点击Next

f6162119dc0049218473b0a1f012d114

如果发现点击Next出现的还是和上图一样,就点击Previous(往前一步),再点击Next(下一步)。

ps.这个插件有一点bug ╮(╯▽╰)╭

如下图所示选择后,点击Finish

02ac108d13f3440fa69e7884d442eaa2

多了一个generator目录,代码都生成在这里。

bcec140e1ce54185bdfc514d48d13d21

3. 迁移生成的代码

复制generator目录下的service目录

粘贴至com.yupi.project下。

7fb6a4f2b3ad47238f6309600243f061

复制generator目录domain目录下的InterfaceInfo

粘贴至com.yupi.projectmodel目录entity目录下。

c46a16c0f31a4d509bed1c0feffc42aa

复制generator目录mapper目录下的InterfaceInfoMapper

粘贴至com.yupi.projectmapper目录下;

然后把generator目录删掉。

e46e9274a2bd4bcc92e7ef0b0cdd26bf

底下的InterfaceInfoMapper.xmlmybatis-plus用到的一个配置文件,会和mapper文件关联上。

它现在生成的包名是generator目录的,我们已经移到别的目录下了。

65ef30901837408bbbcad15b257dbc7e

选中generator.mapper,按住[Ctrl+Shift+R]]全局搜索。

10f68f8b25164981b30ba4ec81439953

InterfaceInfoMapper也是引用了generator目录,我们要修改为正确的路径。

6bdbc6744745465590111dcea1127fd8

选中generator.domain,按住[Ctrl+Shift+R]全局搜索。

c729043107c4495a81147a31f620a963

现在 mapper、service 层有了,还缺一个 controller 层。

来到PostController,按[Ctrl+Alt+O]自动删除未使用的导入语句,并将重复的导入语句合并成一个。

24140d2889ed474d99546dfae83226e0

初始化项目模板里有现成的增删改查、分页查询,可以直接用,复制PostController粘贴controller目录下,并重命名为InterfaceInfoController(接口信息 Controller)。

fb3032210cde40f1a21b7fadea6bd50d

InterfaceInfoController内选中post按住Ctrl+R

post全部替换成interfaceInfo

0d6e0f15acd74afea97a42d863ac0785

Post全部替换成InterfaceInfo

aa25674da9be41e6a71111ff6f8a3398

InterfaceInfoMapping全部替换成PostMapping,因为PostMapping是 springMVC 的语法。

3a76488199a54c138e568e795f5a76f0

这里我们要自己编写这个用户要传递的参数的对象。

2e0f8d5e6e694ccfa2fe5a79820b815d

所以说现在我们要自己去复制粘贴一下,我们要自己写一下这个参数;

model目录dto目录下创建interfaceInfo目录

531ef97452c347a1a44ca3ecc629cba5

复制post目录下的文件;PostDoThumbRequest是之前点赞的一个请求参数,不用复制。

ce51a0236b784850b886a8fd965e7106

把文件名中的Post改为InterfaceInfo

da3f72be43a542c7b66797c6433dd6dc

我们要自己编写这个用户要传递的参数的对象,而这个对象就是用来接收前端的请求参数,为了更好地满足不同表的设计,我们应该选择用户需要输入哪些字段;


💭 你可能会问,为什么要将其分成三个接口? 如果你们还在学校,可能觉得一个接口就能解决问题,但实际上,你们需要考虑写接口的目的 —— 是为了服务前端开发人员。

想象一下,如果你只用一个通用对象处理所有事情,当前端开发人员查看你的接口文档时,他们应该如何知道哪些参数是必需的,哪些参数是可选的?你可能需要额外添加备注来说明这一点。而且,如果有些参数并不需要前端提交,如userid、id等,你还需要把它们包含在文档中吗?这可能会造成前端开发人员的困扰。

另一个问题是,如果你在对象中公开了一些字段,而开发者又没有留意到,就可能导致问题。例如,如果你公开了 userid,而且在编码时没有注意,如果用户提交了一个 userid,然后这个 userid 被无意中写入数据库,导致用户控制了这个 userid,这将是一个问题。

如果你觉得写三个对象很麻烦,那你可以按照自己的开发习惯来。然而,从规范的角度出发,建议为每个接口创建一个单独的对象,除非这个对象是特别通用的。例如,对于只需要一个 ID 参数的删除请求,你可以将其转换为通用的。总的来说,通用的时候就使用通用对象,不通用的时候就不要强行通用。


找到entity目录下的InterfaceInfo

555cb0178e69495da8a2e0d33260393b

isDelete字段上添加@TableLogic

因为是逻辑删除,要添加@TableLogic注解,不然就被真实的删除了。

0bd727f16ffd48e5b5e597f4498cd9b9

复制以下InterfaceInfo的字段。

/**
 * 主键
 */
@TableId(type = IdType.AUTO)
private Long id;

/**
 * 名称
 */
private String name;

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

/**
 * 接口地址
 */
private String url;

/**
 * 请求头
 */
private String requestHeader;

/**
 * 响应头
 */
private String responseHeader;

/**
 * 接口状态(0-关闭,1-开启)
 */
private Integer status;

/**
 * 请求类型
 */
private String method;

/**
 * 创建人
 */
private Long userId;

/**
 * 创建时间
 */
private Date createTime;

/**
 * 更新时间
 */
private Date updateTime;

/**
 * 是否删除(0-未删, 1-已删)
 */
@TableLogic
private Integer isDelete;

替换掉InterfaceInfoAddRequest的字段。

package com.yupi.project.model.dto.interfaceInfo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * 创建请求
 *
 * @TableName product
 */
@Data
public class InterfaceInfoAddRequest implements Serializable {


    /**
     * 主键;
     * 在用户上传接口的时候,不用自己输入 id,后台会自动生成,所以不用用户上传;(刪掉)
     */
//    @TableId(type = IdType.AUTO)
//    private Long id;

    /**
     * 名称
     */
    private String name;

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

    /**
     * 接口地址
     */
    private String url;

    /**
     * 请求头
     */
    private String requestHeader;

    /**
     * 响应头
     */
    private String responseHeader;

    /**
     * 接口状态(0-关闭,1-开启)
     * 接口状态默认都是关闭,有默认值就不用用户上传;(刪掉)
     */
//    private Integer status;

    /**
     * 请求类型
     */
    private String method;

    /**
     * 创建人
     * 创建人由当前登录的用户自动同步到后台,是后台自动生成的,不用用户上传;(刪掉)
     */
//    private Long userId;

//   创建时间、更新时间、是否删除都是自动生成的,不用用户上传;(刪掉)
    /**
     * 创建时间
     */
//    private Date createTime;

    /**
     * 更新时间
     */
//    private Date updateTime;

    /**
     * 是否删除(0-未删, 1-已删)
     */
//    @TableLogic
//    private Integer isDelete;
}
package com.yupi.project.model.dto.interfaceInfo;

import lombok.Data;
import java.io.Serializable;

/**
 * 创建请求
 *
 * @TableName product
 */
@Data
public class InterfaceInfoAddRequest implements Serializable {

    /**
     * 名称
     */
    private String name;

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

    /**
     * 接口地址
     */
    private String url;

    /**
     * 请求头
     */
    private String requestHeader;

    /**
     * 响应头
     */
    private String responseHeader;

    /**
     * 请求类型
     */
    private String method;
}

再来编写查询对象,把复制的InterfaceInfo的字段,替换掉InterfaceInfoQueryRequest的字段。

package com.yupi.project.model.dto.interfaceInfo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.yupi.project.common.PageRequest;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.util.Date;

/**
 * 查询请求
 *
 * @author yupi
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class InterfaceInfoQueryRequest extends PageRequest implements Serializable {

    /**
     * 主键
     * 用户可能根据id查询
     */
//    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 名称
     * 用户可能根据名称查询
     */
    private String name;

    /**
     * 描述
     * 用户可能根据描述查询
     */
    private String description;

    /**
     * 接口地址
     * 用户可能根据地址查询
     */
    private String url;
//下面的都有可能
    /**
     * 请求头
     */
    private String requestHeader;

    /**
     * 响应头
     */
    private String responseHeader;

    /**
     * 接口状态(0-关闭,1-开启)
     */
    private Integer status;

    /**
     * 请求类型
     */
    private String method;

    /**
     * 创建人
     */
    private Long userId;

//    用户不太可能根据创建时间、更新时间、是否删除查询,一般都是范围查询(删除)
    /**
     * 创建时间
     */
//    private Date createTime;

    /**
     * 更新时间
     */
//    private Date updateTime;

    /**
     * 是否删除(0-未删, 1-已删)
     */
//    @TableLogic
//    private Integer isDelete;
}
package com.yupi.project.model.dto.interfaceInfo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.yupi.project.common.PageRequest;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.util.Date;

/**
 * 查询请求
 *
 * @author yupi
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class InterfaceInfoQueryRequest extends PageRequest implements Serializable {

    /**
     * 主键
     */
    private Long id;

    /**
     * 名称
     */
    private String name;

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

    /**
     * 接口地址
     */
    private String url;

    /**
     * 请求头
     */
    private String requestHeader;

    /**
     * 响应头
     */
    private String responseHeader;

    /**
     * 接口状态(0-关闭,1-开启)
     */
    private Integer status;

    /**
     * 请求类型
     */
    private String method;

    /**
     * 创建人
     */
    private Long userId;
}

InterfaceInfoUpdateRequest(更新请求) —— 哪些字段的值是可以交给用户或管理员整改的。

package com.yupi.project.model.dto.interfaceInfo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * 更新请求
 *
 * @TableName product
 */
@Data
public class InterfaceInfoUpdateRequest implements Serializable {
    /**
     * 主键
     * id不能改,但是我们更新对象需要知道是更新哪条数据
     */
//    @TableId(type = IdType.AUTO)
    private Long id;

// 下面的都有可能会改
    /**
     * 名称
     */
    private String name;

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

    /**
     * 接口地址
     */
    private String url;

    /**
     * 请求头
     */
    private String requestHeader;

    /**
     * 响应头
     */
    private String responseHeader;

    /**
     * 接口状态(0-关闭,1-开启)
     */
    private Integer status;

    /**
     * 请求类型
     */
    private String method;

    /**
     * 创建人
     * 创建人一般情况下不会改(删除)
     * 如果你限定只有管理员能改,修改创建人也是合理的
     */
//    private Long userId;

    // 创建时间、更新时间、是否删除也不能改
    /**
     * 创建时间
     */
//    private Date createTime;

    /**
     * 更新时间
     */
//    private Date updateTime;

    /**
     * 是否删除(0-未删, 1-已删)
     */
//    @TableLogic
//    private Integer isDelete;
}
package com.yupi.project.model.dto.interfaceInfo;

import lombok.Data;

import java.io.Serializable;

/**
 * 更新请求
 *
 * @TableName product
 */
@Data
public class InterfaceInfoUpdateRequest implements Serializable {
    /**
     * 主键
     */
    private Long id;

    /**
     * 名称
     */
    private String name;

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

    /**
     * 接口地址
     */
    private String url;

    /**
     * 请求头
     */
    private String requestHeader;

    /**
     * 响应头
     */
    private String responseHeader;

    /**
     * 接口状态(0-关闭,1-开启)
     */
    private Integer status;

    /**
     * 请求类型
     */
    private String method;
}

现在我们用来接收前端请求参数的对象全部写好了,回到InterfaceInfoController,引入这三个请求。

05ced5aaab614bacb71b914e3790a595

还有几个爆红的地方,如validInterfaceInfo(参数校验);

由于每个不同的表或对象需要校验的参数肯定是各不相同的,所以我们需要自己编写参数校验的代码。

ac01af6f33974b93890563b196169c84

具体来说,我们需要确定如何对这些接口信息进行参数校验;

复制PostServiceImpl的校验逻辑,粘贴到InterfaceInfoServiceImpl中。

8ce7510e81ce4d1d8cd4013a3674cb0d

选中InterfaceInfoServiceImpl中的Post,然后按住[Ctrl+R]。

b86ff6c5912c4d7fbc8b89709952c047

post替换成interfaceInfo

8b3c0e5b748e4585bfd4a751bb73484c

复制接口。

ccca18966700444b9b43afbcae54b045

粘贴到InterfaceInfoService

65a6aa3caa374101a1f00e12e6f1162f

接下来就要编写下图所示校验接口信息的方法。

88f66a4a804a457a9de16217a23d797c

先删除InterfaceInfoServiceImpl多余的内容。

b31af3ec11af4b47af6c165ef0832b56
package com.yupi.project.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.model.entity.InterfaceInfo;
import com.yupi.project.service.InterfaceInfoService;
import com.yupi.project.mapper.InterfaceInfoMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

/**
* @author Administrator
* @description 针对表【interface_info(接口信息)】的数据库操作Service实现
* @createDate 2023-06-04 21:40:56
*/
@Service
public class InterfaceInfoServiceImpl extends ServiceImpl<InterfaceInfoMapper, InterfaceInfo>
    implements InterfaceInfoService {
    @Override
    public void validInterfaceInfo(InterfaceInfo interfaceInfo, boolean add) {
        if (interfaceInfo == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // 创建时,所有参数必须非空
        if (add) {
            if (StringUtils.isAnyBlank()) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR);
            }
        }
        if (StringUtils.isNotBlank(content) && content.length() > 8192) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "内容过长");
        }
    }
}

在这里推荐安装一个插件,点击左上角 File → Settings → Plugins , 搜索GenerateAllSetter

95958176aee4473d9ff3814f29c08bac

ps.这个插件可以一键帮你生成对象的所有 get、set 方法、生成一个充满假数据的类,充满假数据的对象等等。

把光标放在interfaceInfo上,按[Alt+Enter],选择第二个,调用该对象所有的 get 方法。

ef6c89c4261f4768858b8f1f6a9a9309

瞬间生成 get 方法。

1f71fea7eeb24db4babf72ece6c967c3

这里我们就只拿接口名称进行校验测试,就假设用户第一次创建对象时,他必须传接口名称。

package com.yupi.project.service.impl;
import java.util.Date;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.model.entity.InterfaceInfo;
import com.yupi.project.service.InterfaceInfoService;
import com.yupi.project.mapper.InterfaceInfoMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

/**
* @author Administrator
* @description 针对表【interface_info(接口信息)】的数据库操作Service实现
* @createDate 2023-06-04 21:40:56
*/
@Service
public class InterfaceInfoServiceImpl extends ServiceImpl<InterfaceInfoMapper, InterfaceInfo>
    implements InterfaceInfoService {
    @Override
    public void validInterfaceInfo(InterfaceInfo interfaceInfo, boolean add) {
        // 判断接口信息对象是否为空,为空则抛出参数错误的异常
        if (interfaceInfo == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // 获取接口信息对象的名称
        String name = interfaceInfo.getName();

        // 如果是添加操作,所有参数必须非空,否则抛出参数错误的异常
        if (add) {
            if (StringUtils.isAnyBlank(name)) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR);
            }
        }
        // 如果接口名称不为空且长度大于50,抛出参数错误的异常,错误信息为"名称过长"
        // 本期写成<50,(没有解决),第二期视频中解决了
        if (StringUtils.isNotBlank(name) && name.length() > 50) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "名称过长");
        }
    }
}

校验搞定,回到InterfaceInfoController,还有几个报错的地方Content(模糊搜索),改成description(描述)。

abaac3dd803048d4baa49696441fceee

重启后端项目,访问 http://localhost:7529/api/doc.html,打开接口文档。

29f503901f0b4d61be78427d68cd3884

增加接口信息、删除接口信息、根据id获取接口信息.... 这些都生成好了。

后端搞定🥳

14. 前端开发

现在我们的前端需要做哪些事情呢?是不是要去实现用户登录?实现接口信息的增删改查?

1. 前端模板优化

1. 部分前端目录介绍

6419b1196b8543528e873730b729b0e4
目录介绍
b69f8a62ccf84877a3cafdfa954b4c9a
7d78f16ba6374494b67402d65aaeece2
6f9b14c1e54a4e4a869f63aafde8052f
2ca9ee5e87d0419d8b1e97b49654b64e
4472799615ce47078d0dfa67e504e61f
c217c7e0491b416ab2c650643482df9f

app.ts 是 Ant Design Pro 的核心入口文件,其中的 getInitialState 函数用于在我们首次打开页面时加载某些用户信息。例如,用户信息等。除非是登录页面,否则它会自动跳转,这都是由 Ant Design Pro 预先编写的逻辑,我们无需过分关注。

2. 启用美化配置

这里用webstorm作示范,vscode的,懒的配置,咳咳(绝不是找不到,别打别打🐶)

点击 File → Settings。

d4e0b93da8b14594914e33cbbe13d504

搜索eslint

cdf74cc6ca924422b2724f757a0d7d62

搜索prettier

1b6685b75f89460193f2fd39c9555618

这样的话,写代码的时候就会自动格式化了。

2. 前端代码自动生成

1. 介绍

我们现在需要解决的问题是如何让前端调用后台接口,实现接口信息的增删改查管理。通常,我们会在前端定义 TS 类型对象,并手动编写调用后台的方法,例如获取当前登录用户、退出登录等。写前端代码的同学们,你们是自己编写调用后台接口的代码还是利用了某种自动生成方法?相信一些同学已经尝试过使用自动生成方法。我们正在使用的 Ant Design Pro 框架,它已经支持自动化生成这些接口。现在,向大家介绍一个叫做 openapi 的插件,它可以帮助我们实现接口的自动生成。

接下来,我们来探讨如何实现接口的自动生成。如果后端已经定义了各种接口,我们如何自动生成相应的接口调用代码呢?我们需要一个文档或者介质来同步这些信息。这就是我们需要使用 openapi 规范的地方。只要为 oneapi 插件提供基于其规范的文档,就可以实现接口的自动生成。

那么,什么是 oneapi 呢?简单地说,oneapi 是一种接口文档的规范,可以理解为接口文档的格式或者规则。举个例子,我们常用的 Swagger 这种后端接口文档,就是遵循了 openapi 规范。

访问 http://localhost:7529/api/doc.html,查看接口文档的主页。

4c621c5a53fe4d918221cd40624addd9

这里有个v3/api-docs,访问 http://localhost:7529/api/v3/api-docs,这里能看见一堆 Json 的代码,这个东西就是遵循 openapi 的规范提供的 Json 的文档,第一行信息说明它是基于 openapi3 版本去生成的文档 。

e1aa425a2e8145f4b4a7454d91513cb0

所以我们如果现在已经有了现成的后端,想要前端直接生成调用这些后端的代码,只要把这个基于这个 openapi 规范的文档提供给前端就可以了。

2. 前端代码自动生成

复制 http://localhost:7529/api/v3/api-docs,回到前端,找到config.ts中的openAPI插件。

2251f64369af4a03bd87527970a713b1

把多余的删掉。

00cc1f3c82e94895805d126dfd0078d4

修改内容。

b2e2fa1a595a4643969bd5aa5c375fa3

打开package.json,找到openapi运行(或在终端输入yarn run openapi)。

8d8eae9031354f16bf7372cd906b98db

提示成功生成 service 文件。

7f211f83303b44e4ab8811af087d0864

services目录多出一个yuapi-backend目录,里面自动生成调用后端指定接口的方法代码。

cc4310372d034deebf5f55cf5e7be4f1

可以点击interfaceInfoController.ts查看,生成了后端接口方法全部的代码。

aecbfaa7dfba426aa3a468f46f459c9c

而且还生成了 ts 类型,比如选中InterfaceInfoAddRequest(新增接口信息),然后按[Ctrl+鼠标左键]。

52d656220d1447ccb0264ede7e6d4700

就可以看见它的请求参数类型:name 名称、description 描述、url 接口地址、requestHeader 请求头、responseHeader 响应头、method 请求类型。

1b01b2d509184ffabc7fca3bc0f414a0

这个和我们在后端写的InterfaceInfoAddRequest(创建请求)完全一致:

e06b068b004f46f0ac1b1de72417156a

ps.所以,当有同学问为什么每个请求参数都需要单独编写一个方法,单独创建一个对象时,现在你应该明白了原因。是的,这样做可以根据这些对象直接生成清晰、易于使用的前端文档。这是不是非常便捷呢?

3. 修改请求配置

这里的请求配置(requestErrorConfig.ts)写的不是很好(个人感觉🐶)。

f700b68d831f49d092a4675b50ab1433

重命名为requestConfig.ts

68519147f8244845888cc0c987d0384b

回到requestConfig.ts

ebb4966d27214197809fbbd1b67208a5

errorConfig改为requestConfig

7804e9e52c924097bedc3e4ce3b55d37

app.tsx里引入修改了的请求配置。

26132e50ec04414e84700edb8eae2b68

虽然模板(requestConfig.ts)生成的代码原本只期望你在这里处理错误,但是在app.tsx中又完全引用了这个对象,我们直接将所有的请求配置都写在一个对象中,不需要在app.tsx中再嵌套一个对象。

requestConfig.ts编写前端的请求地址为后端地址http://localhost:7529

2b632ae575dc4764be48d84c83b0d8cb

4. 前后端对接

我们要请求真实的后端接口,用dev模式(或在前端终端输入yarn run dev)去启动项目,访问http://localhost:8000/;

登录后发现登录失败,鼠标右键 → 选择检查 → 点击网络,查看一下登录请求。

c14d0b62a20a4b29b4be691e22adf0c1

状态码为 404,因为它请求的接口地址不对,这个接口请求的接口地址是脚手架自动生成的。

0e0ab39e6e76409d81f6e6b1917aa7b9

所以要改成自己的后端,去找到登录的index.tsx, 当用户点击登录按钮之后,会执行handleSubmit提交函数。

ae51eee74ef8462c99d82fac668951be

这里实际上是请求咱们的后端,要改成刚刚用 openapi 生成的调用后端的方法。

7913582b96994110a5cd58dffff6952b

为什么有现成的方法?因为我们用 openapi 自动生成了(如图所示):

55404a8a635445e3ae6f4e602be8a307

所以这里就不用再去写方法了,直接改成参数即可;

type删掉,这里values的类型本来是 Ant Design Pro 框架生成的类型,改成自己的后端类型。

698d300370414120bf2c04b9989c99ff

修改响应判断。

7581be7697d242898dec709377cef2a7

修改后:

3c6649897edb443b8711e089270f46b6
const handleSubmit = async (values: API.UserLoginRequest) => {
  try {
    // 调用 userLoginUsingPOST 方法进行用户登录,values 为包含登录信息(如用户名和密码)的对象
    const res = await userLoginUsingPOST({
      ...values,
    });
    // 检查返回的 res 对象中是否包含 data 属性,如果包含则表示登录成功
    if (res.data) {
      // 创建一个新的 URL 对象,并获取当前 window.location.href 的查询参数
      const urlParams = new URL(window.location.href).searchParams;
      // 将用户重定向到 'redirect' 参数指定的 URL,如果 'redirect' 参数不存在,则重定向到首页 ('/')
      history.push(urlParams.get('redirect') || '/');
      // 用登录用户的数据更新初始状态
      setInitialState({
        loginUser: res.data
      });
      return;
    }
    // 如果抛出异常
  } catch (error) {
    // 定义默认的登录失败消息
    const defaultLoginFailureMessage = '登录失败,请重试!';
    // 在控制台打印出错误
    console.log(error);
    // 使用 message 组件显示错误信息
    message.error(defaultLoginFailureMessage);
  }
};

然后把它给我们这个页面生成的一些类型都改为自己的。

06b58c3e5bdc478b912a6a6d7160cf63

LoginParams改为UserLoginRequest

4cb0cc642dbf4c89a583b05f3e7112b8

把表单用户名和密码的name改成后端的字段:userAccount、userPassword;

这样的话,用户在表单输入的值才会真正传递给后台。

92b66e26af6d49e38c261612cf0070b0

ps.通常,前端从后台加载用户信息后,会将数据保存到全局状态中。当现在并未保存到全局状态,所以要进行相应的修改。找到app.tsx,这里有一个 getInitialState 的方法。每当首次访问页面时,就会被执行用以获取用户信息和当前的全局状态。

找到typings.d.ts,定义全局状态类型(InitialState),保存用户登录的状态(当成全局变量即可)。

c483538178d340ccb12858feb125f6cf

找到app.tsx(全局入口文件)。

58b7048f8ee54e58a841b8e89071aa87

把上图的对象改成我们刚刚设置的类型。

a1453dbf945d4e81b141baddff0b2b86

我们把状态改了之后,这里有一个函数(如下图所示)。这个函数做什么事情呢?

当页面首次加载时,获取要全局保存的数据,比如用户登录信息,这里默认生成的代码就有一个fetchUserInfo(获取当前用户信息)。

8c94e4eb6ae1474dae970e8d5d1decce

queryCurrentUser改成自己的接口getLoginUserUsingGET(也是 openapi 自动生成),也不用传任何参数:

b3d2c4596b734ad6a79e9f045665440a

接下来要把状态保存至全局状态中,先定义一个全局状态。

404b52eba3374f478b76444a85044cd3

删除多余的内容。

3118de5f8d79438093bdfc8d60de77c6
export async function getInitialState(): Promise<InitialState> {
  // 当页面首次加载时,获取要全局保存的数据,比如用户登录信息
    const state: InitialState = {
      // 初始化登录用户的状态,初始值设为undefined
      loginUser: undefined,
    }
    try {
      // 调用getLoginUserUsingGET()函数,尝试获取当前已经登录的用户信息
      const res = await getLoginUserUsingGET();
      // 如果从后端获取的数据不为空,就把获取到的用户数据赋值给state.loginUser
      if (res.data) {
        state.loginUser = res.data;
      }
    // 如果在获取用户信息的过程中发生错误,就把页面重定向到登录页面
    } catch (error) {
      history.push(loginPath);
    }
    // 返回修改后的状态
    return state;
  };

往下滑,把currentUser改成loginUser,把name改成userNameavatar改成userAvatar

7d056738b3de4604923b6cf54c81ae64

dev模式启动前端,访问 http://localhost:8000/,注册可以去用户中心复制一个进行改造,这里之前在后端接口文档注册过一个账号。

a5a9149f696242ea82fff18179ddd771

我们就用它进行登录。

00dc38d10ad64819918309a5554eddb6

登录成功。

4b3c6a77c9a0430791005d9da2bd6653

左侧菜单栏没有管理员页面,因为我们不是管理员,所以少了一个页面。

1. 问题:需要点击两次登录

此时你突然发现,诶,得点击两次登录按钮,才能跳转到登录之后的页面,还没有报错。

ps.好家伙.jpg 不做了不做了,这项目不做也罢,咳咳,别打别打🐶

这个问题是由于 React 组件更新的异步性质引起的。在调用 setInitialState 后,状态可能并没有立即更新,而你又立即执行了 history.push,试图导航到一个依赖于新状态的页面。这就导致了你需要点击两次登录按钮才能看到预期的页面。

9840d63be4b54fe59bf6c2530c7beb0d

为了避免这个问题,可以尝试以下两种方式:

  1. 确保 setInitialState 在 history.push 之前完成: 你可以尝试使用 async/await 来确保 setInitialState 在 history.push 之前完成。在 index.tsx 中的 handleSubmit 修改为以下形式:
const handleSubmit = async (values: API.UserLoginRequest) => {
  try {
    const res = await userLoginUsingPOST({
      ...values,
    });
    if (res.data) {
      const urlParams = new URL(window.location.href).searchParams;
      await setInitialState({
        loginUser: res.data
      });
      history.push(urlParams.get('redirect') || '/');
      return;
    }
  } catch (error) {
    const defaultLoginFailureMessage = '登录失败,请重试!';
    console.log(error);
    message.error(defaultLoginFailureMessage);
  }
};

这个方法进入登录页之后得等上 5 秒左右,然后才能点击登录,点击一次就可以;

如果我们访问后一出现登录页就点击登录,就还需要点击两次登录,好像才解决一半问题诶;

这里是因为 setInitialState 函数的执行需要等待一段时间,或者函数内部有某些延迟操作,导致 setInitialState 函数并没有立即将状态更新,直接加个 setTimeOut 延迟 100ms,确保 setInitialState 在 history.push 之前完成。

const handleSubmit = async (values: API.UserLoginRequest) => {
    try {
      // 登录
      const res = await userLoginUsingPOST({
        ...values,
      });
      // 如果登录成功(响应有数据)
      if (res.data) {
        // 获取当前URL的查询参数
        const urlParams = new URL(window.location.href).searchParams;
        // 设置一个延迟100毫秒的定时器
        // 定时器触发后,导航到重定向URL,如果没有重定向URL,则导航到根路径
        setTimeout(() => {
          history.push(urlParams.get('redirect') || '/');
        }, 100);
        // 更新全局状态,设置登录用户的信息
        setInitialState({
          loginUser: res.data
        });

        return;
      }
    // 如果抛出异常
    } catch (error) {
      // 定义默认的登录失败消息
      const defaultLoginFailureMessage = '登录失败,请重试!';
      // 在控制台打印出错误
      console.log(error);
      // 使用 message 组件显示错误信息
      message.error(defaultLoginFailureMessage);
    }
  };
  1. 来自球友@YukeSeko 👉 [登录页需要点击两次登录才能进入的问题]

5. 登录状态失效

登录后,刷新页面(按 F5),又要重新登录,说明前端没有 cookie,登录状态失效了;

有些同学可能会纠结于如何保存这个状态,过程是这样的:

当你成功登录后,后端会在前端设置一个 cookie。设立 cookie 后,每当前端页面刷新时,它会执行什么操作呢?大家还记得之前提到的 app.tsx 文件吗?每次页面首次加载时,它都会执行 getInitialState 函数。所以,当我们首次加载页面的时候,这个方法就会被调用以获取当前登录用户的信息。获取到信息后,我们只需将其设置到全局变量中,初始化变量即可。

requestConfig.ts中添加上withCredentials: true

f775190d3e68443789ee435a2afa26b5

再次登录,然后刷新页面,没有跳转回登录页面,成功。

5e6ae49bc1444fa780621ee59b541da7

6. 头像加载

之前登录之后会发现右上角的头像一直在加载状态,这里使用ooxx导航头像,将链接粘贴至后端数据库中。

893ad81d16dd4b78bcd10c1dcd0b3f75
https://yupi.icu/logo.png

用户名设置为:老谷牛。

ac1673d4b86b4bf4a26c42e8e1c708fd

在 AvatarDropdown.tsx 按[Ctrl+F]。

109bfec83d0e4666a78e017762ef9fc9

把所有的 currentUser 换成 loginUser,name 换成 userName。

ps.webstorm按[Ctrl+R]

043bdb5d9517466aa159886fac94c802

7. 实现注销

找到AvatarDropdown.tsx

33ca91d06b874c969a447f270594a34d

把默认的注销方法(loginOut)改成我们后端自己的方法(userLogoutUsingPOST)。

6d9e649096614da3a22486e884edf3da

在 userLogoutUsingPOST 后编写跳转回登录页面。

c786196564604c2d8c388d105fb20066

这样用户点击退出登录后,会先执行登出操作,并清除全局状态中的用户信息,然后跳转到登录页面。

把原先的 loginOut 删掉。

92f2756b44ef48fe8445acec1af1ba6d
import { userLogoutUsingPOST } from '@/services/yuapi-backend/userController';
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import { history, useModel } from '@umijs/max';
import { Spin } from 'antd';
import type { MenuInfo } from 'rc-menu/lib/interface';
import React, { useCallback } from 'react';
import { flushSync } from 'react-dom';
import HeaderDropdown from '../HeaderDropdown';

export type GlobalHeaderRightProps = {
  menu?: boolean;
  children?: React.ReactNode;
};

export const AvatarName = () => {
  const { initialState } = useModel('@@initialState');
  const { loginUser } = initialState || {};
  return <span className="anticon">{loginUser?.userName}</span>;
};

export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, children }) => {
  const actionClassName = useEmotionCss(({ token }) => {
    return {
      display: 'flex',
      height: '48px',
      marginLeft: 'auto',
      overflow: 'hidden',
      alignItems: 'center',
      padding: '0 8px',
      cursor: 'pointer',
      borderRadius: token.borderRadius,
      '&:hover': {
        backgroundColor: token.colorBgTextHover,
      },
    };
  });
  const { initialState, setInitialState } = useModel('@@initialState');

  const onMenuClick = useCallback(
    (event: MenuInfo) => {
      const { key } = event;
      if (key === 'logout') {
        flushSync(() => {
          setInitialState((s) => ({ ...s, loginUser: undefined }));
        });
        userLogoutUsingPOST();
        const { search, pathname } = window.location;
        const redirect = pathname + search;
        history.replace('/user/login', { redirect });
        return;
      }
      history.push(`/account/${key}`);
    },
    [setInitialState],
  );

  const loading = (
    <span className={actionClassName}>
      <Spin
        size="small"
        style={{
          marginLeft: 8,
          marginRight: 8,
        }}
      />
    </span>
  );

  if (!initialState) {
    return loading;
  }

  const { loginUser } = initialState;

  if (!loginUser || !loginUser.userName) {
    return loading;
  }

  const menuItems = [
    ...(menu
      ? [
          {
            key: 'center',
            icon: <UserOutlined />,
            label: '个人中心',
          },
          {
            key: 'settings',
            icon: <SettingOutlined />,
            label: '个人设置',
          },
          {
            type: 'divider' as const,
          },
        ]
      : []),
    {
      key: 'logout',
      icon: <LogoutOutlined />,
      label: '退出登录',
    },
  ];

  return (
    <HeaderDropdown
      menu={{
        selectedKeys: [],
        onClick: onMenuClick,
        items: menuItems,
      }}
    >
      {children}
    </HeaderDropdown>
  );
};

测试一下,前端在终端输入yarn run dev,访问 http://localhost:8000/,登录。

be9828d6d52c47528dfb091879eb487e

登录之后点击头像→退出登录,退出成功,跳转回登录页。

3e43dfceb67440c88d2c3dddfecd0ae5

大家有没有觉得现在编写前端代码变得不那么困难?我们似乎没有怎么手写代码,所有与后台的请求接口以及类型代码都是自动生成的,我们只需要在对应的位置去调用这些方法,使用这些函数,利用这些代码即可。

想一想,我们利用这种自动生成代码的方式有什么优势呢?一个最显著的好处是,如果有一天后台接口发生了变化,比如,现在的创建接口,前端的参数对象是 description,假如有一天我把这个 description 改成了descript。那么我们应该如何处理呢?需要在前端再修改所有的代码,再去替换什么东西吗?其实并不必要。你只需要在前端再生成一下接口即可。只需再使用 openapi 生成,它就会自动地根据后台的最新接口文档重新生成services。

因为方法名没有变,只有参数变了,所以基本上不会有什么影响。所以说,自动生成代码的优点在此处就显得非常重要,它实现了同步,同时还降低了沟通成本。当然,如果遇到一些特殊情况,我们可能还需要进行沟通,具体情况具体分析。

8. 接口管理

现在我们要做一个只有管理员可以使用的接口信息管理。

首先,我们要了解前端是怎么区分权限的。刚刚我们登录后,会发现原本有三个菜单栏,但是管理员菜单消失了。那么这个现象是如何发生的呢?原因就在于前端进行了权限校验。

找到access.ts,这个是 Ant Design Pro 为我们内置的一套权限管理机制。

d6aebb3cf15541fe8023ef8b86569ede

修改一下,取全局初始化状态(InitialState)的loginUser,根据当前登录用户判断它是否有 管理员权限 or 用户权限。

36e2824086984371ba7616be915f6ff3
/**
 * @see https://umijs.org/zh-CN/plugins/plugin-access
 * */
export default function access(initialState: InitialState | undefined) {
  const { loginUser } = initialState ?? {};
  return {
    canUser: loginUser,
    // 如果loginUser存在,并且用户角色为 'admin',说明该用户是管理员
    canAdmin: loginUser?.userRole === 'admin',
  };
}

然后访问到后端,把账号角色改为admin

07d02e38e5104856b8733c18e703dc41

回到前端页面,管理员页面出现了。

c824db2dcf5042d38daacee76e31fe07

现在我们要把接口信息填充进来,来改造一下页面。

借鉴一下表格页,看看怎么写的。

84d55d5a62a041b394372f8ad9ccbfe2

webstorm按 [Ctrl+Shift+ 减号(-)]全局压缩 — 把代码块都折叠起来。

47683c3ed83b4d0ca1831698e9e34862

vscode没翻到类似的功能,手动折叠。

37d0f7c86bf54823aad61058b8952cbf

第一个:处理新建按钮的功能。

21da58ac650b40678a1b2406933678c7

比如点击新建→ 弹窗,就是handleAdd做的事情。

fd94e5186b7a4d74b21bec82413d01cb

第二个:处理修改数据的功能。 比如点这个数据的修改,它会去处理更新,就是handleUpdate做的事情。

41f03798ab184ea7af49c21d678faf57

第三个:处理删除数据的功能。

0067abe453d043b48682096180c10a30

往下滑,这里就是 react 写页面的地方,非常简单,这个表格是用到了 Ant Design Pro components 的 ProTable 组件。

1a6649afa2fe41f7bfd8a39d621b9e87

这个组件只要传入一下你的表格要有哪些列,每一列要怎么展示,就能够自动生成数据。所以现在我们要做的事情就是告诉这个组件它有哪些列,就是要改一下 columns,选中 columns,按[Ctrl+鼠标左键]点进来。

f3f109122f5f4fd9aaccd5c61b580a16

这里的规则名称、描述、服务调用次数、状态....和前端页面的表格页表头一致。

25f78d38fb1747e78724e5b191d440f0

我们现在改成自己的就行了,字段的名称根据接口文档改。

92b9131047204dd2bd91ea5621f0f5fd
import { addRule, removeRule, rule, updateRule } from '@/services/ant-design-pro/api';
import { PlusOutlined } from '@ant-design/icons';
import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components';
import {
  FooterToolbar,
  ModalForm,
  PageContainer,
  ProDescriptions,
  ProFormText,
  ProFormTextArea,
  ProTable,
} from '@ant-design/pro-components';
import '@umijs/max';
import { Button, Drawer, Input, message } from 'antd';
import React, { useRef, useState } from 'react';
import type { FormValueType } from './components/UpdateForm';
import UpdateForm from './components/UpdateForm';

/**
 * @en-US Add node
 * @zh-CN 添加节点
 * @param fields
 */
const handleAdd = async (fields: API.RuleListItem) => {
  const hide = message.loading('正在添加');
  try {
    await addRule({
      ...fields,
    });
    hide();
    message.success('Added successfully');
    return true;
  } catch (error) {
    hide();
    message.error('Adding failed, please try again!');
    return false;
  }
};

/**
 * @en-US Update node
 * @zh-CN 更新节点
 *
 * @param fields
 */
const handleUpdate = async (fields: FormValueType) => {
  const hide = message.loading('Configuring');
  try {
    await updateRule({
      name: fields.name,
      desc: fields.desc,
      key: fields.key,
    });
    hide();
    message.success('Configuration is successful');
    return true;
  } catch (error) {
    hide();
    message.error('Configuration failed, please try again!');
    return false;
  }
};

/**
 *  Delete node
 * @zh-CN 删除节点
 *
 * @param selectedRows
 */
const handleRemove = async (selectedRows: API.RuleListItem[]) => {
  const hide = message.loading('正在删除');
  if (!selectedRows) return true;
  try {
    await removeRule({
      key: selectedRows.map((row) => row.key),
    });
    hide();
    message.success('Deleted successfully and will refresh soon');
    return true;
  } catch (error) {
    hide();
    message.error('Delete failed, please try again');
    return false;
  }
};
const TableList: React.FC = () => {
  /**
   * @en-US Pop-up window of new window
   * @zh-CN 新建窗口的弹窗
   *  */
  const [createModalOpen, handleModalOpen] = useState<boolean>(false);
  /**
   * @en-US The pop-up window of the distribution update window
   * @zh-CN 分布更新窗口的弹窗
   * */
  const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
  const [showDetail, setShowDetail] = useState<boolean>(false);
  const actionRef = useRef<ActionType>();
  const [currentRow, setCurrentRow] = useState<API.RuleListItem>();
  const [selectedRowsState, setSelectedRows] = useState<API.RuleListItem[]>([]);

  /**
   * @en-US International configuration
   * @zh-CN 国际化配置
   * */
  // 首先把变量的类型改成我们接口的类型
  const columns: ProColumns<API.InterfaceInfo>[] = [
    {
      title: 'id',
      dataIndex: 'id',
      valueType: 'index',
    },
    {
      title: '接口名称',
      //name对应后端的字段名
      dataIndex: 'name',
      // tip不用管,一个规则
      // tip: 'The rule name is the unique key',

      // render不用管,它是说渲染类型,默认我们渲染类型就是text
      // render: (dom, entity) => {
      //   return (
      //     <a
      //       onClick={() => {
      //         setCurrentRow(entity);
      //         setShowDetail(true);
      //       }}
      //     >
      //       {dom}
      //     </a>
      //   );
      // },

      // 展示文本
      valueType: 'text'
    },
    {
      title: '描述',
       //description对应后端的字段名
      dataIndex: 'description',
      // 展示的文本为富文本编辑器
      valueType: 'textarea',
    },
    {
      title: '请求方法',
      dataIndex: 'method',
      // 展示的文本为富文本编辑器
      valueType: 'text',
    },
    {
      title: 'url',
      dataIndex: 'url',
      valueType: 'text',
    },
    {
      title: '请求头',
      dataIndex: 'requestHeader',
      valueType: 'textarea',
    },
    {
      title: '响应头',
      dataIndex: 'responseHeader',
      valueType: 'textarea',
    },
    {
      title: '状态',
      dataIndex: 'status',
      hideInForm: true,
      valueEnum: {
        0: {
          text: '关闭',
          status: 'Default',
        },
        1: {
          text: '开启',
          status: 'Processing',
        },
      },
    },
    {
      title: '创建时间',
      dataIndex: 'createTime',
      valueType: 'dateTime',
    },
    {
      title: '更新时间',
      dataIndex: 'updateTime',
      valueType: 'dateTime',
    },
    {
      title: '操作',
      dataIndex: 'option',
      valueType: 'option',
      render: (_, record) => [
        <a
          key="config"
          onClick={() => {
            handleUpdateModalOpen(true);
            setCurrentRow(record);
          }}
        >
          配置
        </a>,
        <a key="subscribeAlert" href="https://procomponents.ant.design/">
          订阅警报
        </a>,
      ],
    },
  ];
  return (
    <PageContainer>
      <ProTable<API.RuleListItem, API.PageParams>
        headerTitle={'查询表格'}
        actionRef={actionRef}
        rowKey="key"
        search={{
          labelWidth: 120,
        }}
        toolBarRender={() => [
          <Button
            type="primary"
            key="primary"
            onClick={() => {
              handleModalOpen(true);
            }}
          >
            <PlusOutlined /> 新建
          </Button>,
        ]}
        request={rule}
        columns={columns}
        rowSelection={{
          onChange: (_, selectedRows) => {
            setSelectedRows(selectedRows);
          },
        }}
      />
      {selectedRowsState?.length > 0 && (
        <FooterToolbar
          extra={
            <div>
              已选择{' '}
              <a
                style={{
                  fontWeight: 600,
                }}
              >
                {selectedRowsState.length}
              </a>{' '}
              项 &nbsp;&nbsp;
              <span>
                服务调用次数总计 {selectedRowsState.reduce((pre, item) => pre + item.callNo!, 0)}</span>
            </div>
          }
        >
          <Button
            onClick={async () => {
              await handleRemove(selectedRowsState);
              setSelectedRows([]);
              actionRef.current?.reloadAndRest?.();
            }}
          >
            批量删除
          </Button>
          <Button type="primary">批量审批</Button>
        </FooterToolbar>
      )}
      <ModalForm
        title={'新建规则'}
        width="400px"
        open={createModalOpen}
        onOpenChange={handleModalOpen}
        onFinish={async (value) => {
          const success = await handleAdd(value as API.RuleListItem);
          if (success) {
            handleModalOpen(false);
            if (actionRef.current) {
              actionRef.current.reload();
            }
          }
        }}
      >
        <ProFormText
          rules={[
            {
              required: true,
              message: '规则名称为必填项',
            },
          ]}
          width="md"
          name="name"
        />
        <ProFormTextArea width="md" name="desc" />
      </ModalForm>
      <UpdateForm
        onSubmit={async (value) => {
          const success = await handleUpdate(value);
          if (success) {
            handleUpdateModalOpen(false);
            setCurrentRow(undefined);
            if (actionRef.current) {
              actionRef.current.reload();
            }
          }
        }}
        onCancel={() => {
          handleUpdateModalOpen(false);
          if (!showDetail) {
            setCurrentRow(undefined);
          }
        }}
        updateModalOpen={updateModalOpen}
        values={currentRow || {}}
      />

      <Drawer
        width={600}
        open={showDetail}
        onClose={() => {
          setCurrentRow(undefined);
          setShowDetail(false);
        }}
        closable={false}
      >
        {currentRow?.name && (
          <ProDescriptions<API.RuleListItem>
            column={2}
            title={currentRow?.name}
            request={async () => ({
              data: currentRow || {},
            })}
            params={{
              id: currentRow?.name,
            }}
            columns={columns as ProDescriptionsItemProps<API.RuleListItem>[]}
          />
        )}
      </Drawer>
    </PageContainer>
  );
};
export default TableList;

查看前端的表格页,表头已经变了,只不过现在没有数据,我们只要知道这个表格组件什么时候去请求数据就可以了。

9f63a6b1ffd94355ac460bb5ef9bc737

回到表格页,这里有个 request 方法,这个地方其实就是发起一个请求。

1d75025a48e84e37b26c1ba42edc7fa1

我们直接把这个请求函数改成自己的。

5afc45c9c1164e7093b3aaba5948f16d

🪔 这个 request 会在什么时候触发请求?

一是当你刚打开页面或刚加载表格时;

二是当你手动点击刷新按钮时;

三是当你点击查询按钮时;

也就是说这个请求函数何时被调用,完全由这个组件来管理。你只需要设定请求函数如何调用,以及应该请求哪个后端接口即可。我们不再需要反复绑定和编写事件处理程序。


然后发现 request 报错了,因为它传递的参数不太一致。

521c76aa25894d9199baf9fd00cf75e4

看看前端页面→表格页,也报错了,鼠标右键选择检查网络,按F5刷新,看看有没有拿到数据,拿到了数据。

8f0e0e272e25472ca2bc590ee5bc57ec

已经获取到了数据,但是为何会报错呢?大家猜猜看,如果在替换接口后出现错误,可能的原因是什么?如果你遇到这种情况,你会如何进行排查呢?

首先,我们需要确定你的请求参数与 request 的请求参数是否一致。其次,你的响应值是否与 request 的响应值相匹配。因此,建议大家尽量避免完全替换。


把光标放在 request 上,按[Ctrl+鼠标左键]。

a2e0f11a15364858b789e664eb55d53b

看一下 request 接收什么参数。

5906ea7e106f458bab6ce13215e37b70

复制参数,粘贴到 request 里。

22e7194fa63240e79bcdeb7c1ca4ea2c
(params: U & {
        pageSize?: number;
        current?: number;
        keyword?: string;
    }, sort: Record<string, SortOrder>, filter: Record<string, (string | number)[] | null>)

然后看一下它要求的响应对象是什么,把光标放在 RequestData 上,按[Ctrl+鼠标左键]。

515504ced54e461b8e25d6e06060d84e

写一下这个函数再去调用。

68b75298345943f38c52e05597646c25

虽然这里有报错,但是这是 ts 一些语法层面的问题,不用管它。

request={async (params, sort: Record<string, SortOrder>, filter: Record<string, React.ReactText[] | null>) => {
          const res = await listInterfaceInfoByPageUsingGET({
            ...params
          })
          if (res?.data) {
            return  {
              data: res?.data.records || [],
              success: true,
              total: res.total,
            }
          }
        }}

回到前端页面,刷新一下,在表格页成功显示数据。

48c87a1738aa4f64b8b46ab46e556e13

实际上我们做了哪些工作呢?我们只是使用了 ProTable 表格,定义了表格应该有的列,然后编写了请求方法并修改了请求后台的接口。这些接口并不是我们手动编写的,而是自动生成的。只要大家遵循这个流程,反复尝试几次,实现增删改查和登录等功能只需要几分钟的时间。