MemoryAPI 忆汇廊-开发文档

本文最后更新于:2 个月前

MemoryAPI 忆汇廊

前端代码仓库:memory-api-frontend

☕ 项目概述

这个项目是一个基于 Spring Cloud + React 的全栈微服务架构 API 接口开放平台,致力于提供丰富的 API 接口管理与调试 工具、灵活的 计费与限制管理 功能,为开发者提供高效安全易用的服务,助力企业创新和数字化转型。

🥘 效果展示

用户登录

image-20240226093246181

丰富的接口服务

image-20240224170147055

接口调用

image-20240224163717124

SDK 集成

image-20240226200537043

流量监控

image-20240224170440069

注册用户管理

image-20240224160321907

接口发布/下线

image-20240224160344953

个人信息管理

image-20240224163806434

🍚 使用场景

  • 开放平台的接口提供:适用于需要提供 API 接口给其他开发者或应用程序使用的项目。

  • 接口管理和调试:管理员和开发者可以使用该平台进行接口管理、调试和监控。

  • 应用程序开发:开发者可以在代码中使用提供的 SDK 快速调用接口,加速应用程序开发。

  • 计费与限制管理:适用于需要对接口调用进行计费和限制的项目,提供了灵活的计费和充值系统。

🥩 核心功能与特点

  • 多元 API 生态:平台提供多样化的 API 接口,涵盖各种应用场景和需求,满足不同开发者的需求。接口文档详尽,易于理解和使用,方便开发者快速集成和使用。

  • 高效 SDK 集成:平台提供多种编程语言和框架的客户端 SDK,简化开发者调用接口的过程。SDK 稳定、兼容性好,提供简洁易用的 API,提高开发效率。

  • 实时流量监控:平台提供热门接口调用排行榜,展示最受欢迎和最常用的接口。提供详细的流量统计分析,帮助开发者了解接口的使用情况和性能表现。

  • 资源集中管理:管理员可以全面管理平台的资源,包括注册用户信息、接口的增删管理、发布上线等。提供灵活的权限设置和角色划分,确保不同管理员之间的权限互不干扰且职责明确。

  • 个人信息管理:用户可以注册并登录平台,管理自己的个人信息,包括账户设置、密码修改等。平台保障用户信息的安全性和隐私性,遵守相关法律法规。

🍜 访问地址

暂未部署上线,点击跳转至:个人博客 MemorySearch 开发文档

🍝 架构设计

原图链接:项目架构图

image-20240224105144169

🍺 技术选型

后端

  • Java 核心知识:精通 Java 语言的核心特性,包括集合类、异常处理机制,以及泛型、注解等高级特性。
  • Lambda 表达式:能够熟练运用 Lambda 表达式简化代码,提升代码的可读性和可维护性。
  • 工具库:熟练运用 Hutool、Apache Commons 等工具库,为日常开发提供便捷和高效的支持。
  • SSM + Spring Boot:熟悉 Spring、SpringMVC、MyBatis 组成的 SSM 框架,以及基于 Spring Boot 的快速开发模式。
  • MyBatis Plus + MyBatis X:能够利用 MyBatis Plus 的增强功能,结合 MyBatis X 自动化工具,实现高效的 CRUD 代码生成。
  • MySQL 数据库:精通 MySQL 数据库设计,掌握索引优化、性能调优等关键技能,通过 Explain 分析等手段不断提升数据库性能。
  • API 签名认证 :熟悉 API 签名认证机制,确保数据传输的安全性。
  • 用户权限管理:具备实现用户角色、权限管理的经验,能够设计并实现复杂的权限控制逻辑。
  • Spring Boot Starter SDK:熟悉 Spring Boot Starter 的扩展机制,能够快速集成第三方 SDK,满足业务需求。
  • Nacos:掌握 Nacos 作为服务注册与发现、配置管理中心的用法,为分布式系统提供稳定的支持。
  • Dubbo RPC :熟悉 Dubbo RPC 框架,了解服务治理和远程调用机制,为微服务架构提供强大的通信能力。
  • Spring Cloud Gateway:能够使用 Spring Cloud Gateway 实现 API 网关,提供统一的请求入口、访问控制等功能。
  • Git:熟练使用 Git 进行版本控制,保证代码的安全和可追溯性。
  • IDEA:精通 IntelliJ IDEA 开发工具,利用其强大的功能提升开发效率。
  • ChatGPT:能够利用 ChatGPT 进行需求理解、代码片段生成等辅助开发工作。
  • Swagger:熟悉 Swagger 文档生成工具,为 API 提供清晰、规范的文档支持。
  • Navicat:掌握 Navicat 数据库管理工具,方便日常数据库操作和维护。

前端

  • React 核心开发:掌握 React.js 框架,能够根据业务需求定制前端模板,实现高效的前端页面渲染。
  • Ant Design Pro:熟悉 Ant Design Pro 框架,能够快速搭建企业级的前端应用。
  • Ant Design:熟悉 Ant Design 组件库,能够快速构建美观、功能丰富的前端界面。
  • Echarts:掌握 Echarts 数据可视化库,为前端界面提供丰富的图表展示功能。
  • Axios:用于发送 HTTP 请求,与后端 API 进行交互。
  • ECharts:用于数据可视化,展示统计图表。
  • VS Code:精通 Visual Studio Code 开发工具,利用其丰富的插件和强大的功能提升开发体验。
  • WebStorm IDE:熟悉 WebStorm IDE,为前端开发提供稳定的支持。

🍰 快速启动

拉取代码后,应该如何运行该项目?

后端

修改配置文件

  • 配置 Nacos、MySQL、Redis 为本机地址:
1
2
3
4
5
6
# 数据库配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xxx
username: xxx
password: xxx
1
2
3
4
5
6
7
# Redis 配置
redis:
database: 0
host: localhost
port: 6379
timeout: 5000
password: Dw990831
1
2
3
4
5
6
7
8
9
10
# Nacos 配置
dubbo:
application:
name: dubbo-springboot-demo-provider
protocol:
name: dubbo
port: -1
registry:
id: nacos-registry
address: nacos://localhost:8848

额外安装

  • 在本地安装 Nacos:Nacos 下载地址

  • 在 Nacos bin 目录下执行以下命令,启动 Nacos:

1
startup.cmd -m standalone

启动微服务

  • 依次启动 memory-core、memory-gateway、memory-client 微服务

前端

::: warning 注意
确保本地 Node.js 环境配置完成,版本为 v18.x.x及以上
:::

  • 修改接收请求的后端地址:
1
2
baseURL: process.env.NODE_ENV === 'production' ? 'http://120.55.62.195:8102' : 'http://localhost:8102',
withCredentials: true,
  • 执行以下命令,一键启动前端项目:
1
yarn start:dev

🦪 持续优化

随着项目的发展和用户需求的增加,我们将持续优化系统性能,提升用户体验。

加强系统的安全性措施,定期进行安全审计和漏洞扫描,确保用户数据的安全。引入微服务架构将项目向容器化部署发展,确保系统的可扩展性和灵活性。

同时将引入持续集成与部署的流程,实现自动化测试和部署上线,降低运维成本。

前端框架搭建

介绍

搭建

  • 在正确安装Node.js环境下的前提下,依次执行以下命令:
  • 使用npm全局安装 pro-cli 构建工具(脚手架):
1
npm i @ant-design/pro-cli -g
  • 使用脚手架,在指定目录下快速搭建前端框架:
1
pro create memory-api 
  • 项目下安装yarn包管理工具:
1
yarn -V
  • 至此,前端项目框架搭建完成,启动项目:
1
yarn run start

image-20230711232614130

  • 执行效果如下:

image-20230711232655300

  • 登录页面,效果如下:

image-20230711232732396

踩坑经历

  • 如果未安装yarn工具,而直接启动项目,会报如下错误:

后端框架搭建

介绍

建库建表

  • 新建接口信息表,建表信息如下:(2023/07/15晚)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 接口信息表
use memory_api;
create table if not exists interface_info
(
`id` bigint not null auto_increment comment '主键' primary key,
`name` varchar(256) not null comment '用户名',
`description` varchar(256) null comment '描述',
`url` varchar(512) not null comment '接口地址',
`requestHeader` text null comment '请求头',
`responseHeader` text null comment '响应头',
`userId` varchar(256) not null comment '创建人',
`status` int default 0 not null comment '接口状态(0 - 关闭, 1 - 开启))',
`method` varchar(256) 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 '接口信息表';

实体类映射

  • 将interface_info表映射为实体类,
  • 这一步我们借助MybatisX-Generator插件来完成(具体使用方法可移步至《掌握-JetBrains-IntelliJ-IDEA:使用心得与技巧》一文中学习了解)
  • InterfaceInfo接口信息类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/**
* 接口信息表
* @TableName interface_info
*/
@TableName(value ="interface_info")
@Data
public class InterfaceInfo implements Serializable {
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;

/**
* 用户名
*/
@TableField(value = "name")
private String name;

/**
* 描述
*/
@TableField(value = "description")
private String description;

/**
* 接口地址
*/
@TableField(value = "url")
private String url;

/**
* 请求头
*/
@TableField(value = "requestHeader")
private String requestHeader;

/**
* 响应头
*/
@TableField(value = "responseHeader")
private String responseHeader;

/**
* 创建人
*/
@TableField(value = "userId")
private String userId;

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

/**
* 请求类型
*/
@TableField(value = "method")
private String method;

/**
* 创建时间
*/
@TableField(value = "createTime")
private Date createTime;

/**
* 更新时间
*/
@TableField(value = "updateTime")
private Date updateTime;

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

@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

前端请求参数封装

  • 在实体类基础上,封装前端请求参数类,处理前端发送的HTTP请求,前端人员使用接口文档调试更加方便:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 创建请求
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @from <a href="https://yupi.icu">编程导航知识星球</a>
*/
@Data
public class InterfaceInfoAddRequest implements Serializable {
/**
* 用户名
*/
@TableField(value = "name")
private String name;

/**
* 描述
*/
@TableField(value = "description")
private String description;

/**
* 接口地址
*/
@TableField(value = "url")
private String url;

/**
* 请求头
*/
@TableField(value = "requestHeader")
private String requestHeader;

/**
* 响应头
*/
@TableField(value = "responseHeader")
private String responseHeader;

/**
* 创建人
*/
@TableField(value = "userId")
private Long userId;

/**
* 请求类型
*/
@TableField(value = "method")
private String method;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 查询请求
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @from <a href="https://yupi.icu">编程导航知识星球</a>
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class InterfaceInfoQueryRequest extends PageRequest implements Serializable {
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;

/**
* 用户名
*/
@TableField(value = "name")
private String name;

/**
* 描述
*/
@TableField(value = "description")
private String description;

/**
* 接口地址
*/
@TableField(value = "url")
private String url;

/**
* 请求头
*/
@TableField(value = "requestHeader")
private String requestHeader;

/**
* 响应头
*/
@TableField(value = "responseHeader")
private String responseHeader;

/**
* 创建人
*/
@TableField(value = "userId")
private Long userId;

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

/**
* 请求类型
*/
@TableField(value = "method")
private String method;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 更新请求
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @from <a href="https://yupi.icu">编程导航知识星球</a>
*/
@Data
public class InterfaceInfoUpdateRequest implements Serializable {
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;

/**
* 用户名
*/
@TableField(value = "name")
private String name;

/**
* 描述
*/
@TableField(value = "description")
private String description;

/**
* 接口地址
*/
@TableField(value = "url")
private String url;

/**
* 请求头
*/
@TableField(value = "requestHeader")
private String requestHeader;

/**
* 响应头
*/
@TableField(value = "responseHeader")
private String responseHeader;

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

/**
* 请求类型
*/
@TableField(value = "method")
private String method;
}

后端接口实现

  • service层:
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 参数校验
*
* @param interfaceInfo
* @param add
*/
@Override
public void validInterfaceInfo(InterfaceInfo interfaceInfo, boolean add) {
if (interfaceInfo == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
}
  • controller层:(处理增、删、改、查需求)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 创建接口
*
* @param interfaceInfoAddRequest 创建接口参数
* @param request request
* @return 创建接口成功
*/
@PostMapping("/add")
public BaseResponse<Long> addInterfaceInfo(@RequestBody InterfaceInfoAddRequest interfaceInfoAddRequest,
HttpServletRequest request) {
if (interfaceInfoAddRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}

InterfaceInfo interfaceInfo = new InterfaceInfo();
BeanUtils.copyProperties(interfaceInfoAddRequest, interfaceInfo);
interfaceInfoService.validInterfaceInfo(interfaceInfo, true);

User loginUser = userService.getLoginUser(request);
interfaceInfo.setUserId(loginUser.getId());

boolean result = interfaceInfoService.save(interfaceInfo);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);

long newInterfaceInfoId = interfaceInfo.getId();
return ResultUtils.success(newInterfaceInfoId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 删除接口
*
* @param deleteRequest 删除接口参数
* @param request request
* @return 删除接口成功
*/
@PostMapping("/delete")
public BaseResponse<Boolean> deleteInterfaceInfo(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
if (deleteRequest == null || deleteRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}

User user = userService.getLoginUser(request);
long id = deleteRequest.getId();

// 判断是否存在
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
ThrowUtils.throwIf(oldInterfaceInfo == null, ErrorCode.NOT_FOUND_ERROR);

// 仅本人或管理员可删除
if (!oldInterfaceInfo.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}

boolean b = interfaceInfoService.removeById(id);
return ResultUtils.success(b);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 更新接口(仅管理员)
*
* @param interfaceInfoUpdateRequest 更新接口参数
* @return 更新接口成功
*/
@PostMapping("/update")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updateInterfaceInfo(@RequestBody InterfaceInfoUpdateRequest interfaceInfoUpdateRequest) {
if (interfaceInfoUpdateRequest == null || interfaceInfoUpdateRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}

InterfaceInfo interfaceInfo = new InterfaceInfo();
BeanUtils.copyProperties(interfaceInfoUpdateRequest, interfaceInfo);
interfaceInfoService.validInterfaceInfo(interfaceInfo, false);

long id = interfaceInfoUpdateRequest.getId();
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
ThrowUtils.throwIf(oldInterfaceInfo == null, ErrorCode.NOT_FOUND_ERROR);

boolean result = interfaceInfoService.updateById(interfaceInfo);
return ResultUtils.success(result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 根据id获取接口信息
*
* @param id 接口id
* @return 接口信息
*/
@GetMapping("/get")
public BaseResponse<InterfaceInfo> getInterfaceInfoById(long id, HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}

InterfaceInfo interfaceInfo = interfaceInfoService.getById(id);

if (interfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}

return ResultUtils.success(interfaceInfo);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 获取接口信息列表
* 分页查询
*
* @param interfaceInfoQueryRequest 查询参数
* @param request request
* @return 接口信息列表
*/
@PostMapping("/list/page")
public BaseResponse<Page<InterfaceInfo>> listInterfaceInfoByPage(@RequestBody InterfaceInfoQueryRequest interfaceInfoQueryRequest,
HttpServletRequest request) {
if (interfaceInfoQueryRequest == null || interfaceInfoQueryRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}

long current = interfaceInfoQueryRequest.getCurrent();
long size = interfaceInfoQueryRequest.getPageSize();

InterfaceInfo interfaceInfo = new InterfaceInfo();
BeanUtils.copyProperties(interfaceInfoQueryRequest, interfaceInfo);
interfaceInfoService.validInterfaceInfo(interfaceInfo, false);

// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);

Page<InterfaceInfo> interfaceInfoPage = interfaceInfoService.page(new Page<>(current, size));
return ResultUtils.success(interfaceInfoPage);
}
  • 成功运行项目,接口文档调试页面正常:(2023/07/19晚)

image-20230719101803347

前端开发

  • 他奶奶的,早上启动前端项目时,发现报错了:

image-20230720104434926

  • 看起来好像是少了什么依赖,但我也没动这依赖啊,不好搞。
  • 于是我直接全部重新构建了下前端项目,可以跑起来了
  • 2023/10/03早,时隔3个月,终于解决了这个疑惑:
    • 出现这个报错一定是因为在项目构建完成后,移动了项目所在文件夹的路径(2023/10/03早)
  • 拿到 Ant Design Pro前端框架后,要做四件事:
    • 修改登录页面
    • 新增注册页面
    • 生成请求接口
    • 保存用户登录态
    • 修改表单获取后台信息

修改登录页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<LoginForm
contentStyle={{
minWidth: 280,
maxWidth: '75vw',
}}
logo={<img alt="logo" src="/logo.svg" />}
title="Memory API"
subTitle={intl.formatMessage({ id: 'pages.layouts.userLayout.title' })}
initialValues={{
autoLogin: true,
}}
actions={[
<FormattedMessage
key="loginWith"
id="pages.login.loginWith"
defaultMessage="其他登录方式"
/>,
<ActionIcons key="icons" />,
]}
onFinish={async (values) => {
await handleSubmit(values as API.LoginParams);
}}
>

新增注册页面

  • 基本仿照登录页面,在Page/User下新增Register/index.tsx文件,开发注册页面
  • 在config/routes.ts下新增Register页面路由:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default [
{
path: '/user',
layout: false,
routes: [
// 登录页
{
name: 'login',
path: '/user/login',
component: './User/Login',
},
// 注册页
{
name: 'register',
path: '/user/register',
component: './User/Register',
},
],

..............................
},
  • 修改app.tsx文件,修改跳转login页的逻辑:
1
2
3
const isDev = process.env.NODE_ENV === 'development';
const loginPath = '/user/login';
const registerPath = '/user/register';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
// 1.获取当前用户登录态
const fetchUserInfo = async () => {
try {
const msg = await queryCurrentUser({
skipErrorHandler: true,
});
return msg.data;
} catch (error) {
history.push(loginPath);
}
return undefined;
};

// 2.1.非登录/注册页,执行
const { location } = history;
if (location.pathname !== loginPath && location.pathname !== registerPath) {
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
currentUser,
settings: defaultSettings as Partial<LayoutSettings>,
};
}

//2.2.登录/注册页,执行
return {
fetchUserInfo,
settings: defaultSettings as Partial<LayoutSettings>,
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 构建页面时
export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
return {
actionsRender: () => [<Question key="doc" />, <SelectLang key="SelectLang" />],
// 获取头像
avatarProps: {
src: initialState?.currentUser?.avatar,
title: <AvatarName />,
render: (_, avatarChildren) => {
return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
},
},
// 获取水印
waterMarkProps: {
content: initialState?.currentUser?.name,
},
// 获取脚标
footerRender: () => <Footer />,
// 监测登录状态
onPageChange: () => {
const { location } = history;
// 如果没有登录,且非登录/注册页面,重定向到 login
if (
!initialState?.currentUser &&
location.pathname !== loginPath &&
location.pathname !== registerPath
) {
history.push(loginPath);
}
},
  • 成功请求到register页面
  • 下午又更新了下前端对 InterfaceInfo 增、删、改、查的参数校验逻辑
  • 这里实现了两个枚举类,分别用来判断接口信息的状态和接口的请求方法是否正确:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 接口请求方法
*/
public enum InterfaceInfoMethodEnum {
GET("GET", "GET方法"),

POST("POST", "POST方法");

private final String value;
private final String text;

InterfaceInfoMethodEnum(String value, String text) {
this.value = value;
this.text = text;
}

/**
* 判断接口请求方法
*
* @param value 接口请求方法
* @return 存在与否
*/
public static InterfaceInfoMethodEnum getEnumByValue(String value) {
if (value == null) {
return null;
}
InterfaceInfoMethodEnum[] values = InterfaceInfoMethodEnum.values();
for (InterfaceInfoMethodEnum infoStatusEnum : values) {
if (infoStatusEnum.getValue().equals(value)) {
return infoStatusEnum;
}
}
return null;
}

public String getValue() {
return value;
}

public String getText() {
return text;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 接口状态
*/
public enum InterfaceInfoStatusEnum {
USUAL(0, "正常"),

ERROR(1, "错误");

private final int value;
private final String text;

InterfaceInfoStatusEnum(int value, String text) {
this.value = value;
this.text = text;
}

/**
* 判断接口状态
*
* @param value 接口状态
* @return 存在与否
*/
public static InterfaceInfoStatusEnum getEnumByValue(Integer value) {
if (value == null) {
return null;
}
InterfaceInfoStatusEnum[] values = InterfaceInfoStatusEnum.values();
for (InterfaceInfoStatusEnum infoStatusEnum : values) {
if (infoStatusEnum.getValue() == value) {
return infoStatusEnum;
}
}
return null;
}

public int getValue() {
return value;
}

public String getText() {
return text;
}
}

生成请求接口

  • Ant Design Pro为我们提供了快速编写请求接口的方法,更加方便前端调用后端接口:
  • 首先拿到后端生成的接口文档的 json 信息:

image-20230720232106758

  • 访问这个地址:
1
localhost:8101/api/v2/api
  • 可以看到该接口文档的所有 json 信息,如下图所示:

image-20230720232255418

  • 在前端config/config.ts下,修改相关信息,自动对接后端,生成可调用的接口方法:
1
2
3
4
5
6
openAPI: [
{
requestLibPath: "import { request } from '@umijs/max'",
schemaPath: 'http://localhost:8101/api//v2/api-docs',
projectName: 'memory-api',
},
  • 输入以下命令,启动项目:
1
yarn run openapi

image-20230720232623522

  • 在services/memory-api下,生成了与后端对应的接口方法(2023/07/20晚)
  • 利用SQL之父工具,快速生成了模拟数据

保存用户登录态

  • 改造了Ant Design Pro的代码结构,详情可看《掌握-Ant-Design-Pro:目录结构、业务逻辑与代码改造技巧》(2023/07/21午)

获取在线用户信息

  • 他爷爷的,之前提到过的跨域问题,可算解决了,仅仅在 requestErrorConfig 下加一行代码:
1
2
3
4
5
6
7
8
9
export const errorConfig: RequestConfig = {
baseURL: 'http://localhost:8101',
withCredentials: true,
// 错误处理: umi@3 的错误处理方案。
errorConfig: {
..................
}
........................
}

自定义页面

注册页

  • 这个目前还没做好,日后安排(2023/07/23早)

用户信息页

  • 添加页面很简单,新增路由,在指定路径下新增对应页面即可
  • 在 config/routes.ts 下新增 User 路由
1
2
3
4
5
6
7
8
//用户信息页
{
name: '用户信息',
access: 'canAdmin',
icon: 'user',
path: '/user/list',
component: './TableList/User',
},
  • 这里,我把该页面查看权限设置为:管理员可查看
  • 在 pages/TableList 下新增 User 页面
  • 这里展示下请求发送逻辑和信息展示表格:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
const columns: ProColumns<API.User>[] = [
{
title: 'id',
dataIndex: 'id',
copyable: true,
ellipsis: true,
tip: '用户id是唯一的',
},

{
title: '账户',
dataIndex: 'userAccount',
valueType: 'textarea',
},

{
title: '昵称',
dataIndex: 'userName',
valueType: 'textarea',
},

{
title: '头像',
dataIndex: 'userAvater',
valueType: 'textarea',
render: (_, record) => {
const url = record.userAvatar;

return (
<Space>
<img src={url} alt="img" style={{ width: '30px', height: '30px' }} />
</Space>
);
},
},

{
title: '微信开放平台id',
dataIndex: 'unionId',
copyable: true,
ellipsis: true,
},

{
title: '公众号id',
dataIndex: 'mpOpenId',
copyable: true,
ellipsis: true,
},

{
title: '角色',
disable: true,
dataIndex: 'userRole',
valueType: 'select',
filters: true,
onFilter: true,

render: (_, record) => {
let tagColor = 'grey';
let userRole = '';

switch (record?.userRole) {
case 'admin':
tagColor = 'green';
userRole = '管理员';
break;
case 'user':
tagColor = 'blue';
userRole = '普通用户';
break;
default:
tagColor = 'default';
}

return (
<Space>
<Tag color={tagColor} key={record?.userRole}>
{userRole}
</Tag>
</Space>
);
},
},

{
title: '介绍',
dataIndex: 'userProfile',
ellipsis: true,
},

{
title: '注册时间',
dataIndex: 'createTime',
ellipsis: true,
},

{
title: '状态',
dataIndex: 'isDelete',
valueEnum: {
0: {
text: '正常',
status: 'success',
},
user: {
text: '异常',
status: 'error',
},
},
},

{
title: '操作',
valueType: 'option',
key: 'option',
render: () => [
<a
key="update"
onClick={() => {
// action?.startEditable?.(record.id);
}}
>
更新
</a>,

<a
key="delete"
onClick={() => {
// action?.startEditable?.(record.id);
}}
>
删除
</a>,
],
},
];
1
2
3
4
5
6
7
8
9
10
11
12
13
request={async (params: { pageSize?: number; current?: number; keyword?: string }) => {
const res = await listUserByPageUsingPOST({
...params,
});

if (res?.data) {
return {
data: res?.data.records || [],
success: true,
total: res?.data.total,
};
}
}}

image-20230723143312368

接口信息页

  • 在 config/routes.ts 下新增 InterfaceInfo 路由
1
2
3
4
5
6
7
//接口信息页
{
name: '接口信息',
icon: 'user',
path: '/interfaceInfo/list',
component: './TableList/InterfaceInfo',
},
  • 在 pages/TableList 下新增 User 页面
  • 这里展示下请求发送逻辑和信息展示表格:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
const columns: ProColumns<API.InterfaceInfoQueryRequest>[] = [
{
title: 'id',
dataIndex: 'id',
copyable: true,
},
{
title: '接口名称',
dataIndex: 'name',
copyable: true,
ellipsis: true,
},
{
title: '接口描述',
dataIndex: 'description',
ellipsis: true,
valueType: 'select',
},

{
title: '接口状态',
dataIndex: 'status',
filters: true,
onFilter: true,
valueEnum: {
0: {
text: '开放',
status: 'success',
},
1: {
text: '异常',
status: 'error',
disabled: true,
},
2: {
text: '解决中',
status: 'Processing',
},
},
},
{
title: '接口方法',
disable: true,
dataIndex: 'method',
valueType: 'select',
filters: true,
onFilter: true,

render: (_, record) => (
<Space>
<Tag color={'green'} key={record?.method}>
{record?.method}
</Tag>
</Space>
),
},
{
title: '创建人',
dataIndex: 'userId',
copyable: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
valueType: 'date',
hideInSearch: true,
},

{
title: '操作',
valueType: 'option',
key: 'option',
render: () => [
<a
key="editable"
onClick={() => {
// action?.startEditable?.(record.id);
}}
>
更新
</a>,
<a target="_blank" rel="noopener noreferrer" key="view">
删除
</a>,
],
},
];
  • 这里使用了高级表格组件
  • 优化了表格展现效果:可复制、省略、可过滤等等(2023/07/23午)

image-20230723120419634

新增接口

  • 用户信息页的这些完成批量处理的代码可以删除:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
actionRef={actionRef}
rowKey="key"
search={{
labelWidth: 120,
}}
toolBarRender={() => [
<Button
type="primary"
key="primary"
onClick={() => {
handleModalOpen(true);
}}
>
<PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="New" />
</Button>,
]}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{/* 选中记录 批量处理记录*/}
{selectedRowsState?.length > 0 && (
<FooterToolbar
extra={
<div>
<FormattedMessage id="pages.searchTable.chosen" defaultMessage="Chosen" />{' '}
<a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>{' '}
<FormattedMessage id="pages.searchTable.createForm.newRule" defaultMessage="项" />
&nbsp;&nbsp;
<span>
<FormattedMessage
id="pages.searchTable.totalServiceCalls"
defaultMessage="Total number of service calls"
/>{' '}
{selectedRowsState.reduce((pre, item) => pre + item.callNo!, 0)}{' '}
<FormattedMessage id="pages.searchTable.tenThousand" defaultMessage="万" />
</span>
</div>
}
>
{/* 批量删除 */}
<Button
onClick={async () => {
await handleRemove(selectedRowsState);
setSelectedRows([]);
actionRef.current?.reloadAndRest?.();
}}
>
<FormattedMessage
id="pages.searchTable.batchDeletion"
defaultMessage="Batch deletion"
/>
</Button>
{/* 批量审批 */}
<Button type="primary">
<FormattedMessage
id="pages.searchTable.batchApproval"
defaultMessage="Batch approval"
/>
</Button>
</FooterToolbar>
)}
  • 优化了用户信息页表格的展现方式,解决了登录用户头像获取不成功的问题:

image-20230726180123695

  • 做了一下午,仅仅完成了接口信息的新增功能(—_—)(2023/07/26)
  • 本来一切都很顺利,但是在设计新增接口表单时,我犯难了:简单来讲,有两种表单的写法,均可实现新增接口功能
  • 在 TableList/InterfaceInfo 下新增 CreateForm.tsx文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export type Props = {
columns: ProColumns<API.InterfaceInfo>[];
onCancel: () => void;
onSubmit: (values: API.InterfaceInfo) => Promise<void>;
visible: boolean;
};

const CreateModal: React.FC<Props> = (props) => {
const { visible, columns, onCancel, onSubmit } = props;

return (
<Modal visible={visible} footer={null} onCancel={() => onCancel?.()} title="查询接口信息">
<ProTable
// 设置表单的标题
type="form"
columns={columns}
onSubmit={(value) => {
onSubmit?.(value);
}}
onReset={() => {
console.log('表单已重置'); // 表单重置的回调函数
}}
/>
</Modal>
);
};

export default CreateModal;
  • 然后在 index.tsx 下 引入上面导出的表单:
1
2
3
4
5
6
7
8
9
10
11
12
{/* 新增记录表单 */}
<CreateModal
columns={columns}
onCancel={() => {
handleModalVisible(false);
}}
onSubmit={(values) => {
handleModalVisible(false);
addInterfaceInfoUsingPOST(values);
}}
visible={createModalVisible}
/>
  • 这个很方便,自动识别字段,并填充表单,但对于各个字段的校验和单独处理,不是很方便(目前我这么认为)
  • 所以我选择第二种方法,所有表单的校验都手动处理,简单粗暴,更适合我这种偏后端的程序员,设计表单更好理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<ModalForm
title="新增接口表单"
width="600px"
open={createModalOpen}
onOpenChange={handleModalOpen}
onFinish={async (value) => {
const success = await addInterfaceInfoUsingPOST(value as API.InterfaceInfo);
if (success) {
message.success('新增接口成功');
handleModalOpen(false);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
>
<ProFormText
name="name"
label="接口名称"
placeholder="请输入接口名称"
required
rules={[{ required: true, message: '接口名称不能为空' }]}
/>
<ProFormTextArea
name="description"
label="接口描述"
placeholder="请输入接口描述"
required
rules={[{ required: true, message: '接口描述不能为空' }]}
/>
<ProFormText
name="url"
label="接口地址"
placeholder="请输入接口地址"
required
rules={[{ required: true, message: '账户不能为空' }]}
/>
<ProFormText
name="requestHeader"
label="请求头"
placeholder="请输入请求头"
required
rules={[{ required: true, message: '请求头不能为空' }]}
/>
<ProFormText
name="responseHeader"
label="响应头"
placeholder="请输入请求头"
required
rules={[{ required: true, message: '响应头不能为空' }]}
/>
<ProFormText
name="userTd"
label="创建人"
placeholder="请输入创建人"
required
rules={[{ required: true, message: '创建人不能为空' }]}
/>
<ProFormRadio.Group
name="method"
label="请求类型"
placeholder="请选择请求类型"
required
options={['GET', 'POST']}
rules={[{ required: true, message: '请求类型不能为空' }]}
/>
</ModalForm>

image-20230727130821670

  • 对于用户信息的增删改,我觉得不是很现实,哪有管理员直接在后台改人家信息的,我就不做了
  • 另外,鱼聪明真是我的得力干将,有不懂的代码直接问它就完事了
  • 晚上继续快速开发剩下的修改和删除接口功能,再做后端调用接口的开发

更新接口

  • 这个功能很好做,想明白思路即可:获取本行信息,回显在更新接口表单中,更新完成后执行提交
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** getInterfaceInfoById GET /api/interfaceInfo/get */
export async function getInterfaceInfoByIdUsingGET(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
id: number,
options?: { [key: string]: any },
) {
return request<API.BaseResponseInterfaceInfo_>('/api/interfaceInfo/get', {
method: 'GET',
params: {
id,
},
...(options || {}),
});
}
1
2
3
4
5
6
7
8
9
10
11
12
<a
key="editable"
onClick={async () => {
const res = await getInterfaceInfoByIdUsingGET(Number(record.id));
if (res.data) {
setInterfaceInfo(res.data);
}
handleUpdateModalOpen(true);
}}
>
更新
</a>
1
2
3
const [createUpdateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);

const [interfaceInfo, setInterfaceInfo] = useState<API.InterfaceInfo | null>(null);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<ModalForm
name="updateInterfaceInfo"
title="更新接口表单"
width="600px"
open={createUpdateModalOpen}
onOpenChange={handleUpdateModalOpen}
onFinish={async (value) => {
const additionalData = {
// 添加额外的数据
id: interfaceInfo?.id,
};

const formData = {
...value, // 表单数据
...additionalData, // 额外的数据
};
const success = await updateInterfaceInfoUsingPOST(formData as API.InterfaceInfo);
if (success) {
console.log('updateInterfaceInfo表单提交');
message.success('更新接口成功');

handleUpdateModalOpen(false);
if (actionRef.current) {
actionRef.current.reload();
}
}
// window.location.reload(); // 刷新页面
}}
>
<ProFormText
name="name"
label="接口名称"
initialValue={interfaceInfo?.name}
placeholder="请输入接口名称"
required
rules={[{ required: true, message: '接口名称不能为空' }]}
/>
<ProFormTextArea
name="description"
label="接口描述"
initialValue={interfaceInfo?.description}
placeholder="请输入接口描述"
required
rules={[{ required: true, message: '接口描述不能为空' }]}
/>
<ProFormText
name="url"
label="接口地址"
initialValue={interfaceInfo?.url}
placeholder="请输入接口地址"
required
rules={[{ required: true, message: '账户不能为空' }]}
/>
<ProFormText
name="requestHeader"
label="请求头"
initialValue={interfaceInfo?.requestHeader}
placeholder="请输入请求头"
required
rules={[{ required: true, message: '请求头不能为空' }]}
/>
<ProFormText
name="responseHeader"
label="响应头"
initialValue={interfaceInfo?.responseHeader}
placeholder="请输入请求头"
required
rules={[{ required: true, message: '响应头不能为空' }]}
/>
<ProFormText
name="userTd"
label="创建人"
initialValue={interfaceInfo?.userId}
placeholder="请输入创建人"
required
rules={[{ required: true, message: '创建人不能为空' }]}
/>
<ProFormRadio.Group
name="method"
label="请求类型"
placeholder="请选择请求类型"
value={interfaceInfo?.method}
options={['GET', 'POST']}
rules={[{ required: true, message: '请求类型不能为空' }]}
/>
</ModalForm>
  • 短短的几行代码,埋藏了不少坑,我前端也不扎实,太难搞了:
  • 点击更新后携带id查询本行数据,那个id一直显示不是number值,record.id明明是string却用不了任何API,均报错,不知道怎么封装的
  • 拿到本行数据后,又不会搞怎么接收返回值了,发现使用useState可以解决这个问题:
1
const [interfaceInfo, setInterfaceInfo] = useState<API.InterfaceInfo | null>(null);
1
2
3
4
const res = await getInterfaceInfoByIdUsingGET(Number(record.id));
if (res.data) {
setInterfaceInfo(res.data);
}
  • 使用 setInterfaceInfo 设置 Interface的值后,竟然可以直接在表单里用了:
1
2
3
4
5
6
7
8
<ProFormText
name="name"
label="接口名称"
initialValue={interfaceInfo?.name}
placeholder="请输入接口名称"
required
rules={[{ required: true, message: '接口名称不能为空' }]}
/>
  • 前端真几把神奇,啥玩意儿都给封装好了
  • 单选框这里实现默认勾选值,挺莫名其妙的,就这么实现的:
1
2
3
4
5
6
7
8
<ProFormRadio.Group
name="method"
label="请求类型"
placeholder="请选择请求类型"
value={interfaceInfo?.method}
options={['GET', 'POST']}
rules={[{ required: true, message: '请求类型不能为空' }]}
/>

image-20230727133607574

删除接口

  • 这个功能可太好做了:
1
2
3
4
5
6
7
8
9
10
11
/** deleteInterfaceInfo POST /api/interfaceInfo/delete */
export async function deleteInterfaceInfoUsingPOST(id: number, options?: { [key: string]: any }) {
return request<API.BaseResponseBoolean_>('/api/interfaceInfo/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: { id },
...(options || {}),
});
}
1
2
3
4
5
6
7
8
9
10
11
12
<a
onClick={async () => {
const res = await deleteInterfaceInfoUsingPOST(Number(record.id));
if (res.data) {
message.success('删除接口信息成功');
} else {
message.error('删除接口信息失败');
}
}}
>
删除
</a>
  • 发送 POST 请求,data 一定是一个对象,像上面写的那样
  • 没有弹窗告警,现在先不加这玩意儿了,日后优化再加,先往后做功能,增删改查写的人犯恶心(2023/07/27午)

模拟接口

  • Hutool工具类库:入门和安装 (hutool.cn)
  • 新建一个项目MemoryClient,用来实现后台的接口服务,注意SpringBoot与JDK版本兼容问题
  • 引入Hutool工具类库,导入依赖坐标:
1
2
3
4
5
6
7
   <dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
</dependency>
</dependencies>

  • 在 controller/NameController 下编写模拟接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/name")
public class NameController {
@GetMapping("/")
public String getNameByGet(String name) {
return "GET 我的名字是: " + name;
}

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

@PostMapping("/user")
public String getUserNameBuPost(@RequestBody User user) {
return "POST 我的名字是: " + user.getName();
}
}
  • 在 client/MemoryClient 下使用Hutool工具提供的接口,发送请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class MemoryClient {
/**
*
* @param name
* @return
*/
public String getNameByGet(String name) {
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("name", name);
String result = HttpUtil.get("http://localhost:8123/api/name/", paramMap);
System.out.println(result);
return result;
}

/**
* @param name
* @return
*/
public String getNameByPost(String name) {
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("name", name);
String result = HttpUtil.post("http://localhost:8123/api/name/", paramMap);
System.out.println(result);
return result;
}

/**
*
* @param user
* @return
*/
public String getUserByPost(User user) {
String json = JSONUtil.toJsonStr(user);
String result = HttpRequest.post("http://localhost:8123/api/name/user")
.body(json)
.execute().body();
System.out.println(result);
return result;
}
}
  • 编写测试类,测试结果:
1
2
3
4
5
6
7
8
9
10
class MemoryClientTest {
public static void main(String[] args) {
MemoryClient memoryClient = new MemoryClient();
memoryClient.getNameByGet("邓哈哈");
memoryClient.getNameByPost("邓嘻嘻");

User user = new User("邓八嘎");
memoryClient.getUserByPost(user);
}
}

image-20230728234735474

  • 测试完成!使用Hutool工具包发送请求,成功调用到模拟接口(2023/07/28晚)

API 签名认证

1
2
3
4
5
6
7
8
9
class MemoryClientTest {
public static void main(String[] args) {
String accessKey = "memory";
String secretKey = "12345678";

User user = new User("邓尼玛");
memoryClient.getUserByPost(user);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @param user
*/
public void getUserByPost(User user) {
String json = JSONUtil.toJsonStr(user);
String result = HttpRequest.post("http://localhost:8123/api/name/user")
.addHeaders(getHeaderMap(json))
.body(json)
.execute()
.body();

System.out.println(result);
}
  • 设置请求头(accessKey(公钥)、nonce、timestamp、sign(签名)):
1
2
3
4
5
6
7
8
9
10
11
 public Map<String, String> getHeaderMap(String body) {
Map<String, String> hashMap = new HashMap<>();
hashMap.put("accessKey", accessKey);
// 一定不能直接发送
// hashMap.put("secretKey", secretKey);
hashMap.put("nonce", RandomUtil.randomNumbers(4));
hashMap.put("body", body);
hashMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
hashMap.put("sign", getSign(body, secretKey));
return hashMap;
}
  • 签名生成算法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 签名工具
*
* @author 邓哈哈
*/
public class SignUtils {
/**
* 生成签名
*
* @param body
* @param secretKey
* @return
*/
public static String getSign(String body, String secretKey) {
Digester md5 = new Digester(DigestAlgorithm.SHA256);
String content = body + "." + secretKey;
return md5.digestHex(content);
}
}
  • 服务器接受请求,拿到公钥和私钥,按约定的签名生成算法,生成sign,判断与请求中的sign是否一致:(2023/07/31午)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    @PostMapping("/user")
public String getUserNameByPost(@RequestBody User user, HttpServletRequest request) {
String accessKey = request.getHeader("accessKey");
String nonce = request.getHeader("nonce");
String timestamp = request.getHeader("timestamp");
String body = request.getHeader("body");
String sign = request.getHeader("sign");
//
if (!accessKey.equals("memory")) {
throw new RuntimeException("无权限");
}
//
if (Long.parseLong(nonce) > 10000) {
throw new RuntimeException("无权限");
}

return "POST 我的名字是: " + user.getName();
}

吐槽

  • 这里启动MemoryClient服务之后,一定要留心是否抛出异常
  • 测试的时候,这边accessKey没传输,MemoryClient服务下一直抛异常,我没看到,盯着测试类的终端看了半天(2023/07/31晚)

开发一个SDK

1
2
3
<groupId>com.memory</groupId>
<artifactId>memory-client-sdk</artifactId>
<version>0.0.1</version>
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
1
2
3
4
5
6
7
8
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

接口的发布/下线

后台接口开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 发布
*
* @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);
}
long id = idRequest.getId();
// 判断是否存在
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
if (oldInterfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 判断该接口是否可以调用
com.memory.clientsdk.model.User user = new com.memory.clientsdk.model.User();
user.setName("test");
String username = memoryClient.getUserByPost(user);
if (StringUtils.isBlank(username)) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "接口验证失败");
}
// 仅本人或管理员可修改
InterfaceInfo interfaceInfo = new InterfaceInfo();
interfaceInfo.setId(id);
interfaceInfo.setStatus(InterfaceInfoStatusEnum.ONLINE.getValue());

boolean result = interfaceInfoService.updateById(interfaceInfo);
return ResultUtils.success(result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 下线
*
* @param idRequest
* @param request
* @return
*/
@PostMapping("/offline")
@AuthCheck(mustRole = "admin")
public BaseResponse<Boolean> offlineInterfaceInfo(@RequestBody IdRequest idRequest,
HttpServletRequest request) {
if (idRequest == null || idRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
long id = idRequest.getId();
// 判断是否存在
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
if (oldInterfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 仅本人或管理员可修改
InterfaceInfo interfaceInfo = new InterfaceInfo();
interfaceInfo.setId(id);
interfaceInfo.setStatus(InterfaceInfoStatusEnum.OFFLINE.getValue());
boolean result = interfaceInfoService.updateById(interfaceInfo);
return ResultUtils.success(result);
}
1
2
3
// 接口状态 枚举
ONLINE(1, "发布"),
OFFLINE(0, "下线");

前端页面展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
record.status === 0 ? <a
key="config"
onClick={() => {
handleOnline({record: record});
}}
>
发布
</a> : null,
record.status === 1 ? <Button
type="text"
key="config"
danger
onClick={() => {
handleOffline(record);
}}
>
下线
</Button> : null,
  • 至此,接口的发布/下线功能完成:(2023/08/16午)

image-20230816152844795

接口调用

后端接口开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 接口调用
*
* @param interFaceInfoInvokeRequest 接口调用参数
* @param request request
* @return
*/
@PostMapping("/invoke")
public BaseResponse<Object> invokeInterfaceInfo(@RequestBody InterFaceInfoInvokeRequest interFaceInfoInvokeRequest,
HttpServletRequest request) {
// controller校验参数
if (interFaceInfoInvokeRequest == null || interFaceInfoInvokeRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
long id = interFaceInfoInvokeRequest.getId();
String userRequestParams = interFaceInfoInvokeRequest.getRequestParams();
// 判断是否存在
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
if (oldInterfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 判断是否状态是否正常
if (oldInterfaceInfo.getStatus().equals(InterfaceInfoStatusEnum.OFFLINE.getValue())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "接口已关闭");
}
// 用户调用接口
User LoginUser = userService.getLoginUser(request);
String accessKey = LoginUser.getAccessKey();
String secretKey = LoginUser.getSecretKey();

MemoryClient tempClient = new MemoryClient(accessKey, secretKey);
Gson gson = new Gson();
com.memory.clientsdk.model.User user = gson.fromJson(userRequestParams, com.memory.clientsdk.model.User.class);
//TODO 根据不同地址调用对应接口
String usernameByPost = tempClient.getUserByPost(user);
return ResultUtils.success(usernameByPost);
}
  • 这里在 interfaceInfo 表中新增了 requestParams(请求参数)字段
  • 在 user 表中新增了 accessKey、secretKey 字段

前端页面开发

  • 接口详情页实现路由跳转:(2023/08/16午)
1
2
3
4
5
6
7
8
9
10
11
renderItem={(item) => {
const apiLink = `/interfaceInfo/${item.id}`;
return (
<List.Item actions={[<a key={item.id} href={apiLink}>查看</a>]}>
<List.Item.Meta
title={<a href={apiLink}>{item.name}</a>}
description={item.description}
/>
</List.Item>
);
}}
  • 添加路由:
1
2
3
4
5
6
7
//接口调用页
{
name: '接口调用',
icon: 'user',
path: '/interfaceInfo/:id',
component: './TableList/InterfaceInfo',
},
  • 接口调用页:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Card>
{data ? (
<Descriptions title={data.name} column={1}>
<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.requestParams}</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>
) : (
<>接口不存在</>
)}
</Card>
  • 接口调用页面 开发完毕:

image-20230816173416646

请求参数

  • 填写请求参数,发送请求
1
2
3
4
5
6
7
8
9
10
11
12
<Card title="在线测试">
<Form name="invoke" layout="vertical" onFinish={onFinish}>
<Form.Item label="请求参数" name="userRequestParams" >
<Input.TextArea/>
</Form.Item>
<Form.Item wrapperCol={{span: 16}}>
<Button type="primary" htmlType="submit">
调用
</Button>
</Form.Item>
</Form>
</Card>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 调用接口
const onFinish = async (values: any) => {
if (!params.id) {
message.error('接口不存在');
return;
}
setInvokeLoading(true);
try {
const res = await invokeInterfaceInfoUsingPOST({
id: params.id,
...values,
});
setInvokeRes(res.data);
message.success('请求成功');
} catch (error: any) {
message.error('操作失败,' + error.message);
}
setInvokeLoading(false);
};
  • 调用成功!如下图所示:(2023/08/17早)

image-20230817105054929

统计接口调用次数

  • 我们实现了:用户可以在前台页面,调用后台提供的接口服务
  • 接下来,我们要实现用户调用每个接口调用的总次数、剩余的调用次数,以保证接口的规范性

  • 这个功能实现很简单:

    • 建立用户-接口信息表,统计调用总次数、剩余次数
    • 实现某个接口调用后,该用户调用总次数+1,剩余次数-1
    • 详细业务流程,日后再表
  • 建立用户-接口信息表:
1

  • 统计接口调用次数:
1

GateWay网关

1
2
3
4
 // 白名单
private static final List<String> IP_WHITE_HOSTS = Arrays.asList("127.0.0.1:8090");
// 请求路由
private static final String INTERFACE_HOST = "http://localhost:8090";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
 /**
* 过滤请求
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 用户发送请求到 API 网关
// 2. 请求日志
ServerHttpRequest request = exchange.getRequest();
log.info("请求唯一标识:{}", request.getId());
String path = INTERFACE_HOST + request.getPath();
log.info("请求路径:{}", path);
log.info("请求方法:{}", request.getMethod());
log.info("请求参数:{}", request.getQueryParams());
String sourceAddress = request.getLocalAddress().toString();
log.info("请求来源地址:{}", sourceAddress);
log.info("请求目标地址:{}", request.getRemoteAddress());
ServerHttpResponse response = exchange.getResponse();
// 3. 黑白名单
if (!IP_WHITE_HOSTS.contains(sourceAddress)) {
// 设置错误状态码并返回
// response.setStatusCode(HttpStatus.FORBIDDEN);
// return response.setComplete();
}

// 4. 用户鉴权(判断 accessKey, secretKey 是否合法)
HttpHeaders headers = request.getHeaders();
String accessKey = headers.getFirst("accessKey");
String nonce = headers.getFirst("nonce");
String timestamp = headers.getFirst("timestamp");
String body = headers.getFirst("body");
String sign = headers.getFirst("sign");

// 4.1.校验accessKey
// todo 从数据库中查询, accessKey是否分配给该用户
if (accessKey == null || !accessKey.equals("memory")) {
return handleNoAuth(response);
}
// 4.2.校验nonce 不能超过10000
if (nonce == null || Long.parseLong(nonce) > 10000) {
throw new RuntimeException("无权限");
}
// 4.3.校验timestamp 不能超时5分钟
final long FIVE_MINUTES = 60L * 5L;
if (timestamp == null || System.currentTimeMillis() / 1000L - Long.parseLong(timestamp) >= FIVE_MINUTES) {
return handleNoAuth(response);
}
// 4.4.校验body
if (body == null)
return handleNoAuth(response);

// 4.5.校验sign
// todo 从数据库中查询, secretKey是否分配给该用户
String secretKey = "123456";
String serverSign = SignUtils.getSign(body, secretKey);
if (sign == null || !sign.equals(serverSign)) {
return handleNoAuth(response);
}

// 5. 请求的模拟接口是否存在
// todo 从数据库中查询请求调用的接口是否存在 请求方法是否匹配 请求参数是否符合要求等

// 6. 响应日志
Mono<Void> result = chain.filter(exchange);
// Mono<Void> result = handelResponse(exchange, chain);

// 7. 调用成功,次数 + 1
// todo 接口调用次数·+1, invokeCount

// 8. 调用失败,返回一个规范的错误码

return result;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* Response 请求处理
*
* @param exchange exchange
* @param chain chain
* @return 接口响应
*/
public Mono<Void> handelResponse(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();

HttpStatus statusCode = originalResponse.getStatusCode();

if (statusCode == HttpStatus.OK) {
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {

@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
//log.info("body instanceof Flux: {}", (body instanceof Flux));
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
//
return super.writeWith(fluxBody.map(dataBuffer -> {
// 7. 调用成功,接口调用次数 + 1 invokeCount
// try {
// innerUserInterfaceInfoService.invokeCount(interfaceInfoId, userId);
// } catch (Exception e) {
// log.error("invokeCount error", e);
// }
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);//释放掉内存
// 构建日志
StringBuilder sb2 = new StringBuilder(200);
sb2.append("<--- {} {} \n");
List<Object> rspArgs = new ArrayList<>();
rspArgs.add(originalResponse.getStatusCode());
//rspArgs.add(requestUrl);
String data = new String(content, StandardCharsets.UTF_8);//data
sb2.append(data);
log.info(sb2.toString(), rspArgs.toArray());//log.info("<-- {} {}\n", originalResponse.getStatusCode(), data);
return bufferFactory.wrap(content);
}));
} else {
log.error("<--- {} 响应code异常", getStatusCode());
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
return chain.filter(exchange);//降级处理返回数据
} catch (Exception e) {
log.error("gateway log exception.\n" + e);
return chain.filter(exchange);
}
}
1
2
3
4
5
public Mono<Void> handleNoAuth(ServerHttpResponse response) {
// 设置状态码并返回
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
  • 改写 memory-client-sdk 中的请求路径,请求打到 GateWay网关 而非直接请求到 MemoryClient,比如:
1
2
3
4
public String getUserByPost(User user) {
String json = JSONUtil.toJsonStr(user);
return ((HttpRequest)HttpRequest.post("http://localhost:8090/api/name/user").addHeaders(this.getHeaderMap(json))).body(json).execute().body();
}
  • 在前端测试请求,请求成功,结果如下:

image-20230819203736592

整体架构

  • 到目前为止,我们引入了网关,整体架构基本成型,如下所示:(2023/08/19晚)

image-20230819205032864

梳理网关业务逻辑

  • 到此为止,我们梳理下引入网关服务后,用户调用接口时,整体项目的业务流程:(2023/08/23晚)
    • 用户点击调用接口
    • memory-api-backend处理HTTP请求
    • 引入memory-client-sdk工具包,转发请求至GateWay网关
    • GateWay网关统一鉴权、将路由转发至memory-client
    • memory-client处理该请求,调用对应的后台接口,调用成功后返回响应结果

Dubbo远程调用

业务流程分析

  • 这个步骤的作用是什么呢?

  • 前面我们引入了GateWay网关,对调用后台接口的请求作统一鉴权,包括以下几方面:

    • 确认调用者权限、是否分配accessKey、secretKey,是否剩余调用次数
    • 确认被调用接口是否存在、是否发布上线
    • 执行调用,做好后台用户-接口的调用次数统计,比如调用次数totalNum+1、剩余次数leftNum-1
  • 以上业务流程执行完毕以后,GateWay网关才会把用户请求转发给后台接口,由后台接口响应请求(2023/08/23晚)
  • 这些统一鉴权操作都涉及到对数据库的操作,在GateWay网关中,该如何实现呢?

  • 而我们不希望在GateWay网关中实现这些方法,我们可以在网关中,调用其他项目的方法

如何调用其他项目的方法

  1. 复制代码、依赖和环境
  2. HTTP 请求(提供/暴露一个接口,供其他项目使用)
  3. RPC
  4. 把公共的代码打包 JAR 包,其他项目引用(客户端 SDK)
  • 我们这里选用RPC方法调用远程服务(2023/08/23晚)

RPC

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

  1. 对开发者更透明,减少了很多的沟通 成本
  2. RPC 想远程服务器发送请求时,未必要使用 HTTP 请求,比如还可以用 TCP/IP,性能更高(内部服务更适用)

Dubbo 框架(RPC 实现)

  • GRPC、TRPC

示例学习

  • zookeeper 注册中心:通过内嵌的方式运行,更方便
  • 最先启动注册中心,先启动服务提供者,再启动服务消费者

  • 有关 Dubbo 框架 + Nacos / ZooKeeper 实现调用远程服务,可以在官方文档中了解到:

  • Nacos 快速开始
  • Nacos | Apache Dubbo
  • 当然,也可以在该文章下了解学习:《从零开始构建分布式服务架构:用Dubbo和注册中心实现远程调用、服务注册与发现、配置管理》(2023/08/23晚)

抽象公共服务

  • 了解了Dubbo实现远程调用原理,我们着手实现:

  • 抽象公共服务,提供接口,便于GateWay网关调用的同时,降低开发成本(2023/08/23晚)
    • 新建memory-commen项目,提供公共服务
  • 抽取User、InterfaceInfo等实体类到memory-commen中
  • memory-commen提供三个接口:校验用户、校验接口、执行调用
  • 由memory-api-backend实现这三个接口,提供对外服务,并注册服务至nacos注册中心
  • GateWay网关向nacos注册中心调用服务,实现统一鉴权
  • 这里使用Maven实现公共服务的抽取,非常值得学习参考,详情可见《Maven奇技淫巧:优化项目构建与性能调优》一文

统计分析功能

  • 开发一个新页面,统计调用次数最多的接口(调用次数多:收费提高,调用次数少:下线)

后端接口开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@GetMapping("/top/interface/invoke")
@AuthCheck(mustRole = "admin")
public BaseResponse<List<InterfaceInfoVO>> listTopInvokeInterfaceInfo() {
List<UserInterfaceInfo> userInterfaceInfoList = userInterfaceInfoMapper.listTopInvokeInterfaceInfo(3);
Map<Long, List<UserInterfaceInfo>> interfaceInfoIdObjMap = userInterfaceInfoList.stream()
.collect(Collectors.groupingBy(UserInterfaceInfo::getInterfaceInfoId));
QueryWrapper<InterfaceInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.in("id", interfaceInfoIdObjMap.keySet());
List<InterfaceInfo> list = interfaceInfoService.list(queryWrapper);
if (CollectionUtils.isEmpty(list)) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
List<InterfaceInfoVO> interfaceInfoVOList = list.stream().map(interfaceInfo -> {
InterfaceInfoVO interfaceInfoVO = new InterfaceInfoVO();
BeanUtils.copyProperties(interfaceInfo, interfaceInfoVO);
int totalNum = interfaceInfoIdObjMap.get(interfaceInfo.getId()).get(0).getTotalNum();
interfaceInfoVO.setTotalNum(totalNum);
return interfaceInfoVO;
}).collect(Collectors.toList());
return ResultUtils.success(interfaceInfoVOList);
}
1
2
3
4
<select id="listTopInvokeInterfaceInfo" resultType="com.example.memorycommen.model.entity.UserInterfaceInfo">
select interfaceInfoId, sum(totalNum) as totalNum from user_interface_info group by interfaceInfoId
order by totalNum desc limit #{limit};
</select>
  • 大致给出代码,这里涉及到在Mybatis中写原生SQL,还是很值得学习的,详情可见《快速启动:开发自己的定制化-Spring-Boot-项目模板》一文

可视化数据图表

image-20230825191153348

梳理全局业务流程

  • 接下来,我们将梳理用户选择调用接口,到接口成功响应并返回结果的全过程:(2023/08/24早)
    • 先后启动Nacos注册中心、memory-api-backend、GateWay网关服务、memory-client接口服务
    • 用户调用接口,发起请求
    • 处理请求,使用memory-client-sdk工具包,将请求转发至GateWay网关:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 用户调用接口
    User LoginUser = userService.getLoginUser(request);
    String accessKey = LoginUser.getAccessKey();
    String secretKey = LoginUser.getSecretKey();

    MemoryClient tempClient = new MemoryClient(accessKey, secretKey);
    Gson gson = new Gson();
    com.memory.clientsdk.model.User user = gson.fromJson(userRequestParams, com.memory.clientsdk.model.User.class);
    //TODO 根据不同地址调用对应接口
    String usernameByPost = tempClient.getUserByPost(user);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /**
    * @param user
    */
    public String getUserByPost(User user) {
    String json = JSONUtil.toJsonStr(user);

    return HttpRequest.post(GATEWAY_HOST + "/api/name/user")
    .addHeaders(getHeaderMap(json))
    .body(json)
    .execute()
    .body();
    }
    • GateWay网关作统一鉴权:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 4.1.校验accessKey
    // todo 从数据库中查询, accessKey是否分配给该用户
    if (accessKey == null || !accessKey.equals("memory")) {
    return handleNoAuth(response);
    }
    // accessKey是否分配给该用户
    User invokeUser = innerUserService.getInvokeUser(accessKey);
    if (invokeUser == null) {
    return handleNoAuth(response);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 4.5.校验sign
    // todo 从数据库中查询, secretKey是否分配给该用户
    // secretKey是否分配给该用户
    String secretKey = invokeUser.getSecretKey();
    if (secretKey == null || !secretKey.equals("123456")) {
    return handleNoAuth(response);
    }

    String serverSign = SignUtils.getSign(body, secretKey);
    if (sign == null || !sign.equals(serverSign)) {
    return handleNoAuth(response);
    }
    1
    2
    3
    4
    5
    6
    // 5. 请求的模拟接口是否存在
    // todo 从数据库中查询请求调用的接口是否存在 请求方法是否匹配 请求参数是否符合要求等
    InterfaceInfo interfaceInfo = innerInterfaceInfoService.getInterfaceInfo(path, method);
    if (interfaceInfo == null) {
    return handleNoAuth(response);
    }
    • GateWay转发请求,调用后台接口:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:
    # 网关过滤
    cloud:
    gateway:
    routes:
    - id: memory-api
    uri: http://localhost:8123
    predicates:
    - Path=/api/**
    1
    2
    // 6.调用接口
    return handleResponse(exchange, chain, interfaceInfo.getId(), invokeUser.getId());
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    return super.writeWith(fluxBody.map(dataBuffer -> {
    // todo 调用成功, 次数 + 1
    // 7. 调用成功,接口调用次数 + 1 invokeCount
    try {
    innerUserInterfaceInfoService.invokeCount(interfaceInfoId, userId);
    } catch (Exception e) {
    log.error("invokeCount error", e);
    }
    .....................
    }
    • 调用成功,返回响应结果:
    1
    2
    3
    4
    @PostMapping("/user")
    public String getUserNameByPost(@RequestBody User user, HttpServletRequest request) {
    return "POST 我的名字是: " + user.getName();
    }

后续优化

接口添加

随机显示一条名言

  • 在Mappper中写自定义SQL查询语句
1
2
3
<select id="getRandomWord" resultType="memory.cloud.memoryclient.domain.Words">
select * from words where type = #{type} ORDER BY RAND() LIMIT 1;;
</select>
  • 添加的时候还是有些许坎坷的,毕竟好长时间呢没看这个项目了,调用成功后,总算理清了整个接口调用的思路:

image-20230921135241899

  • 总结下简单的流程:(2023/09/21午)
    • 用户携带参数请求接口
    • 根据请求接口Id,校验判断该接口的可用性,借助sdk工具包调用该接口
    • sdk发送的调用接口的请求,首先来到 GateWay 作统一鉴权、统一业务处理、路由转发等操作
    • 网关转发接口请求到对应接口,处理完成后返回结果,完成接口调用
  • 后续将进行页面优化,并提供多个简单的接口实现,目前有想法的接口实现为:(2023/09/21午)
    • 自动生成头像
    • 返回随即壁纸(这个计划使用Java爬虫爬取别的网页数据,正好复习下这方面的知识,巩固MemorySearch的学习经验)
    • 在线翻译功能(暂时没有想法,接入别的在线翻译平台嘛)

获取随机壁纸

  • 基本流程跑通:(2023/11/19晚)
1
2
3
4
5
6
7
8
9
10
11
@PostMapping("/list/page/vo")
public BaseResponse<Page<Picture>> listPictureByPage(@RequestBody PictureQueryRequest pictureQueryRequest,
HttpServletRequest request) throws IOException {
// controller层对参数的校验
int currentPage = pictureQueryRequest.getCurrentPage();
int pageSize = pictureQueryRequest.getPageSize();
String searchText = pictureQueryRequest.getSearchText();

Page<Picture> picturePage = pictureService.listPictureVOByPage(searchText, pageSize, currentPage);
return ResultUtils.success(picturePage);
}
  • 接口改写成这样子了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("/wallpaper")
@Slf4j
public class WallpaperController {
@Resource
private PictureService pictureService;


@PostMapping("/list/page/vo")
public BaseResponse<Page<Picture>> listPictureByPage(@RequestBody Picture picture) throws IOException {
// controller层对参数的校验
String category = picture.getCategory();

Page<Picture> picturePage = pictureService.listPictureVOByPage(category);
return ResultUtils.success(picturePage);
}
}
  • 妈的,出现了这样的报错:
1
2
om.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `memory.cloud.memoryclient.model.domain.Picture` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]
  • 排了一个小时的错,可算发现了:Picture 不支持序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
public class Picture implements Serializable {
public Picture() {
}

/**
* 所属分类
*/
private String category;

/**
* 图片名
*/
private String title;

/**
* 图片路径
*/
private String url;

private static final long serialVersionUID = 1L;
}
  • 平时这种实体类都是使用 MybatisX-Generator 快速生成的,没注意到实体类要支持序列化 (2023/11/20晚)

根据不同url请求不同接口

  • 每条接口的信息有:接口名、接口url、请求参数等等
  • 当前情况是:用户调用接口时,前端传回了调用者的id,且仅实现了对后台一个接口的调用

  • 问题是,当实现调用多个接口时,我们该如何保证用户对不同接口的调用,后台能够正确转发?
    • 每个接口调用都写一个controller
    • 统一controller接收接口调用请求,再分情况调用相应接口
  • 我比较倾向于第二种方法,但如何根据用户调用情况来调用不同的接口呢?我还没想好,目前计划使用switch枚举接口id来确定调用的接口

  • 因为现在开放的接口数量很少嘛,暂时这样也可行(2023/09/06午)
  • 半个月前的想法还是很正确的,当时已经完成这个功能了,只不过没有记录实现思路(被GateWay网关传递中文乱码整麻了)
1
2
//TODO 根据不同地址调用对应接口
String result = interfaceIdSource.invokeInterfaceById(id, userRequestParams, tempClient);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  /**
* 调用接口
*
* @param id 接口id
*/
public String invokeInterfaceById(Long id, String userRequestParams, MemoryClient tempClient) {
// 初始化interfaceInfoIdList
// this.doInit();
List<Long> interfaceInfoIdList = interfaceInfoMapper.getInterfaceInfoIdList();
// 处理其他未匹配到的情况
ThrowUtils.throwIf(!interfaceInfoIdList.contains(id), ErrorCode.NOT_FOUND_ERROR, "该接口不存在或已被禁用");
// 匹配成功,调用接口
this.userRequestParams = userRequestParams;
Gson gson = new Gson();
String result = "";
if (id == 1646372563419L) {
com.memory.clientsdk.model.User user = gson.fromJson(userRequestParams, com.memory.clientsdk.model.User.class);
result = tempClient.getUserByPost(user);
} else if (id == 1646335784547L) {
com.memory.clientsdk.model.Words words = gson.fromJson(userRequestParams, com.memory.clientsdk.model.Words.class);
result = tempClient.getRandomWord(words);
}
return result;
}
  • 这里的实现方法还是挺丑陋的
    • 因为接口调用时,传递了调用接口的id,在多次斟酌后,还是选择使用简单的if-else分支实现对不同接口的调用
  • 在这里当时还记录了一个踩坑经验:mapper 注入为 null,这次经历确实让我记忆犹新(2023/09/21午)

用户登录/登出体验优化

  • AvatarDropdown.tsx下,我修改了当用户未登录时的页面展示状态,并使用了umi插件声明式页面跳转,完成用户登录跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 未登录
const unLoading = (
<span>
<span>
<h6>还未登录</h6>
</span>
<span className={actionClassName}>
<Link to="/user/login">
<Button type="primary" ghost>
去登录
</Button>
</Link>
</span>
</span>
);
1
2
3
4
5
6
7
8
9
// 如果用户未登录
if (!initialState) {
return unLoading;
}

const {loginUser} = initialState;
if (!loginUser || !loginUser.userAvatar) {
return unLoading;
}
  • 改写了原有的退出登录逻辑,更加简洁和符合逻辑:
1
2
3
4
5
6
7
8
const logout = () => {
userLogoutUsingPOST()
.then(() => {
message.success("退出登录成功");
window.location.reload();
}
)
};
  • 当前页面效果:

image-20230922162009730

image-20230922161957348

  • 这里留下了疑问,也是我之前遇到的,即umi的声明式跳转使用不了,官方文档中的教程有问题(待解决,2023/09/22午)
  • 在页面间跳转 | UmiJS

丰富项目首页

  • 简单地优化了首页关于该项目的介绍我的个人简介

image-20230922170138843

区分管理员页面和普通用户页面

  • 在管理员账号下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 管理员
{
path: '/',
access: 'canAdmin',
name: 'admin',
icon: 'smile',
routes: [
//用户信息页
{
name: '用户信息',
access: 'canAdmin',
icon: 'user',
path: '/user/list',
component: './TableList/Admin/User',
},
//接口信息页
{
name: '接口信息',
access: 'canAdmin',
icon: 'user',
path: '/interfaceInfo/list',
component: './TableList/Admin/InterfaceInfo',
},
// 接口分析页
{
name: '接口分析',
icon: 'user',
path: '/admin/interface_analysis',
component: './TableList/Admin/InterfaceAnalysis'
},

],
},
  • 如上,新增了一个二级菜单:管理员页面
  • 值得注意的是,这里管理员页面默认路由为 /,如果想实现访问 / 跳转至/welcome,则可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 //欢迎页
{
path: '/welcome',
name: 'welcome',
icon: 'smile',
component: './Welcome',
},

{
path: '/',
redirect: '/welcome',
},

// 管理员
{
path: '/',
access: 'canAdmin',
name: 'admin',
....................
},
  • 如上,这里的重定向一定要写在管理员页面路由的前面,这样才能正常跳转至/welcome(2023/09/27午)

简单的主页画面优化

  • 这个其实没什么好记录的,但是我最近一直在写 Vue,换成React后有点不适应,就先在此记录了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
return (
<div>
<Card
hoverable
style={{
width: 500, height: 150, margin: 20, marginLeft: 300,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
>
</Card>

<List
itemLayout="horizontal"
dataSource={list}
grid={{gutter: 16, column: 5}}
renderItem={(item) => (
<List.Item>
<Card
hoverable
style={{width: 240}}
cover={<img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png"/>}
>
<Card.Meta title={item.name} description={item.description}/>
</Card>
</List.Item>
)}
/>
</div>
)
  • React的语法真神奇,计划将默认的侧边栏改掉,改成顶部导航栏

  • 成功写好了接口广场首页,如图所示:

image-20230927181914495

  • 在 .umi/plugin-layout/layout.tsx下,可以修改侧边栏的属性(宽度、title、LOGO等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const userConfig = {
"locale": true,
"navTheme": "light",
"colorPrimary": "#1890ff",
"layout": "mix",
"contentWidth": "Fluid",
"fixedHeader": false,
"fixSiderbar": true,
"colorWeak": false,
"title": "Memory API接口开放平台",
"pwa": true,
"logo": "https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg",
"iconfontUrl": "",
"token": {}
};
1
2
3
4
5
6
7
<ProLayout
route={route}
location={location}
title={userConfig.title || 'plugin-layout'}
navTheme="dark"
siderWidth={270}
...........................
  • 接口后台添加:接口封面图片字段(interfaceUrl) (2023/09/27晚)

接口大厅页面优化

  • 前段时间参加了职业生涯规划大赛参赛材料为:一份生涯发展报告(.pdf 文件) + 一份生涯发展展示(PPT)

  • 本来还满心欢喜地盼着打到校赛,拿个实习 offer 啥的,结果昨晚通知群直接通知今晚7点开始院赛,我还没被评上。

奶奶的,一个院里六十来个参赛者,绝大部分都是 2022 级的,竟然还有好多大一的,大三大四的倒没几个

我这 PPT 也是花了心思的,我用文心一言大模型给我做了4份大学生职业生涯规划的PPT,然后取其精华,去其糟粕

凭借着我浅陋且贫瘠的审美he1设计灵感,选择了最合适的主题色,结合我自己的履历,拼凑出了一份完美的生涯发展展示

结果这份参赛材料竟然没有撑过第一轮筛选,连区区几个学弟学妹的履历都拼不过嘛。。。咱学校的比赛环境真的是一言难尽

他们究竟是什么样的可怕怪物,今晚7点见分晓,届时再到此处做评判

  • 附上几张 生涯发展展示 的截图吧,自我感觉很满意的:(2023/11/09晚)

image-20231109221856370

image-20231109221916491

image-20231109221907242

image-20231109221933453

image-20231109221937872

  • 看到那几张个人项目展示的截图没?所以我才在这个栏目下唠闲话hiahiahia

我观摩回来了,一群妖魔鬼怪,什么几把职业规划大赛,吹牛逼大赛

没有任何履历,不讲述自己的专业水平能力,张口闭口就是要做自动化工程师、软件测评师,我的专业水平还很欠缺,还需努力学习

我要做好大学生活的规划,有个家伙做规划,四个阶段,每个阶段10年,都特么规划到2049年了,关键还没细讲,一句话带过

全是写个指导老师就上去的:自我认知,职业评测,,职业规划,未来展望,他们的生涯发展展示跟一个模子里刻出来的一样

好多都是23级的,有着丰富的项目实战经验优秀的专业水平能力,你特么上大学才两个月不到,这PPT抄的太不走心了吧

大学的这种比赛含金量真是让我一言难尽,还是把时间都放在个人提升上吧(2023/11/09晚)

用户头像链接

  • 哈哈哈哈,直接在 itab标签页 上找到我的头像链接:

image-20231118221636206

  • 我只能说相当好用:(2023/11/18晚)

image-20231118221711543

优化 API 文档描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
title: '类型',
dataIndex: 'type',
key: 'type',
render: (type: any) => (
<div>
{
type === 'string' ? (
<Tag color={"green"}>String</Tag>
) : type === 'int' ? (
<Tag color={"blue"}>int</Tag>
) : 'hhh'
}
</div>
),
},
  • 核心功能已经完成了,做好 API 文档、在线接口调试、错误码参照和示例代码的页面优化(2023/11/22晚)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
label: <><FileTextFilled/>API文档</>,
key: '1',
children: [
<CaretRightFilled style={{color: "green"}}/>,
<span style={{fontSize: 16, fontWeight: "bolder"}}>请求参数说明:</span>,
<Table columns={requestParamsColumn} dataSource={requestParamsInfo}/>,

<CaretRightFilled style={{color: "green"}}/>,
<span style={{fontSize: 16, fontWeight: "bolder"}}>响应参数说明:</span>,
<Table columns={responseParamsColumn} dataSource={responseParamsInfo}/>,

<CaretRightFilled style={{color: "green"}}/>,
<span style={{fontSize: 16, fontWeight: "bolder"}}>返回结果示例:</span>,
]
},

image-20231122204634581

代码块

  • 如何实现代码块显示?
  • 这里简单演示一下使用 ReactMonacoEditor 组件实现代码块:
  • 安装 ReactMonacoEditor
1

  • 导入组件:
1

  • 编辑代码和简单的样式设置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const [code, setCode] = useState('{\n' +
' "code": 0,\n' +
' "data": "{\\"code\\":0,' +
'\\"data\\":' +
'{\\"records\\":' +
{\\"category\\":null,\\"title\\":\\"Our Dynamic Sun\\",\\"url\\":\\"https://www-istp.gsfc.nasa.gov/exhibit/dyn5.gif\\"},{\\"category\\":null,\\"title\\":\\"Southern Sun Waterfront\\",\\"url\\":\\"https://wetu.com/Resources/1198/img_9539.jpg\\"},{\\"category\\":null,\\"title\\":\\"Clipart Panda - Free Clipart Images\\",\\"url\\":\\"http://images.clipartpanda.com/sun-clipart-4cbo5reoi.jpeg\\"},{\\"category\\":null,\\"title\\":\\"Sun, Moon, and Stars: Tradition for the Saints - OnePeterFive\\",\\"url\\":\\"https://onepeterfive.com/wp-content/uploads/2021/02/sunmoonstars.jpg\\"},{\\"category\\":null,\\"title\\":\\"53+ The Sun Clipart | ClipartLook\\",\\"url\\":\\"http://img.clipartlook.com/sun-clipart-png-clipart-panda-free-clipart-imagestop-30-png-the-sun-clip-art-illustrations-the-sun-clipart-1024_1024.jpg\\"}],\\"total\\":0,\\"size\\":1,\\"current\\":5,\\"orders\\":[],\\"optimizeCountSql\\":true,\\"searchCount\\":true,\\"countId\\":null,\\"maxLimit\\":null,\\"pages\\":0},\\"message\\":\\"ok\\"}",\n' +
' "message": "ok"\n' +
'}'); // 初始代码内容

const options = {
// 编辑器选项设置
selectOnLineNumbers: true,
roundedSelection: false,
scrollBeyondLastLine: false,
roundedEdges: false,
handleMouseWheel: true,
};
  • 使用 ReactMonacoEditor 组件:
1
2
3
4
5
6
7
<ReactMonacoEditor
language="javascript" // 指定代码语言
theme="vs-dark" // 指定主题风格
options={options}
height="200"
value={code} // 设置初始代码内容
/>
  • 注意:这里不指定 height 的话,我这里显示该组件高度为0,样式有点问题,还得显示指定高度(日后实现 height 自适应高度

  • 效果如下:

image-20231125213957583

开发者文档

  • 拖欠很久的想法了,为我的项目做一个开发者文档:(2023/11/25晚)

参考实现:欢迎 - Qi-API 接口开放平台 (qimuu.icu)

  • 使用 Vuepress 快速建站,并配置第三方主题:了解了整个 demo 文档的架构之后,着手修改,开启自定义配置:

image-20231125173310088

区别各个接口调用页面显示

  • 花费了将近半个月的时间,总算将基于 Vuepress 搭建文档站点的思路摸清楚了 (2023/12/07晚)

  • 详细的搭建和部署流程都记录在《前端框架踩坑与技巧总结:Ant Design、Vite,助您无忧编程》一文中,如有需求可跳转至该文章下了解学习~

  • 随着接口的增多,每个接口对应的 API 文档、请求参数配置、响应码等都各不相同,我们需要根据不同的接口,显示不同的页面

  • 这就需要我们在外部文件编写页面,并在接口调用页面导入外部页面组件了:

  • 如下,编写页面组件 RandomPoem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 接口分析
* @constructor
*/
import {PageContainer} from "@ant-design/pro-components";
import {CaretRightFilled} from "@ant-design/icons";
import {Button, Form, message, Input, Tag, Table} from 'antd';
import {ColumnsType} from "antd/es/table";
import React, {useState} from "react";
import initialState from "@@/plugin-initialState/@@initialState";
import {invokeInterfaceInfoUsingPOST} from "@/services/memory-api/interfaceInfoController";
import {useParams} from "@@/exports";

const RandomPoem: React.FC = () => {
.....................

return (
<PageContainer>
.....................
</PageContainer>
);
};

export default RandomPoem;

image-20231207235417783

  • 在需要引入该组件的页面导入该组件,并引入:
1
import RandomPoem from "@/pages/TableList/InterfaceInfo/RandomPoem";

image-20231207235457463

  • 具体的思路是:根据当前接口(根据当前接口对应的 url 值,直接匹配展示对应的页面组件),简单的代码演示如下:(2023/12/08晚)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const loadData = async () => {
if (!params.id) {
message.error('参数不存在');
return;
}
setLoading(true);

try {
const res = await getInterfaceInfoByIdUsingGET({
id: Number(params.id),
});
setData(res.data);
} catch (error: any) {
message.error('请求失败,' + error.message);
}
setLoading(false);
};
1
2
3
4
<div>
<span>{data ? data.url : "空"}</span>
{data ? '好好好' + data.url : "空"}
</div>
  • 如上,用三元表达式来展示接口的 url

  • 成功实现:

1
2
3
4
5
6
7
8
9
10
<div>
{data && data.url === 'http://localhost:8090/api/words/one/random' ? (
<RandomPoem/>
) : data && data.url === 'http://localhost:8090/api/wallpaper/list/page/vo' ? (
<RandomPicture/>
) : data && data.url === 'http://localhost:8090/api/name/user' ? (
<NameRepeat/>
) : <div>No component to render</div>
}
</div>

image-20231209002507861

  • 基本完成了各个接口的调用页面区分:(2023/12/09晚)

自定义 Starter

  • 之前我们开发过 memory-client-sdk 接口调用 SDK,但是不够完善,今天在保持原有功能不变的情况下,重构该 SDK:(2024/01/08晚)

新建 Spring Boot 项目

  • 新建 Spring Boot 项目 memory-client-spring-boot-starter:
1
2
3
<groupId>com.memory</groupId>
<artifactId>memory-client-spring-boot-starter</artifactId>
<version>0.0.1</version>

依赖配置

  • pom.xml 文件下,新增如下依赖:
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>s
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
  • spring-boot-configuration-processor 在自定义 Spring Boot Starter 时的作用主要是生成配置元数据、提供代码提示和自动补全,以及确保配置属性的正确解析:
    • 生成配置元数据:该依赖会根据在项目中定义的带有 @ConfigurationProperties 注解的类,在 META-INF 文件夹下生成 spring-configuration-metadata.json 文件。这个文件是一种元数据文件,其中包含了关于配置属性的信息,如属性名称、类型、默认值等。这些信息可以用于在IDE中编辑配置文件时提供代码提示和自动补全等功能。
    • 提供代码提示和自动补全:当你在IDE中编辑配置文件时,由于 spring-boot-configuration-processor 生成的元数据,IDE 会提供代码提示和自动补全功能。这使得在编写配置文件时更加方便,降低了因拼写错误或配置项不正确而导致的错误。
    • 确保配置属性的正确解析spring-boot-configuration-processor 在编译时会对带有 @ConfigurationProperties 注解的类进行处理,确保配置属性能够被正确地解析和绑定。这对于自定义的 Starter 来说非常重要,因为正确的解析和绑定配置属性是保证 Starter 功能正常的前提。
  • 注意,新建的 Spring Boot 项目的pom.xml文件下,都会有build标签,记得移除 👇:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- 移除该内容 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>

新增配置文件类

  • 在 properties 目录下,新增配置文件类 MemoryClientProperties:
1
2
3
4
5
6
7
8
@ConfigurationProperties(prefix = "memory.client")
public class MemoryClientProperties {
private String accessKey;
private String secretKey;

// 省略 Getter()、Setter() 方法
..............................
}
  • @ConfigurationProperties注解能够自动获取 application.properties 配置文件中前缀为 spring.girlfriend 节点下 message属性的内容

新增功能接口

  • 在 service 目录下,新增功能接口 MemoryClientService,用来实现对各个接口发起调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public interface MemoryClientService {
/**
* 获取随机名言
*
* @param words 名言类型
* @return 随机名言
*/
String getRandomWord(Words words);

/**
* 获取随机壁纸
*
* @param picture 壁纸类型
* @return 壁纸名言
*/
String getPictureListByType(Picture picture);
}

新增功能接口实现类

  • 在 service/impl 目录下,新增功能接口实现类 MemoryClientServiceImpl,用来实现对各个接口发起调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 获取随机名言
*
* @param words 名言类型
* @return 随机名言
*/
@Override
public String getRandomWord(Words words) {
String json = JSONUtil.toJsonStr(words);
return HttpRequest.post(GATEWAY_HOST + "/api/words/one/random")
.addHeaders(getHeaderMap(json))
.body(json)
.execute()
.body();
}

/**
* 获取随机壁纸
*
* @param picture 壁纸类型
* @return 壁纸名言
*/
@Override
public String getPictureListByType(Picture picture) {
String json = JSONUtil.toJsonStr(picture);
return HttpRequest.post(GATEWAY_HOST + "/api/wallpaper/list/page/vo")
.addHeaders(getHeaderMap(json))
.body(json)
.execute()
.body();
}

// 省略其他接口调用方法
.................................

新增自动配置类

  • 新增自动配置类 MemoryClientAutoConfiguration,实现自动化配置功能:
1
2
3
4
5
6
7
8
9
10
11
@Configuration
@ConditionalOnClass(MemoryClientService.class)
@EnableConfigurationProperties(MemoryClientProperties.class)
class MemoryClientAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public MemoryClientService memoryClientService() {
return new MemoryClientServiceImpl();
}
}
  • 简单介绍下这几个注解的作用:
    • @Configuration: 标注类为一个配置类,让 spring 去扫描它;
    • @ConditionalOnClass:条件注解,只有在 classpath 路径下存在指定 class 文件时,才会实例化 Bean
    • @EnableConfigurationProperties:使指定配置类生效;
    • @Bean: 创建一个实例类注入到 Spring Ioc 容器中;
    • @ConditionalOnMissingBean`:条件注解,意思是,仅当 Ioc 容器不存在指定类型的 Bean 时,才会创建 Bean。

配置自动装配类路径

  • 配置自动装配的类的路径,这样 Spring Boot 会在启动时,自动会去查找指定文件 /META-INF/spring.factories,若有,就会根据配置的类的全路径去自动化配置:

  • 在 Spring Boot 2.x 中,在 resource/META-INF/spring.factories 文件下,添加如下配置来标记自动配置类:

1
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.starter3.NameAutoConfiguration
  • 而在 Spring Boot 3.x 中,在 resource/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件下,添加如下配置:
1
com.example.starter3.NameAutoConfiguration

打包

  • 是将 girl-friend-spring-boot-starter 打成 jar 包,放到本地的 maven 仓库中去在项目根路径下执行 maven 命令:
1
mvn clean install

引用自定义 Starter

  • 在需要引入 memory-client-spring-boot-starter 接口调用功能的 Spring Boot 项目中的 pom.xml文件中,导入依赖:
1
2
3
4
5
<dependency>
<groupId>com.memory</groupId>
<artifactId>memory-client-sdk</artifactId>
<version>0.0.1</version>
</dependency>
  • resouce目录下的application.yaml配置文件下,添加如下配置:
1
2
3
4
memory-api:
client:
access-key: memory
secret-key: 12345678
  • 注入 MemoryClientService,可以对任一接口服务发起调用:
1
2
3
4
5
6
7
8
9
10
11
12
@Resource
private MemoryClientService memoryClientService;

// 随机名言
com.memory.client.model.Words words = gson.fromJson(userRequestParams, com.memory.client.model.Words.class);
result = memoryClientService.getRandomWord(words);

// 随机壁纸
com.memory.client.model.Picture picture = gson.fromJson(userRequestParams, com.memory.client.model.Picture.class);
result = memoryClientService.getPictureListByType(picture);

............................

随机壁纸优化

  • 推荐该网站:

image-20240116181424321

  • 使用 Jsoup 库爬取该网页:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 // 非空条件,转码
if (StringUtils.isNotBlank(category)) {
category = URLEncoder.encode(category, "UTF-8");
}

Random random = new Random();
int randomPage = random.nextInt(5) + 1;
String url = String.format("https://www.vcg.com/creative-image/%s/?page=%d", category, randomPage);

Connection connect = Jsoup.connect(url);
Document doc = connect.get();
Elements galleryItem = Objects.requireNonNull(doc.getElementById("imageContent"))
.select(".gallery_inner")
.select(".galleryItem");
  • 从结果中随机选取五条结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 随机选择五个标签
List<Element> selectedItems = galleryItem.subList(0, 5);
// 打印选中的标签
for (Element item : selectedItems) {
Elements img = item.select("> a > img");
String src = "https:" + img.attr("data-src");
String title = img.attr("title");

Picture picture = new Picture();
picture.setCategory(category);
picture.setTitle(title);
picture.setUrl(src);

pictureList.add(picture);
}

优化方向

  • 看了别人的接口统计计费功能,很复杂呵:(2023/12/27早)

    • 限制接口调用次数,支持购买接口调用次数
    • 设计虚拟货币,比如说积分,开设相应的积分购买活动,用户发起支付即可获取对应积分,积分可用来换取接口调用次数
    • 这就需要在用户管理中,增加积分管理功能
    • 开设了相应的积分购买活动,还需要在管理员功能中,增加积分购买活动的管理功能,可支持活动的发布和下线
    • 用户购买积分,生成一份订单,需要设计对应的订单管理功能
    • 我时间精力有限,暂时不做这些复杂的功能,不过接口调用次数限制是很有必要的,可以简单实现每日获取50次接口调用的功能
    • 暂时就这些想法
  • 接口开放平台优化:

    • 个人中心:个人信息设置、我的积分、签到免费领取、提供开发者 SDK 下载,快速接入 API 接口
    • 接口调用:限制接口调用次数,做好提示
  • 统计分析功能:

    • 调用次数分析:
    • 响应时间分析:
    • 用户行为分析:
    • 接口调用来源分析:
    • 趋势分析:
  • 完善用户信息管理,完善接口信息管理,完善接口服务

亮点

踩坑记录

mapper 注入为 null

  • 半个月前,我新增了一条接口:随机返回一条名言
  • 当时是这样调用接口的:

image-20230921132949167

  • 当请求转发到 GateWay 网关时,接受到的参数就变成乱码了(可惜截不到原图了,这是现在的调用效果):

image-20230921133040857

  • 这就是 GateWay 网关接收中文参数变成乱码的问题了,关于这个我尝试了各种配置去解决,但均无果
  • 把我搜集到的这些:通过配置来解决中文乱码的解决方案(虽然没什么效果),简单的贴出来吧,有机会再研究:
1
2
3
4
5
6
7
8
9
10
11
// 设置请求编码
MediaType requestContentType = requestHeaders.getContentType();
if (requestContentType != null && requestContentType.getCharset() == null) {
requestHeaders.setContentType(new MediaType(requestContentType.getType(), requestContentType.getSubtype(), Charset.forName("UTF-8")));
}

// 设置响应编码
MediaType responseContentType = responseHeaders.getContentType();
if (responseContentType != null && responseContentType.getCharset() == null) {
responseHeaders.setContentType(new MediaType(responseContentType.getType(), responseContentType.getSubtype(), Charset.forName("UTF-8")));
}
1
2
3
spring:
main:
web-application-type: reactive
  • 于是我就想,我为什么非得传中文呢?写个枚举类,把名言类型枚举,改为传递对应的整数不就行了嘛
  • 于是就有了现在的解决方案,详情可见上文中的 后续优化 -> 随机显示一条名言一栏 (2023/09/21)

相关知识

  • Dubbo、Nacos依赖包的导入以及相关服务的正确使用
  • Maven实现抽取公共服务(2023/08/24午)

TODO

  • 注册页面的开发
  • 接口信息增、删、改、查的后端业务逻辑,待完善
  • 前端表格信息的增、删、改,待完善(√)
  • 增、删、改接口信息后,不能立刻刷新,老问题
  • 接口信息的删除,没有弹窗告警
  • 前端操作不当,没有得到清楚的提示
  • 未登录用户可以选择登录,目前还没有添加这个按钮
  • SDK 使用文档、开发文档、页面布局、丰富接口功能(类型)、明确接口的调用(参数)

MemoryAPI 忆汇廊-开发文档
http://example.com/2023/07/11/MemoryAPI 忆汇廊-开发文档/
作者
Memory
发布于
2023年7月11日
更新于
2024年2月27日
许可协议