116.设计地址管理功能
一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
大家好~我是
米洛
!
我正在从0到1打造一个开源的接口测试平台, 也在编写一套与之对应的教程
,希望大家多多支持。
欢迎关注我的公众号米洛的测开日记
,一起交流学习!
回顾
上一节我们讲了如何用Github Action
帮助我们部署自己的项目,这一节我们来改善一下我们现有的请求方式。先来看看当前项目的使用情况:
在请求地址栏,我们可以看到,这边输入的仍然
是一个完整的地址,这样会显得我们很呆,我们来思考下这里的优化点:
很多公司的不同业务模块的地址前缀都是固定的,比如https://api.xxx.com,亦或者是https://xxx.com/user。如果我们每次都用全局变量
去做这个事情,会表现得很繁琐。如果给你一些地址的选项: 大后台系统
、xxx服务
这种更直观的方式,并且能够更显眼地看到对应的url,那样使用起来体验更佳。针对全局变量,我们对于多个环境的处理虽然也支持,但是并不特别友好。
所以我们可以尝试把环境地址这些数据从url中剥离出来,让url = 网关 + 路由
,使得一个http的请求地址更易理解。
编写app/models/address.py
"""
Python请求网关地址表
"""
from sqlalchemy import Column, INT, String, UniqueConstraint
from app.models.basic import PityBase
class PityGateway(PityBase):
__tablename__ = 'pity_gateway'
__table_args__ = (
UniqueConstraint('env', 'name', 'deleted_at'),
)
env = Column(INT, comment='对应环境')
name = Column(String(32), comment="网关名称")
gateway = Column(String(128), comment="网关地址")
__fields__ = (name, env, gateway)
__tag__ = "网关"
__alias__ = dict(name="网关名称", env="环境", gateway="网关地址")
__show__ = 3
def __init__(self, env, name, gateway, user, id=None):
super().__init__(user, id)
self.name = name
self.env = env
self.gateway = gateway
我们的表数据很简单,基本就是以下3个重点:
- 对应环境
- 名称
- 对应的url前缀地址,也可以叫gateway
编写app/crud/config/AddressDao.py
from app.crud import Mapper
from app.models.address import PityGateway
from app.utils.decorator import dao
from app.utils.logger import Log
@dao(PityGateway, Log("PityRedisConfigDao"))
class PityGatewayDao(Mapper):
pass
有了我们编写好的基础Mapper,写一个crud接口就很简单了。
编写app/routers/config/address.py
from fastapi import Depends
from app.crud.config.AddressDao import PityGatewayDao
from app.handler.fatcory import PityResponse
from app.models.address import PityGateway
from app.routers import Permission, get_session
from app.routers.config.environment import router
from app.schema.address import PityAddressForm
from config import Config
@router.get("/gateway/list", summary="查询网关地址")
async def list_gateway(name: str = '', gateway: str = '', env: int = None,
user_info=Depends(Permission(Config.MEMBER))):
data = await PityGatewayDao.list_record(env=env, gateway=f"%{gateway}%", name=f"%{name}%")
return PityResponse.success(PityResponse.model_to_list(data))
@router.post("/gateway/insert", summary="添加网关地址", description="添加网关地址,只有组长可以操作")
async def insert_gateway(form: PityAddressForm, user_info=Depends(Permission(Config.MANAGER))):
model = PityGateway(**form.dict(), user=user_info['id'])
model = await PityGatewayDao.insert_record(model, True)
return PityResponse.success(model)
@router.post("/gateway/update", summary="编辑网关地址", description="编辑网关地址,只有组长可以操作")
async def insert_gateway(form: PityAddressForm, user_info=Depends(Permission(Config.MANAGER))):
model = await PityGatewayDao.update_record_by_id(user_info['id'], form, True, log=True)
return PityResponse.success(model)
@router.get("/gateway/delete", summary="删除网关地址", description="根据id删除网关地址,只有组长可以操作")
async def delete_gateway(id: int, user_info=Depends(Permission(Config.MANAGER)), session=Depends(get_session)):
await PityGatewayDao.delete_record_by_id(session, user_info['id'], id)
return PityResponse.success()
可以看到router里面的内容十分简单,基本上调用一下curd里面的方法即可。这里重点讲一下查询那里:
查询接口的话,我们的name/gateway都需要支持模糊查找,虽然我们知道前后匹配
的效率很低,不会用到索引去查找对应的数据,但考虑到地址数据不会特别多,所以我们采用更友好的查询方式。
这里给gateway和name加上%%,在调用list_record接口时,会自动被查询条件
识别为like,如果为空则不会给出任何查询条件(即查询所有数据)。
编写前端页面
基本上写到这个样子就差不多啦,具体的代码还是老样子:
- service
- model
- pages
我们先放上组件的代码,具体的代码可以去github查看。
import React, {memo, useEffect, useState} from 'react';
import {PageContainer} from "@ant-design/pro-layout";
import {connect} from "umi";
import {Button, Card, Col, Divider, Form, Input, Row, Select, Table} from "antd";
import {CONFIG} from "@/consts/config";
import TooltipTextIcon from "@/components/Icon/TooltipTextIcon";
import {PlusOutlined} from "@ant-design/icons";
import FormForModal from "@/components/PityForm/FormForModal";
import PityPopConfirm from "@/components/Confirm/PityPopConfirm";
const {Option} = Select;
const Address = ({loading, gconfig, dispatch}) => {
const [form] = Form.useForm();
const {envList, envMap, addressList} = gconfig;
const [modal, setModal] = useState(false);
const [item, setItem] = useState({});
const fetchEnvList = () => {
dispatch({
type: 'gconfig/fetchEnvList',
payload: {
page: 1,
size: 1000,
exactly: true
}
})
}
const fetchAddress = () => {
const values = form.getFieldsValue()
console.log(values)
dispatch({
type: 'gconfig/fetchAddress',
payload: values,
})
}
const isLoading = loading.effects['gconfig/fetchAddress'] || loading.effects['gconfig/fetchEnvList'] || loading.effects['gconfig/updateAddress']
|| loading.effects['gconfig/insertAddress'] || loading.effects['gconfig/deleteAddress']
useEffect(() => {
fetchEnvList()
fetchAddress()
}, []);
const columns = [
{
title: '环境',
key: 'env',
dataIndex: 'env',
render: env => envMap[env],
},
{
title: '名称',
key: 'name',
dataIndex: 'name',
},
{
title: <TooltipTextIcon title="地址一般是服务的基础地址,比如https://api.baidu.com, 用例中的地址简写即可" text="地址"/>,
key: 'gateway',
dataIndex: 'gateway',
render: text => <a href={text}>{text}</a>,
ellipsis: true
},
{
title: '操作',
key: 'operation',
render: (_, record) =>
<>
<a onClick={() => {
setItem(record)
setModal(true)
}}>编辑</a>
<Divider type="vertical"/>
<PityPopConfirm text="删除" title="你确定要删除这个地址吗?" onConfirm={async () => {
await onDelete(record)
}}/>
</>
}
]
const fields = [
{
name: 'env',
label: '环境',
required: true,
message: '请选择对应环境',
type: 'select',
component: <Select placeholder="请选择对应环境">
{envList.map(v => <Option key={v.id} value={v.id}>{v.name}</Option>)}
</Select>,
},
{
name: 'name',
label: '地址名称',
required: true,
message: '请输入地址名称',
type: 'input',
placeholder: '请输入地址名称',
},
{
name: 'gateway',
label: '服务名',
required: true,
message: '请输入服务地址',
type: 'input',
placeholder: '请输入服务地址',
}
];
// 删除地址
const onDelete = async record => {
const ans = await dispatch({
type: 'gconfig/deleteAddress',
payload: {
id: record.id,
}
})
if (ans) {
fetchAddress()
}
}
// 新增/修改地址
const onSubmit = async values => {
let ans;
if (item.id) {
ans = await dispatch({
type: 'gconfig/updateAddress',
payload: {
...values,
id: item.id,
}
})
} else {
ans = await dispatch({
type: 'gconfig/insertAddress',
payload: values
})
}
if (ans) {
setModal(false)
fetchAddress()
}
}
return (
<PageContainer breadcrumb={null} title="请求地址管理">
<Card>
<FormForModal visible={modal} fields={fields} title='添加地址' left={6} right={18} record={item}
onFinish={onSubmit} onCancel={() => setModal(false)}/>
<Form form={form} {...CONFIG.LAYOUT} onValuesChange={fetchAddress}>
<Row gutter={12}>
<Col span={3}>
<Form.Item>
<Button type="primary" onClick={() => setModal(true)}><PlusOutlined/>添加地址</Button>
</Form.Item>
</Col>
<Col span={7}>
<Form.Item label="环境" name="env">
<Select allowClear showSearch placeholder="选择对应的环境">
{envList.map(item => <Option value={item.id} key={item.id}>{item.name}</Option>)}
</Select>
</Form.Item>
</Col>
<Col span={7}>
<Form.Item label="名称" name="name">
<Input placeholder="输入对应的地址名称"/>
</Form.Item>
</Col>
<Col span={7}>
<Form.Item label="地址" name="gateway">
<Input placeholder="输入对应的地址"/>
</Form.Item>
</Col>
</Row>
</Form>
<Table columns={columns} loading={isLoading} rowKey={record => record.id} dataSource={addressList}/>
</Card>
</PageContainer>
)
}
export default connect(({gconfig, user, loading}) => ({gconfig, user, loading}))(memo(Address));
当查询表单的数据变化时,我们会自动重新获取查询接口
。那么关于具体的使用细节,我们将在下一章来完善,敬请期待~
最新体验地址: https://pity.fun