Django + Vue3 前后端分离技术实现自动化测试平台从零到有系列 <第三章> 之 基础架构搭建
基础架构搭建
- 1、简介
- 2、开发前准备
- 2.1、本教程需要的基础:
- 2.2、开发环境准备:
- 2.3、数据库准备
- 2.4、前一章节的代码:
- 3、代码
- 3.1、回顾前文
- 3.2、新架构
- 3.2、调整操作
- 3.2、Django识别调整后的APP
- 4、新增APP
- 4.1、自动化测试APP
- 4.2、系统管理APP
- 5、数据
- 5.1、表设计
- 5.2、代码
- 5.2.1、树递归
- 5.2.2、models.py编写
- 5.2.3、生成表
- 6、总结&预告
1、简介
本章第一部份是后端Django项目按照第二章的架构设计将平台架构目录搭建完成;架构直观的表示在代码目录结构的调整;第二部分是自动化测试功能模块的表设计与创建;这是核心,也会是优先开发的部分。
这两部分组合成了整个项目架构基础,有了他们,后续的开发便顺畅了。
后面所有 模块 == APP(django项目使用 python manage.py startapp 创建的APP),后面常描述 ‘APP’,也可能一时不查写成了’模块’,希望理解
2、开发前准备
2.1、本教程需要的基础:
Python代码编写精通
Django 会写Demo
Vue3 + Element 会写Demo
基础欠些也没有关系,本文会细致的讲解用到的代码,按照一步一步来,也能实现;不懂的可以留言与私聊。
2.2、开发环境准备:
一台安装了下面东西电脑
Mysql 8
Python 3.6 +
Nodejs
Django 4 +
Pycharm
Vscode (开发前端用)
2.3、数据库准备
mysql8新增一个数据库apiauto,备用
字符集:utf8mb4
排序规则:utf8mb4_general_ci
2.4、前一章节的代码:
https://download.csdn.net/download/weixin_40331132/91955861
3、代码
3.1、回顾前文
下图为<第二章>内容,见图后端分为用户、自动化测试、系统管理三个大模块;项目架构按照三个模块进行模块化;在<第一章>已经新增了 users APP,还须再增加两个APP。
让我们再回顾<第一章>项目的层级目录:
apiauto/ # 项目根目录(包含 manage.py 和应用目录)
├─ manage.py # Django 管理脚本,项目入口,可运行迁移、启动服务等
│
├─ apiauto/ # 项目配置目录(和项目同名)
│ ├─ __init__.py # Python 包标识(空文件即可)
│ ├─ settings.py # 全局配置文件(数据库、应用、REST、JWT 等)
│ ├─ urls.py # 项目 URL 路由入口,分发到各个 app 的 urls.py
│ ├─ wsgi.py # WSGI 启动文件,传统部署(Gunicorn/Uwsgi 用)
│ └─ asgi.py # ASGI 启动文件,异步部署(Daphne/Uvicorn 用,支持 WebSocket)
│
└─ users/ # 自定义应用(用户模块)├─ __init__.py # Python 包标识├─ admin.py # Django Admin 后台配置(可注册用户模型)├─ apps.py # 应用配置,Django 自动识别 app├─ models.py # 数据模型定义(ORM 对应数据库表)├─ serializers.py # DRF 序列化器(定义注册、用户信息、修改密码等数据结构)├─ views.py # 视图(API 逻辑处理,例如注册、登录、获取用户信息)├─ urls.py # 当前 app 的路由(只负责本 app 的接口)└─ test.py # 单元测试文件(编写测试用例验证功能是否正确)
明显的感觉他层级不明显,需要更容易明显的区分项目与APP的架构层级
3.2、新架构
在新增自动化测试、系统管理这两个APP前,
其中 users 是APP,与项目配置目录同一层级,为了区分与统一管理APP,在同级目录下增加一个apps目录,apps下放所有APP,users会移动过去。改动后目录如下:
apiauto/ # 项目根目录(包含 manage.py 和应用目录)
├─ manage.py # Django 管理脚本,项目入口,可运行迁移、启动服务等
│
├─ apiauto/ # 项目配置目录(和项目同名)
│ ├─ __init__.py # Python 包标识(空文件即可)
│ ├─ settings.py # 全局配置文件(数据库、应用、REST、JWT 等)
│ ├─ urls.py # 项目 URL 路由入口,分发到各个 app 的 urls.py
│ ├─ wsgi.py # WSGI 启动文件,传统部署(Gunicorn/Uwsgi 用)
│ └─ asgi.py # ASGI 启动文件,异步部署(Daphne/Uvicorn 用,支持 WebSocket)
│
└─ apps/ # APPS集中管理目录 ├─ users/ # 用户模块APP│ ├─ __init__.py # Python 包标识(空文件即可)│ ├─ settings.py # 全局配置文件(数据库、应用、REST、JWT 等)│ ├─ urls.py # 项目 URL 路由入口,分发到各个 app 的 urls.py│ ├─ wsgi.py # WSGI 启动文件,传统部署(Gunicorn/Uwsgi 用)│ └─ asgi.py # ASGI 启动文件,异步部署(Daphne/Uvicorn 用,支持 WebSocket)├─ auto/ # 自动化模块APP├─ sysmanages/ # 系统管理模块APP└─ ....
3.2、调整操作
1、在apiauto目录下新增apps目录
apiauto右击----New----Directory
弹出框中输入:apps,
按下 回车,创建apps目录如下图
2、将users APP移动到apps目录,
将users拖过去apps目录,确认即可
3.2、Django识别调整后的APP
这里users APP移动了,已经在apps目录下了,项目运行仍会找原因来位置的users,它不存在原位置了,所以运行会开始报错,说:No module named ‘users’
因为APP移动,django找不到这个APP
解决方案很简单,在apiauto目录下的setting.py里增加一行代码:
import os, sys
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
增加到 “BASE_DIR = Path(file).resolve().parent.parent” 这行代码后面
添加这行后,项目运行成功了。
4、新增APP
4.1、自动化测试APP
现在增加APP需要进入apps目录下增加了
打开Terminal 编辑进,进入apps目录下
输入命令:
python ../manage.py startapp auto
auto是自动化测试模块的APP,接下来新增系统管理APP
4.2、系统管理APP
输入命令
python ../manage.py startapp sysmanage
到这里,整个平台的架构已经初步形成,
users:用户登录与注册、密码修改、三方登录、用户信息等相关的功能;
sysmanage:整个系统的管理,权限、日志、三方接入配置等;
auto: 核心功能,自动化测试的功能,主要有环境、接口、用例、套件、执行计划等的功能;
架构设计目录只是一部分,表设计不可或缺。
users模块的表继承了django的用户相的表,不需要设计
sysmanage,它可能会管到各个模块的表,自己未必有表,暂时搁置
下面便开始设计auto模块的表;
5、数据
5.1、表设计
一个完整的接口自动化测试平台,需要以下这些表
项目表:公司不止一个项目,不同项目之间的数据需要隔离
环境配置表:每个项目不止一个环境,一般有SIT、UAT、性能、线上环境
接口树表:树结构可以让人按功能、模块管理对应接口,以Sawwage作参考
用例树表:树结构可以让人按功能、模块管理对应用例
接口表:接口数据,加上mock自段
用例表:用例数据、检查点、执行状态等
前置后置执行步骤表:关联用例表,是用例执行前或后需要执行的脚本方法(关键字),功能强大;
套件表:用例执行集合
执行计划表:可以是套件的集合、定时、统计数据、
用例执行记录表:测试报告
套件执行记录表:统计报告
计划执行记录表:统计报告
前置后置执行记录表:测试报告
自定义脚本表:存放平台用户编写的脚本,编写完成后会生成关键字在用例中选择到
表的ER图设计如下:
5.2、代码
5.2.1、树递归
通过ER图可以看出,有两个树模型:接口树、用例树。
他们是自已外键自已,等于递归模型;
所以要先做了一个表数据自我递归小工具代码。
问递归是啥?各位如果递归不懂的,可以去力扣或其他的教程中找找答案。
在项目根目录下新增一个目录utils
项下添加个mixins.py文件里面增加代码
class TreeMixin:"""给自关联模型提供树形结构功能"""def to_dict(self):"""转换为 dict 节点"""return {"id": self.id,"name": self.name,"children": [child.to_dict() for child in self.children.filter(is_deleted=False)]}@classmethoddef build_tree(cls, queryset, parent=None):"""构建树形结构 (JSON 风格),自动过滤 is_deleted=True 的节点"""nodes = queryset.filter(parent=parent, is_deleted=False)return [node.to_dict() for node in nodes]@classmethoddef print_tree(cls, queryset, parent=None, indent=0):"""打印缩进树,调试用,自动过滤 is_deleted=True"""nodes = queryset.filter(parent=parent, is_deleted=False)for node in nodes:print(" " * indent + f"- {node.name}")cls.print_tree(queryset, node, indent + 2)
这个工具写完了后,后面大用,很多递归模型可以继承他。然后完成模型数据的自我递归,输出序列数据。
下面便开始对ER图的表进行Model化了。
5.2.2、models.py编写
各位可以回看一下ER图,其中每一个表,都有is_delete、created_at 、updated_at 、created_by 等字段;我们可以将这几个字段抽取出来成一个BaseModel,使用abstract = True标识这个Model不会生成表。
models.py文件做什么的?大家可以了解一下Django的ORM相关的知识。 死记住理论后,照搬下面代码生成数据库表进行理解。
死记理论,只记他的运行逻辑顺序便好,不是记ORM有众多关键字哈,关键字了解一下即可。
代码比较长,请诸位结合ER图喝着茶细品~
from django.db import models
# Create your models here.from django.contrib.auth.models import User
# 引用工具树
from utils.mixins import TreeMixinclass BaseModel(models.Model):"""抽象基类:给所有表提供通用字段"""is_delete = models.BooleanField(default=False, verbose_name="是否删除")created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="创建人")class Meta:abstract = True # 不会生成表,仅供继承class Project(BaseModel):"""项目"""pro_name = models.CharField(max_length=100, null=True, unique=True, verbose_name='项目名称,唯一校验')description = models.TextField(null=True,verbose_name="项目描述")class Meta:managed = Truedb_table = 'auto_project'def __str__(self):return '{id:%d, projectname:%s, description:%s}' \% (self.id, str(self.projectname), str(self.description))class Environment(BaseModel):"""测试环境配置"""name = models.CharField(max_length=50, unique=True, verbose_name="环境名称")pro = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="test_env", verbose_name="项目其下环境")base_url = models.URLField(verbose_name="基础 URL")headers = models.JSONField(blank=True, null=True, verbose_name="公共请求头")db_config = models.JSONField(blank=True, null=True, verbose_name="数据库配置")env_params = models.JSONField(blank=True, null=True, verbose_name="环境变量配置")class Meta:db_table = "auto_environment" # 表名verbose_name = "测试环境"verbose_name_plural = "测试环境"indexes = [models.Index(fields=["name"]), # 添加索引]def __str__(self):return self.nameclass Apinode(BaseModel, TreeMixin):"""接口树,管理接口,此表自我递归,继承TreeMixin的方法"""name = models.CharField(max_length=200, null=False, verbose_name='node名称')parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")pro = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="api_node", verbose_name="项目下节点")class Meta:managed=Truedb_table = 'auto_api_node'def __str__(self):return self.nameclass API(BaseModel):"""接口信息表"""name = models.CharField(max_length=100, verbose_name="接口名称")path = models.CharField(max_length=200, verbose_name="接口路径")method = models.CharField(max_length=10,choices=[("GET", "GET"), ("POST", "POST"), ("PUT", "PUT"), ("DELETE", "DELETE"), ("PATCH", "PATCH")],verbose_name="请求方法")headers = models.JSONField(blank=True, null=True, verbose_name="请求头")params = models.JSONField(blank=True, null=True, verbose_name="Query 参数")body = models.JSONField(blank=True, null=True, verbose_name="请求体")response = models.JSONField(blank=True, null=True, verbose_name='返回结果')node = models.ForeignKey(Apinode, on_delete=models.DO_NOTHING, related_name="test_api", verbose_name="接口树管理接口")ismock = models.IntegerField(default=0, null=True, verbose_name='是否mock, 0为否,1为是')class Meta:db_table = "auto_api"verbose_name = "接口"verbose_name_plural = "接口"indexes = [models.Index(fields=["name"]),models.Index(fields=["method"]),]def __str__(self):return f"{self.name} [{self.method}]"class Casenode(BaseModel, TreeMixin):"""用例树,管理用例,此表自我递归,继承TreeMixin的方法"""name = models.CharField(max_length=200, null=False, verbose_name='node名称')parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")pro = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="case_node", verbose_name="项目下节点")class Meta:managed=Truedb_table = 'auto_case_node'def __str__(self):return self.nameclass TestCase(BaseModel):"""测试用例"""name = models.CharField(max_length=100, verbose_name="用例名称")node = models.ForeignKey(Casenode, on_delete=models.DO_NOTHING, related_name="test_case", verbose_name="接口树管理用例")description = models.TextField(blank=True, null=True, verbose_name="用例描述")path = models.CharField(max_length=200, verbose_name="接口路径")method = models.CharField(max_length=10,choices=[("GET", "GET"), ("POST", "POST"), ("PUT", "PUT"), ("DELETE", "DELETE"), ("PATCH", "PATCH")],verbose_name="请求方法")headers = models.JSONField(blank=True, null=True, verbose_name="请求头")params = models.JSONField(blank=True, null=True, verbose_name="Query 参数")body = models.JSONField(blank=True, null=True, verbose_name="请求体")response = models.JSONField(blank=True, null=True, verbose_name='返回结果')api = models.ForeignKey(API, on_delete=models.DO_NOTHING, related_name="test_cases", verbose_name="关联接口")checkrestype = models.IntegerField(null=True, verbose_name='检查数据类型 0是返回头,1是返回数据,2是接口状态')checkmethod = models.CharField(max_length=50, null=True, verbose_name='返回检查方式')checkdata = models.CharField(max_length=200, null=True, verbose_name='检查期望')execsort = models.IntegerField(null=True, verbose_name='用例执行排序,在新增时通过获取三级节点下的数量自动生成')artificial = models.FloatField(null=True, verbose_name='人工执行用时')status = models.IntegerField(default=0, null=True, verbose_name='执行状态, 1执行中,2执行完成')class Meta:db_table = "auto_test_case"verbose_name = "测试用例"verbose_name_plural = "测试用例"indexes = [models.Index(fields=["name"]),]def __str__(self):return self.nameclass Casexkey(BaseModel):"""前置、后置执行"""case = models.ForeignKey(TestCase, on_delete=models.PROTECT, null=False, related_name="casex_key", verbose_name='用例的id,外键')beaft = models.IntegerField(null=True, verbose_name='前置或后置,0:前置,1:后置')method = models.CharField(max_length=200, null=False, verbose_name='执行关键字,执行时会映射到脚本 ')result = models.CharField(max_length=200, null=True, verbose_name='变量名称')pars = models.IntegerField(null=True, verbose_name='参数数量')params1 = models.TextField(null=True, verbose_name='参数1')params2 = models.TextField(null=True, verbose_name='参数2')params3 = models.TextField(null=True, verbose_name='参数3')params4 = models.TextField(null=True, verbose_name='参数4')params5 = models.TextField(null=True, verbose_name='参数5')params6 = models.TextField(null=True, verbose_name='参数6')type1 = models.CharField(max_length=10, null=True, verbose_name='参数一类型')type2 = models.CharField(max_length=10, null=True, verbose_name='参数二类型')type3 = models.CharField(max_length=10, null=True, verbose_name='参数三类型')type4 = models.CharField(max_length=10, null=True, verbose_name='参数四类型')type5 = models.CharField(max_length=10, null=True, verbose_name='参数五类型')type6 = models.CharField(max_length=10, null=True, verbose_name='参数六类型')class Meta:managed = Truedb_table = 'auto_casexec_key'def __str__(self):return '公共方法名称:%s\t' % (self.method)class TestSuite(BaseModel):"""测试套件:用例集合"""name = models.CharField(max_length=100, verbose_name="套件名称")description = models.TextField(blank=True, null=True, verbose_name="套件描述")test_cases = models.ManyToManyField(TestCase, related_name="suites", verbose_name="包含用例")name = models.CharField(max_length=100, verbose_name="套件名称")pro= models.ForeignKey(Project, null=True, on_delete=models.DO_NOTHING, related_name="test_suite")is_driven = models.IntegerField(default=0, null=True, verbose_name='数据驱动判断')filename = models.CharField(max_length=200, null=True, verbose_name='文件原名称')savename = models.CharField(max_length=200, null=True, verbose_name='文件存储名称')artificial = models.FloatField(null=True, verbose_name='人工执行时间')status = models.IntegerField(default=0, null=True, verbose_name='执行状态,0待执行,1执行中')class Meta:db_table = "auto_test_suite"verbose_name = "测试套件"verbose_name_plural = "测试套件"def __str__(self):return self.nameclass TestPlan(BaseModel):"""执行计划"""name = models.CharField(max_length=100, verbose_name="计划名称")environment = models.ForeignKey(Environment, on_delete=models.SET_NULL, null=True, related_name="test_plan", verbose_name="执行环境")description = models.TextField(blank=True, null=True, verbose_name="计划描述")suite = models.ForeignKey(TestSuite, on_delete=models.CASCADE, related_name="test_plan", verbose_name="关联套件")schedule = models.CharField(max_length=50, blank=True, null=True, verbose_name="定时任务表达式")qymsg = models.IntegerField(null=True, help_text='0,不发送消息, 1,发送消息')webhook = models.CharField(max_length=500, null=True, help_text='企业微信群机器人webhook')class Meta:db_table = "auto_test_plan"verbose_name = "执行计划"verbose_name_plural = "执行计划"def __str__(self):return self.nameclass Script(BaseModel):"""脚本(前置/后置、SQL/Python)"""kw_name = models.CharField(max_length=50, null=True, unique=True, verbose_name='脚本文件名称')script_type = models.CharField(max_length=20,choices=[("PYTHON", "Python Script"), ("SQL", "SQL Script")],verbose_name="脚本类型")methed_name = models.CharField(max_length=200, null=True, unique=True,verbose_name='脚本里面可以多个函数,取其中一个作入口函数名称,唯一')content = models.TextField(null=True, verbose_name='脚本内容 ')pars = models.IntegerField(null=True, verbose_name='参数数量')rules = models.TextField(null=True, verbose_name='规则描述,备注')public = models.IntegerField(default=0, verbose_name="以项目为维度,0:为私有,1:为公开")pro = models.ForeignKey(Project, null=True, on_delete=models.DO_NOTHING, related_name="script",)class Meta:db_table = "auto_script"verbose_name = "脚本"verbose_name_plural = "脚本"def __str__(self):return f"{self.kw_name} ({self.script_type})"class TestPlanResult(BaseModel):"""计划执行记录"""plan = models.ForeignKey(TestPlan, null=True, on_delete=models.DO_NOTHING, related_name="plan_result", verbose_name='执行组ID')tp_name = models.CharField(max_length=255, null=True, verbose_name='执行计划名称')pro = models.ForeignKey(Project, null=True, on_delete=models.DO_NOTHING, related_name="plan_result", verbose_name='项目ID')class Meta:managed=Truedb_table = 'auto_test_plan_result'def __str__(self):return '名称%s' % (str(self.tp_name))class TestSuiteResult(BaseModel):"""测试套件执行报告"""suite = models.ForeignKey(TestSuite, null=True, on_delete=models.DO_NOTHING, related_name="suites_result")excutnum = models.CharField(max_length=200, null=False, verbose_name='执行批次编号,按计划+用例生成数据,用此字段判断报告文件集,有多个相同编号的数据组成一封报告')planName = models.CharField(max_length=200, null=True, verbose_name='计划名称')cases = models.IntegerField(null=True, verbose_name='用例总数')passs = models.IntegerField(null=True, verbose_name='通过数量')fails = models.IntegerField(null=True, verbose_name='失败数量')totaltime = models.CharField(max_length=100, null=True, verbose_name='计划执行总用时')sCount = models.IntegerField(null=True, verbose_name='300ms<=s<700ms的数量')nCount = models.IntegerField(null=True, verbose_name='700ms<=s<1000ms的数量')cCount = models.IntegerField(null=True, verbose_name='1000ms<=s的数量')pro = models.ForeignKey(Project, null=True, on_delete=models.DO_NOTHING, verbose_name='项目id')tpr = models.ForeignKey(TestPlanResult, null=True, on_delete=models.DO_NOTHING, related_name="suites_result", verbose_name='套件的id')artificial = models.FloatField(null=True, verbose_name='计划执行时人工用时')savetime = models.FloatField(null=True, verbose_name='比人工省时')plancname = models.CharField(max_length=50, null=True, verbose_name='计划创建人名称')execuname = models.CharField(max_length=50, null=True, verbose_name='报告创建人名称')status = models.IntegerField(default=0, null=True, verbose_name='执行状态,0:执行完成 ,1:执行中')class Meta:managed=Truedb_table = 'auto_test_suite_result'def __str__(self):return '编号%s' % (str(self.excutnum))class TestResult(BaseModel):"""用例执行结果"""casename = models.CharField(max_length=100, null=True, verbose_name="存放用例名称")excutnum = models.CharField(max_length=200, null=False,verbose_name='执行批次编号,按计划+用例生成数据,用此字段判断报告文件集,有多个相同编号的数据组成一封报告')loopnum = models.CharField(max_length=200, null=True, verbose_name='执行循环的编号')test_plan = models.ForeignKey(TestPlan, null=True, on_delete=models.DO_NOTHING, related_name="case_result", verbose_name="关联执行计划,可为空,用例可单独执行")test_case = models.ForeignKey(TestCase, on_delete=models.DO_NOTHING, related_name="case_result", verbose_name="关联用例")status = models.CharField(max_length=20,choices=[("PASS", "Pass"), ("FAIL", "Fail")],verbose_name="执行结果")response_data = models.JSONField(blank=True, null=True, verbose_name="请求信息与响应数据,这里有数据性能瓶颈,大家遇到再自行解决")duration = models.FloatField(verbose_name="耗时(秒)")driven = models.IntegerField(null=True, default=0, verbose_name='是否数据驱动,0:不驱动, 1:数据驱动')parentid = models.IntegerField(null=True, default=-1, verbose_name='驱动第一条数据id')exectime = models.FloatField(null=True, verbose_name='用例执行用时')savetime = models.FloatField(null=True, verbose_name='比人工省时')class Meta:db_table = "auto_test_result"verbose_name = "测试结果"verbose_name_plural = "测试结果"indexes = [models.Index(fields=["status"]),]def __str__(self):return f"{self.test_case.name} - {self.status}"class BeaftResult(BaseModel):"""前置、后置执行结果"""id = models.AutoField(primary_key=True)casename = models.CharField(max_length=100, null=True, verbose_name="存放用例名称")status = models.IntegerField(null=False, verbose_name='执行结果状态 ,1是成功,0是失败')elapsedt = models.CharField(max_length=100, null=True, verbose_name='请求响应时间')qutoe_case_type = models.IntegerField(null=True, default=0, verbose_name='0前置,1后置')qutoe_case_execorder = models.IntegerField(default=0, null=True, verbose_name='自定义方法前后执行:0 前, 1 后')tr = models.ForeignKey(TestResult, null=True, on_delete=models.DO_NOTHING, related_name="beaft_result", verbose_name='用例测试报告外键')class Meta:managed = Truedb_table = 'auto_beaft_result'
5.2.3、生成表
models.py文件里面class 除了BaseModel外,其他的完成生成表操作后,都会在mysql中生成对应的表。
运行命令(老演员了):
python manage.py makemigrations #根据在 models.py 中定义或修改的模型(Model),初始化用户表,没有新增models,这里运行不会改为任何模型,默认生成在应用目录下的 migrations/ 文件夹里
python manage.py migrate #Django 会读取 migrations 文件,然后执行对应的 SQL 语句。将数据更新到数据库;
生成表的部分各位先自行操作,后续我会补上截图
因为我虚拟机出了一点小问题,安装的mysql8容器崩了,导致无法生成表,不想重装,先去研究如何解决,然后再将解决方案分享出来。
6、总结&预告
本章节将后端代码架构与数据库设计完成,开头最难的部分算是做完了,接下来便是开始编写代码,下一章会进行两个页面代码编写,项目管理与环境管理页面;顺便会将平台前端页面的菜单做出来。
所以下一章正式进行代码编写阶段,期待~