Skip to content

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