需求分析与技术选型

需求分析

面向开发者的API平台,提供API接口供开发者调用,侧重后端。
通过注册登录,开通接口调用权限,浏览和调用接口。
每次调用都会进行统计,用户可以根据统计数据进行分析和优化。
管理员可以发布接口、下线接口、接入接口,并可视化接口的调用情况和数据。

开放平台项目涉及五个子系统的交互

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

关键问题和挑战

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

技术选型

  1. 前端:
    React + Ant Design Pro + Ant Design Procomponents + Umi + Uim Request(Axios)
  2. 后端:
    Spring Boot + Spring Boot Starter(SDK开发) + 网关

前端-初始化

创建项目框架

Ant Design Pro 官网
版本 Ant Design Pro v6 ==> 基于 Umi 4 + React 18/19
安装在系统中
npm i @ant-design/pro-cli -g
创建项目
pro create myapp

换成国内源
yarn config set registry https://registry.npmmirror.com
清除 yarn 缓存(避免损坏的临时文件)
yarn cache clean
安装依赖
yarn install
运行
yarn start

初始化项目包括:
Umi 4
React 19
TypeScript 5+
Biome 替代 ESLint(现代化工具链)

项目廋身:去除国际化
18n-remove
删除locales目录

删除测试工具

  • tests目录

根据后端自动生成代码

启动后端 http://localhost:8101/api/v3/api-docs
复制粘贴到前端,找到config.ts中的openAPI插件。
打开package.json,找到openapi运行(或在终端输入yarn run openapi)

后端-初始化

使用微光后端全能侠项目初始化

全局修改项目名称:

  1. 进入pom.xml
  2. 选中Omni然后按[Ctrl+Shift+R]全局搜索
  3. 替换

修改配置

数据库 MySQL 名称:wgapi_db 端口:3306

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
-- 创建库
create database if not exists wgapi_db;

-- 切换库
use wgapi_db;

-- 用户表
CREATE TABLE IF NOT EXISTS user
(
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键' PRIMARY KEY,
`userAccount` VARCHAR(256) NOT NULL COMMENT '账号',
`userPassword` VARCHAR(512) NOT NULL COMMENT '密码',
`userName` VARCHAR(256) NULL COMMENT '用户昵称',
`userAvatar` VARCHAR(1024) NULL COMMENT '用户头像',
`userProfile` VARCHAR(512) NULL COMMENT '用户简介',
`userRole` VARCHAR(256) NOT NULL DEFAULT 'user' COMMENT '用户角色(user/admin/ban)',
`accessKey` VARCHAR(512) NOT NULL COMMENT 'accessKey',
`secretKey` VARCHAR(512) NOT NULL COMMENT 'secretKey',
`createTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`isDelete` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除(0-未删, 1-已删)'
) COMMENT '用户';

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

-- 用户调用接口关系表
create table if not exists user_interface_info
(
`id` bigint not null auto_increment comment '主键' primary key,
`userId` bigint not null comment '调用用户 id',
`interfaceInfoId` bigint not null comment '接口 id',
`totalNum` int default 0 not null comment '总调用次数',
`leftNum` int default 0 not null comment '剩余调用次数',
`status` int default 0 not null comment '0-正常,1-禁用',
`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 '用户调用接口关系';

Redis 端口:6379
http://localhost:6379/api/doc.html

前后端对接调整

修改前端请求配置

requestErrorConfig.ts重命名为requestConfig.ts
前端的请求地址为后端地址
在app.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
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);
}
};

登录状态失效

requestconfig.ts 中添加withCredentials:true
在AvatarDropdown.tsx按[Ctrl+R]
把所有的currentUser换成loginUser,name换成userName
把默认的注销方法loginOut改成后端的方法userLogoutUsingPOST
在userLogoutUsing POST后编写跳转回登录页面

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

接口管理

找到access.ts,这是Ant Design Pro内置的一套权限管理机制
修改一下,取全局初始化状态(InitialState)的loginUser,根据当前登录用户判断它是否有管理员权限or用户权限。

1
2
3
4
5
6
7
8
9
10
11
/**
* @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',
};
}

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

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
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;

1
2
3
4
5
(params: U & {
pageSize?: number;
current?: number;
keyword?: string;
}, sort: Record<string, SortOrder>, filter: Record<string, (string | number)[] | null>)
1
2
3
4
5
6
7
8
9
10
11
12
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,
}
}
}}

优化前端页面

比如:欢迎页、管理页.没用到的全部删掉。
先删掉路由,找到routes.ts修改路由。

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: '登录', path: '/user/login', component: './User/Login' }],
},
// { path: '/welcome', name: '欢迎', icon: 'smile', component: './Welcome' },
{
path: '/admin',
name: '管理页',
icon: 'crown',
// 权限控制可以去看 and design pro 的官方文档,不用纠结为什么这么写,就是人家设定的规则而已
access: 'canAdmin',
routes: [
{ name: '接口管理', icon: 'table', path: '/admin/interface_info', component: './InterfaceInfo' },
],
},

// { path: '/', redirect: '/welcome' },
{ path: '*', layout: false, component: './404' },
];

把接口管理页的目录名TableList改为InterfaceInfo

齿轮按钮可以设置各种样式,样式调完还能复制配置
复制配置后,粘贴到defaultSettings.ts
然后把app.tsx的initialState?.settings换成defaultSetting就可以生效了

实现新建功能

回到接口管理页,把配置改成修改,删掉订阅警报。

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

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
import { ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
import '@umijs/max';
import React from 'react';

// 这里要定义一些这个组件要接收什么参数、属性
export type Props = {
onCancel: (flag?: boolean, formVals) => void;
onSubmit: (values) => Promise<void>;
updateModalOpen: boolean;
values: Partial<API.RuleListItem>;
};
const CreateModal: React.FC<Props> = (props) => {
return (
<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>
);
};
export default CreateModal;

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
import type { ProColumns } from '@ant-design/pro-components';
import { ProTable } from '@ant-design/pro-components';
import '@umijs/max';
import { Modal } from 'antd';
import React from 'react';

export type Props = {
columns: ProColumns<API.InterfaceInfo>[];
// 当用户点击取消按钮时触发
onCancel: () => void;
// 当用户提交表单时,将用户输入的数据作为参数传递给后台
onSubmit: (values: API.InterfaceInfo) => Promise<void>;
// 模态框是否可见
visible: boolean;
// values不用传递
// values: Partial<API.RuleListItem>;
};

const CreateModal: React.FC<Props> = (props) => {
// 使用解构赋值获取props中的属性
const { visible, columns, onCancel, onSubmit } = props;

return (
// 创建一个Modal组件,通过visible属性控制其显示或隐藏,footer设置为null把表单项的'取消'和'确认'按钮去掉
<Modal visible={visible} footer={null} onCancel={() => onCancel?.()}>
{/* 创建一个ProTable组件,设定它为表单类型,通过columns属性设置表格的列,提交表单时调用onSubmit函数 */}
<ProTable
type="form"
columns={columns}
onSubmit={async (value) => {
onSubmit?.(value);
}}
/>
</Modal>
);
};
export default CreateModal;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{/* 创建一个CreateModal组件,用于在点击新增按钮时弹出 */}
<CreateModal
columns={columns}
// 当取消按钮被点击时,设置更新模态框为false以隐藏模态窗口
onCancel={() => {
handleModalOpen(false);
}}
// 当用户点击提交按钮之后,调用handleAdd函数处理提交的数据,去请求后端添加数据(这里的报错不用管,可能里面组件的属性和外层的不一致)
onSubmit={(values) => {
handleAdd(values);
}}
// 根据更新窗口的值决定模态窗口是否显示
visible={createModalOpen}
/>

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
import { removeRule, updateRule } from '@/services/ant-design-pro/api';
import {
addInterfaceInfoUsingPOST,
listInterfaceInfoByPageUsingGET,
} from '@/services/yuapi-backend/interfaceInfoController';
import { PlusOutlined } from '@ant-design/icons';
import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components';
import {
FooterToolbar,
PageContainer,
ProDescriptions,
ProTable,
} from '@ant-design/pro-components';
import '@umijs/max';
import { Button, Drawer, message } from 'antd';
import React, { useRef, useState } from 'react';

import CreateModal from './components/CreateModal';

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[]>([]);

// 模态框的变量在TableList组件里,所以把增删改节点都放进来
/**
* @en-US Add node
* @zh-CN 添加节点
* @param fields
*/
// 把参数的类型改成InterfaceInfo
const handleAdd = async (fields: API.InterfaceInfo) => {
const hide = message.loading('正在添加');
try {
// 把addRule改成addInterfaceInfoUsingPOST
await addInterfaceInfoUsingPOST({
...fields,
});
hide();
// 如果调用成功会提示'创建成功'
message.success('创建成功');
// 创建成功就关闭这个模态框
handleModalOpen(false);
return true;
} catch (error: any) {
hide();
// 否则提示'创建失败' + 报错信息
message.error('创建失败,' + error.message);
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;
}
};

/**
* @en-US International configuration
* @zh-CN 国际化配置
* */
const columns: ProColumns<API.InterfaceInfo>[] = [
{
title: 'id',
dataIndex: 'id',
valueType: 'index',
},
{
title: '接口名称',
dataIndex: 'name',

valueType: 'text',
},
{
title: '描述',
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',
hideInForm: true,
},
{
title: '更新时间',
dataIndex: 'updateTime',
valueType: 'dateTime',
hideInForm: true,
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
render: (_, record) => [
<a
key="config"
onClick={() => {
handleUpdateModalOpen(true);
setCurrentRow(record);
}}
>
修改
</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={async (
params,
sort: Record<string, SortOrder>,
filter: Record<string, React.ReactText[] | null>,
) => {
const res: any = await listInterfaceInfoByPageUsingGET({
...params,
});
// 如果后端请求给你返回了接口信息
if (res?.data) {
// 返回一个包含数据、成功状态和总数的对象
return {
data: res?.data.records || [],
success: true,
total: res?.data.total || 0,
};
} else {
// 如果数据不存在,返回一个空数组,失败状态和零总数
return {
data: [],
success: false,
total: 0,
};
}
}}
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>
)}
<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>
{/* 创建一个CreateModal组件,用于在点击新增按钮时弹出 */}
<CreateModal
columns={columns}
// 当取消按钮被点击时,设置更新模态框为false以隐藏模态窗口
onCancel={() => {
handleModalOpen(false);
}}
// 当用户点击提交按钮之后,调用handleAdd函数处理提交的数据,去请求后端添加数据(这里的报错不用管,可能里面组件的属性和外层的不一致)
onSubmit={(values) => {
handleAdd(values);
}}
// 根据更新窗口的值决定模态窗口是否显示
visible={createModalOpen}
/>
</PageContainer>
);
};
export default TableList;
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
import type { RequestOptions } from '@@/plugin-request/request';
import type { RequestConfig } from '@umijs/max';

// 与后端约定的响应数据格式
interface ResponseStructure {
success: boolean;
data: any;
errorCode?: number;
errorMessage?: string;
}

/**
* @name 错误处理
* pro 自带的错误处理, 可以在这里做自己的改动
* @doc https://umijs.org/docs/max/request#配置
*/
export const requestConfig: RequestConfig = {
baseURL:'http://localhost:7529',
withCredentials: true,
// 请求拦截器
requestInterceptors: [
(config: RequestOptions) => {
// 拦截请求配置,进行个性化处理。
const url = config?.url?.concat('?token = 123');
return { ...config, url };
},
],

// 响应拦截器
responseInterceptors: [
(response) => {
// 拦截响应数据,进行个性化处理
const { data } = response as unknown as ResponseStructure;
// 打印响应数据用于调试
console.log('data', data);
// 当响应的状态码不为0,抛出错误
if (data.code !== 0) {
throw new Error(data.message);
}
// 如果一切正常,返回原始的响应数据
return response;
},
],
};

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
import {
addInterfaceInfoUsingPOST,
listInterfaceInfoByPageUsingGET,
updateInterfaceInfoUsingPOST,
deleteInterfaceInfoUsingPOST
} from '@/services/yuapi-backend/interfaceInfoController';
import { PlusOutlined } from '@ant-design/icons';
import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components';
import {
FooterToolbar,
PageContainer,
ProDescriptions,
ProTable,
} from '@ant-design/pro-components';
import '@umijs/max';
import { Button, Drawer, message } from 'antd';
import React, { useRef, useState } from 'react';

import CreateModal from './components/CreateModal';
import UpdateModal from './components/UpdateModal';

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.InterfaceInfo>();
const [selectedRowsState, setSelectedRows] = useState<API.InterfaceInfo[]>([]);

// 模态框的变量在TableList组件里,所以把增删改节点都放进来
/**
* @en-US Add node
* @zh-CN 添加节点
* @param fields
*/
// 把参数的类型改成InterfaceInfo
const handleAdd = async (fields: API.InterfaceInfo) => {
const hide = message.loading('正在添加');
try {
// 把addRule改成addInterfaceInfoUsingPOST
await addInterfaceInfoUsingPOST({
...fields,
});
hide();
// 如果调用成功会提示'创建成功'
message.success('创建成功');
// 创建成功就关闭这个模态框
handleModalOpen(false);
return true;
} catch (error: any) {
hide();
// 否则提示'创建失败' + 报错信息
message.error('创建失败,' + error.message);
return false;
}
};

/**
* @en-US Update node
* @zh-CN 更新节点
*
* @param fields
*/
const handleUpdate = async (fields: API.InterfaceInfo) => {
// 如果没有选中行,则直接返回
if(!currentRow){
return;
}
const hide = message.loading('修改中');
try {
// 调用更新接口,传入当前行的id和更新的字段
await updateInterfaceInfoUsingPOST({
id:currentRow.id,
...fields,
});
hide();
message.success('操作成功');
return true;
} catch (error: any) {
hide();
message.error('操作失败,' + error.message);
return false;
}
};

/**
* Delete node
* @zh-CN 删除节点
*
* @param selectedRows
*/
// 把参数的类型改成InterfaceInfo
const handleRemove = async (record: API.InterfaceInfo) => {
// 设置加载中的提示为'正在删除'
const hide = message.loading('正在删除');
if (!record) return true;
try {
// 把removeRule改成deleteInterfaceInfoUsingPOST
await deleteInterfaceInfoUsingPOST({
// 拿到id就能删除数据
id: record.id
});
hide();
// 如果调用成功会提示'删除成功'
message.success('删除成功');
// 删除成功自动刷新表单
actionRef.current?.reload();
return true;
} catch (error: any) {
hide();
// 否则提示'删除失败' + 报错信息
message.error('删除失败,' + error.message);
return false;
}
};

/**
* @en-US International configuration
* @zh-CN 国际化配置
* */
const columns: ProColumns<API.InterfaceInfo>[] = [
{
title: 'id',
dataIndex: 'id',
valueType: 'index',
},
{
title: '接口名称',
dataIndex: 'name',
valueType: 'text',
formItemProps: {
rules: [{
// 必填项
required: true,
// 不设置提示信息,就默认提示'请输入' + title
// message:'阿巴巴',
}]
}
},
{
title: '描述',
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',
hideInForm: true,
},
{
title: '更新时间',
dataIndex: 'updateTime',
valueType: 'dateTime',
hideInForm: true,
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
render: (_, record) => [
<a
key="config"
onClick={() => {
handleUpdateModalOpen(true);
setCurrentRow(record);
}}
>
修改
</a>,
<a
key="config"
onClick={() => {
handleRemove(record);
}}
>
删除
</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={async (
params,
sort: Record<string, SortOrder>,
filter: Record<string, React.ReactText[] | null>,
) => {
const res: any = await listInterfaceInfoByPageUsingGET({
...params,
});
// 如果后端请求给你返回了接口信息
if (res?.data) {
// 返回一个包含数据、成功状态和总数的对象
return {
data: res?.data.records || [],
success: true,
total: res?.data.total || 0,
};
} else {
// 如果数据不存在,返回一个空数组,失败状态和零总数
return {
data: [],
success: false,
total: 0,
};
}
}}
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>
)}
{/* 之前是UpdateForm,现在改成UpdateModal */}
<UpdateModal
// 要传递 columns,不然修改模态框没有表单项
columns={columns}
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);
}
}}
// 要传递的信息改成visible
visible={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>
{/* 创建一个CreateModal组件,用于在点击新增按钮时弹出 */}
<CreateModal
columns={columns}
// 当取消按钮被点击时,设置更新模态框为false以隐藏模态窗口
onCancel={() => {
handleModalOpen(false);
}}
// 当用户点击提交按钮之后,调用handleAdd函数处理提交的数据,去请求后端添加数据(这里的报错不用管,可能里面组件的属性和外层的不一致)
onSubmit={(values) => {
handleAdd(values);
}}
// 根据更新窗口的值决定模态窗口是否显示
visible={createModalOpen}
/>
</PageContainer>
);
};
export default TableList;

模拟接口项目

项目名称:wgapi-interface
依赖:SpringWeb、Lombok、Spring Boot DevTools
提供三个不同种类的模拟接口:

  1. GET接口
  2. POST接口(url传参)
  3. POST接口(Restful)

新建model层:创建一个User类,在User类写一个用户名属性

1
2
3
4
@Data
public class User {
private String username;
}

新建controller层:新建NameController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 名称 API
*/
@RestController
@RequestMapping("name")
public class NameController {
@GetMapping("/")
public String getNameByGet(String name) {
return "GET 你的名字是" + name;
}

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

@PostMapping("/by")
public String getUserNameByPost(@RequestBody User user) {
return "POST 用户名字是" + user.getUsername();
}
}

点击plication.properties,按[Shift+F6]重构成yml格式更精简一些

1
2
3
4
server:
port: 8123
servlet:
context-path: /api

在application.yml指定后端项目的端口号为8123,指定全局接口地址,加一个api前缀。
调用这个接口http:/localhost:8123/api/name/?name=wg

开发调用接口

调用接口的方式
HTTP调用方式:

  1. HttpClient
  2. RestTemplate
  3. 第三方库(OKHTTP、Hutool)

https://hutool.cn/docs/#/

创建一个client层:客户端层,负责与用户交互、处理用户请求,以及调用服务端提供的API接口等任务的部分。
在client层新建一个客户端wgApiclient.java:负责调用第三方接口。

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
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.yupi.yuapiinterface.model.User;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.HashMap;

/**
* 调用第三方接口的客户端
*/
public class wgApiClient {
// 使用GET方法从服务器获取名称信息
public String getNameByGet(String name) {
// 可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
HashMap<String, Object> paramMap = new HashMap<>();
// 将"name"参数添加到映射中
paramMap.put("name", name);
// 使用HttpUtil工具发起GET请求,并获取服务器返回的结果
String result= HttpUtil.get("http://localhost:8123/api/name/", paramMap);
// 打印服务器返回的结果
System.out.println(result);
// 返回服务器返回的结果
return result;
}

// 使用POST方法从服务器获取名称信息
public String getNameByPost(@RequestParam String name) {
// 可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("name", name);
// 使用HttpUtil工具发起POST请求,并获取服务器返回的结果
String result= HttpUtil.post("http://localhost:8123/api/name/", paramMap);
System.out.println(result);
return result;
}

// 使用POST方法向服务器发送User对象,并获取服务器返回的结果
public String getUserNameByPost(@RequestBody User user) {
// 将User对象转换为JSON字符串
String json = JSONUtil.toJsonStr(user);
// 使用HttpRequest工具发起POST请求,并获取服务器的响应
HttpResponse httpResponse = HttpRequest.post("http://localhost:8123/api/name/")
.body(json) // 将JSON字符串设置为请求体
.execute(); // 执行请求
// 打印服务器返回的状态码
System.out.println(httpResponse.getStatus());
// 获取服务器返回的结果
String result = httpResponse.body();
// 打印服务器返回的结果
System.out.println(result);
// 返回服务器返回的结果
return result;
}
}

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
wgApiClient wApiClient = new wgApiClient();
String result1 = wApiClient.getNameByGet("微光");
String result2 = wApiClient.getNameByPost("微光");
User user = new User();
user.setUsername("微光zc");
String result3 = wApiClient.getUserNameByPost(user);
System.out.println(result1);
System.out.println(result2);
System.out.println(result3);
}
}

API签名认证

需要accessKeysecretKey这和用户名和密码类似(区别:ak、sk是无状态的)
数据库user添加(已添加)

1
2
'accessKey' varchar(512) not null comment 'accessKey',
'secretKey' varchar(512) not null comment 'secretKey',

wgApiclient.java中添加

1
2
private String accessKey;
private String secretKey;

按[AIt+Insert]创建构造方法

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PostMapping("/user")
public String getUserNameByPost(@RequestBody User user, HttpServletRequest request) {
// 从请求头中获取名为 "accessKey" 的值
String accessKey = request.getHeader("accessKey");
// 从请求头中获取名为 "secretKey" 的值
String secretKey = request.getHeader("secretKey");
// 如果 accessKey 不等于 "yupi" 或者 secretKey 不等于 "abcdefgh"
if (!accessKey.equals("yupi") || !secretKey.equals("abcdefgh")){
// 抛出一个运行时异常,表示权限不足
throw new RuntimeException("无权限");
}
// 如果权限校验通过,返回 "POST 用户名字是" + 用户名
return "POST 用户名字是" + user.getUsername();
}

wgapiinterface.client

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
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import fun.wgfun.wgapiinterface.model.User;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.HashMap;
import java.util.Map;
/**
* 调用第三方接口的客户端
*/
public class wgApiClient {
private String accessKey;
private String secretKey;
public wgApiClient(String accessKey,String secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
}

public String getNameByGet(String name) {
// 可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
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;
}

public String getNameByPost(@RequestParam String name) {
// 可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
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;
}

// 创建一个私有方法,用于构造请求头
private Map<String, String> getHeaderMap() {
// 创建一个新的 HashMap 对象
Map<String, String> hashMap = new HashMap<>();
// 将 "accessKey" 和其对应的值放入 map 中
hashMap.put("accessKey", accessKey);
// 将 "secretKey" 和其对应的值放入 map 中
hashMap.put("secretKey", secretKey);
// 返回构造的请求头 map
return hashMap;
}

public String getUserNameByPost(@RequestBody User user) {
String json = JSONUtil.toJsonStr(user);
HttpResponse httpResponse = HttpRequest.post("http://localhost:8123/api/name/user")
// 添加前面构造的请求头
.addHeaders(getHeaderMap())
.body(json)
.execute();
System.out.println(httpResponse.getStatus());
String result = httpResponse.body();
System.out.println(result);
return result;
}
}

开发SDK

Starter开发流程

创建新项目:wgapi-client-sdk;
依赖:Lombok、Spring Configuration Processor(帮助开发者自动生成配置的代码提示)
工具包:Hutool
删掉:maven构建项目的方式,我们现在是要构建依赖包,而不是直接运行jar包的项目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

// 通过 @Configuration 注解,将该类标记为一个配置类,告诉 Spring 这是一个用于配置的类
@Configuration
// 能够读取application.yml的配置,读取到配置之后,把这个读到的配置设置到我们这里的属性中,
// 这里给所有的配置加上前缀为"wgapi.client"
@ConfigurationProperties("wgapi.client")
@Data
@ComponentScan // 用于自动扫描组件,使得 Spring 能够自动注册相应的 Bean
public class wgApiClientConfig {
private String accessKey;
private String secretKey;
@Bean
public wgApiClient yuApiClient() {
return new wgApiClient(accessKey, secretKey);
}
}

现在我们要给用户提供ApiClient,把yuapi-interface项目中的client包、modelf包、utils包复制
修改client包下的wgApiClient.java

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
/**
* 调用第三方接口的客户端
*/
public class wgApiClient {

private static final String GATEWAY_HOST = "http://localhost:8090";

private String accessKey;
private String secretKey;
public wgApiClient(String accessKey, String secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
}

public String getNameByGet(String name) {
//可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("name", name);
String result = HttpUtil.get(GATEWAY_HOST + "/api/name/", paramMap);
System.out.println(result);
return result;
}

public String getNameByPost(String name) {
//可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("name", name);
String result = HttpUtil.post(GATEWAY_HOST + "/api/name/", paramMap);
System.out.println(result);
return result;
}

private 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", genSign(body, secretKey));
return hashMap;
}

public String getUsernameByPost(User user) {
String json = JSONUtil.toJsonStr(user);
HttpResponse httpResponse = HttpRequest.post(GATEWAY_HOST + "/api/name/user")
.addHeaders(getHeaderMap(json))
.body(json)
.execute();
System.out.println(httpResponse.getStatus());
String result = httpResponse.body();
System.out.println(result);
return result;
}
}

在resources目录下创建一个目录META-INF
spring.factories

1
2
# spring boot starter
org.springframework.boot.autoconfigure.EnableAutoConfiguration=fun.wgfun.wgapiclientsdk.wgApiClientConfig

上述配置项指定了要自动配置的类fun.wgfun.wgapiclientsdk.wgApiClientConfig,它是我们刚刚编写的配置类。
通过在spring.factories文件中配置我们的配置类,Spring Boot将会在应用启动时自动加载和实例化wgApiClientConfig,并将其应用于我们的应用程序中。
这样,我们就可以使用自动配置生成的wgApiClient对象,而无需手动创建和配置。

点击Lifecycle一install,把它安装为本地的依赖。

1
2
3
4
5
<dependency>
<groupId>fun.wgfun</groupId>
<artifactId>wgapi-client-sdk</artifactId>
<version>0.0.1</version>
</dependency>

application.yml进行配置

1
2
3
4
wgapi:
client:
access-key: wg
secret-key: abcdefghijklmnopqrstuvwxyz

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 表示这是一个基于Spring Boot的测试类
@SpringBootTest
class YuapiInterfaceApplicationTests {
// 注入一个名为yuApiClient的Bean
@Resource
private wgApiClient wgApiClient;
// 表示这是一个测试方法
@Test
void contextLoads() {
// 调用yuApiClient的getNameByGet方法,并传入参数"yupi",将返回的结果赋值给result变量
String result = wgApiClient.getNameByGet("yupi");
// 创建一个User对象
User user = new User();
// 设置User对象的username属性为"liyupi"
user.setUsername("liyupi");
// 调用yuApiClient的getUserNameByPost方法,并传入user对象作为参数,将返回的结果赋值给usernameByPost变量
String usernameByPost = wgApiClient.getUserNameByPost(user);
// 打印result变量的值
System.out.println(result);
// 打印usernameByPost变量的值
System.out.println(usernameByPost);
}
}

开发接口发布和下线功能

找到InterfacelnfoController.java,复制更新接口改成发布和下线。

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);
}
// 1.校验该接口是否存在
long id = idRequest.getId();
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
if (oldInterfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 2.判断该接口是否可以调用
com.yupi.yuapiclientsdk.model.User user = new com.yupi.yuapiclientsdk.model.User();
user.setUsername("test");
String username = yuApiClient.getUserNameByPost(user);
if (StringUtils.isBlank(username)) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "接口验证失败");
}
InterfaceInfo interfaceInfo = new InterfaceInfo();
interfaceInfo.setId(id);
// 3.修改接口数据库中的状态字段为上线
interfaceInfo.setStatus(InterfaceInfoStatusEnum.ONLINE.getValue());
// 调用interfaceInfoService的updateById方法,传入interfaceInfo对象,并将返回的结果赋值给result变量
boolean result = interfaceInfoService.updateById(interfaceInfo);
// 返回一个成功的响应,响应体中携带result值
return ResultUtils.success(result);
}
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

wgapi-common