源码托管于 GitHub,将在项目 ddl 结束后基于 MIT License 协议开源,访问链接:
陈明志:
- 构建Cards和Passengers相关API(基础要求5,6,7)。
- 关于API的扩展要求(站点状况,商务车厢,多参数查询)。
- 处理Price.xlsx,准备测试数据。
- 基于Flask的对后端的封装,RESTful API、连接池、ORM映射的实现。
- 通过Sqlalchemy实现触发器以及Postgres用户权限的配置。
- 项目报告撰写。
邱天润:
- 构建Lines和Stations相关API(基础要求1,2,3,4),以及自定的扩展要求(分页输出支持)。
- 使用Vue等工具构建一个现代化的前端界面进行数据展示和测试,满足好用、优雅的扩展要求。
- MySQL数据库测试。
- 基于Flask的对后端的封装,RESTful API、连接池、ORM映射的实现,以及前后端的包管理。
- 基于Tornado的高并发可用数据库及其压力测试。
- 项目报告撰写。
贡献百分比相同,均为 50%。
├─ backend
│ ├─ __init__.py
│ ├─ config.py
│ ├─ controllers.py
│ ├─ models.py
│ └─ urls.py
├── frontend
│ ├── index.html
│ ├── jsconfig.json
│ ├── package.json
│ ├── postcss.config.js
│ ├── src
│ │ ├── App.vue
│ │ ├── assets
│ │ │ ├── base.css
│ │ │ ├── layout.css
│ │ │ └── main.css
│ │ ├── components
│ │ │ ├── Cards.vue
│ │ │ ├── Lines.vue
│ │ │ ├── Stations.vue
│ │ │ └── golden
│ │ │ ├── GlTemplate.vue
│ │ │ ├── GoldenLayout.vue
│ │ │ ├── SlotExtr.vue
│ │ │ └── index.js
│ │ ├── main.js
│ │ ├── stores
│ │ └── util.js
│ ├── tailwind.config.js
│ ├── vite.config.js
│ └── yarn.lock
├── data_process
│ └─ Process.py
├── tor.py
└── requirements.txt主要文件介绍:
- app.py: 包含应用程序的主要运行逻辑
- config.py: 包含应用程序的配置信息,如数据库连接字符串、密钥等
- controllers.py: 包含处理请求和响应的控制器函数
- models.py: 定义应用程序的数据模型,与数据库表格对应
- urls.py: 定义应用程序的URL路由访问规则
- tor.py: 包含Tornado服务器的相关配置项
- dataProcess.py: 处理或转换数据的脚本,将
票价.xlsx转换为csv文件后再转换为可直接用的数据 - frontend/*:基于Vue,使用现代技术和前后端分离的思想,实现的一个优雅、易用的前端界面,用于进行数据展示和测试
我们均使用以下电脑配置:
- MacBook Pro 14-inch 2021, Apple M1 Pro, 16GB RAM, 512GB SSD
- macOS Monterey, Python 3.9, PostgreSQL 16
- 按照
requirement.txt中的版本安装FlaskFlask_Migrateflask_sqlalchemySQLAlchemy等PyPi包。 cd backend切换到backend目录下后运行flask run即可开启后端服务器。
cd frontend切换到frontend目录下后,使用yarn或npm install命令安装所需NPM包。- 输入
yarn dev或npm run dev,运行Vue服务,进行测试。
基础部分(默认本地路径:http://127.0.0.1:5000)
- 请求路径:
/lines - 请求方法:
GET,POST - 描述:获取所有线路的列表或创建新的线路。对于POST方法,需要在Body字段中按照字典的格式添加每一个字段所对应的信息。(
line_namebusiness_carriagestart_timeend_timeintromileagecolorfirst_openingurl) - 返回值:对于GET方法,返回一个JSON数组,每个元素为一个线路的JSON对象。对于POST方法,返回新创建的线路的JSON对象。
- 请求路径:
/lines/<line_id> - 请求方法:
GET,PUT,DELETE - 描述:获取、更新或删除指定ID的线路。对于PUT方法,需要在Body字段中按照字典的格式添加每一个字段更新后所对应的信息。(
line_namebusiness_carriagestart_timeend_timeintromileagecolorfirst_openingurl) - 返回值:对于GET方法,返回指定线路的JSON对象。对于PUT方法,返回更新后的线路的JSON对象。对于DELETE方法,返回删除成功的信息(附带
line_id)。
- 请求路径:
/stations - 请求方法:
GET,POST - 描述:获取所有车站的列表或创建新的车站。
- 对于POST方法,需要在Body字段中按照字典的格式添加每一个字段所对应的信息, Status字段包括三个状态
openingclosedunder。(English_nameChinese_nameDistrictStatusIntroduction) - 对于GET方法,我们进行分页,在GET参数中添加
page和elem_per_page字段,表示当前的页数和每页长度;返回值为这样的形式:{ "page": "1", "total": "2000", "result": { RESPONSE } }
- 对于POST方法,需要在Body字段中按照字典的格式添加每一个字段所对应的信息, Status字段包括三个状态
- 返回值:对于GET方法,返回一个JSON数组,每个元素为一个车站的JSON对象。对于POST方法,返回新创建的车站的JSON对象。
- 请求路径:
/stations/<station_id> - 请求方法:
GET,PUT,DELETE - 描述:获取、更新或删除指定ID的车站。对于PUT方法,需要在Body字段中按照字典的格式添加每一个字段更新后所对应的信息。, Status字段包括三个状态
openingclosedunder。(English_nameChinese_nameDistrictStatusIntroduction) - 返回值:对于GET方法,返回指定车站的JSON对象。对于PUT方法,返回更新后的车站的JSON对象。对于DELETE方法,返回删除成功的信息(附带
station_id)。
- 请求路径:
/lines/<line_id>/stations - 请求方法:
GET - 描述:获取指定线路上的所有车站。
- 返回值:返回一个JSON数组,每个元素为一个车站的JSON对象。
- 请求路径:
/lines/<line_id>/stations/<station_id> - 请求方法:
GET,POST,DELETE - 描述:
- 获取、添加或删除线路上的指定车站。在POST方法中,需要在Body字段中按照字典的格式添加每一个字段所对应的信息(数字型
line_num)。 - 另外,对于POST方法,我们可以指定station_id为一个数组,从而可以一次放入多个车站,使用格式如
stations/[1,2,3],表示在在line_num位置先后添加station_id为1,2,3的三个车站。若只导入单个车站,使用stations/1格式即可。
- 获取、添加或删除线路上的指定车站。在POST方法中,需要在Body字段中按照字典的格式添加每一个字段所对应的信息(数字型
- 返回值:对于GET方法,获取成功则返回指定线路上的指定车站的JSON对象, 如果该车站不存在则返回"Station not found on line!"。对于POST方法,添加成功则返回添加成功的信息,若该车站已在该地铁线上,则返回"Station already exists in the Line, abort!"。对于DELETE方法,删除成功则返回删除成功的信息(附带
station_id和line_id), 否则返回失败的信息。
- 请求路径:
/lines/<line_id>/stations/<station_id>/n/<n> - 请求方法:
GET - 描述:获取线路上指定车站的前后n个车站。
- 返回值:返回一个JSON数组,每个元素为一个车站的JSON对象。若n超出范围, 则返回错误信息。
注意:在以上的路径中,<line_id>、<station_id>和<n>需要替换为实际的线路ID、车站ID和车站数量。
- 请求路径:
/card_rides - 请求方法:
GET,POST - 描述:获取所有卡行程的列表或创建新的卡行程。对于POST方法(上车),需要在Body字段中按照字典的格式添加每一个字段所对应的信息。(
card_idfrom_stationstart_timebusiness_carriage) - 返回值:对于GET方法,返回一个JSON数组,每个元素为一个卡行程的JSON对象。对于POST方法,返回新创建的卡行程的JSON对象。
- 请求路径:
/card_rides/<ride_id> - 请求方法:
GET,PUT,DELETE - 描述:获取、更新或删除指定ID的卡行程。对于PUT方法(下车),需要在Body字段中按照字典的格式添加每一个字段更新后所对应的信息。(
to_stationend_time) - 返回值:对于GET方法,返回指定卡行程的JSON对象。对于PUT方法,返回更新后的卡行程的JSON对象。对于DELETE方法,返回删除成功的信息(附带
ride_id)。
- 请求路径:
/user_rides - 请求方法:
GET,POST - 描述:获取所有用户行程的列表或创建新的用户行程。对于POST方法(上车),需要在Body字段中按照字典的格式添加每一个字段所对应的信息。(
user_idfrom_stationstart_timebusiness_carriage) - 返回值:对于GET方法,返回一个JSON数组,每个元素为一个用户行程的JSON对象。对于POST方法,返回新创建的用户行程的JSON对象。
- 请求路径:
/user_rides/<ride_id> - 请求方法:
GET,PUT,DELETE - 描述:获取、更新或删除指定ID的用户行程。对于PUT方法(下车),需要在Body字段中按照字典的格式添加每一个字段更新后所对应的信息。(
to_stationend_time) - 注意:在以上的路径中,
<ride_id>需要替换为实际的行程ID。 - 返回值:对于GET方法,返回指定用户行程的JSON对象。对于PUT方法,返回更新后的用户行程的JSON对象。对于DELETE方法,返回删除成功的信息(附带
ride_id)。
- 请求路径:
/online - 请求方法:
GET - 描述:获取当前地铁线上还未下车的人数以及其具体信息。
- 返回值:返回一个JSON数组,每个元素为一个用户行程的JSON对象或者一个卡行程的JSON对象。
在生成Postgres数据库DDL的同时,我们学习MySQL的DDL语法,并同步制作了一个MySQL的数据库副本。
由于我们使用了SQLAlchemy库来通过ORM方式访问数据库,我们使用了pymysql的连接器来替代用于Postgres的psycopg2连接器,便捷地实现了MySQL数据库的支持。
具体使用时,仅需要在/backend/.env文件中,修改DEVELOPMENT_DATABASE_URL为mysql+pymysql://<username>:<password>@<host>:<port>/<database>,即可实现MySQL切换,实现一套代码、两个数据库系统皆可用。
我们另多实现了几种API设计,完成了更多的系统功能需求。
另外,由于部分数据量较大,为了提升访问效率,我们采用了分页输出的形式,较好地优化了性能,具体详见以下描述。
-
地铁站状态:增加并合理使用地铁站状态,例如:建设中、运营中、关闭中等。
- 在
/stations中增加status字段,表示地铁站状态。 - 在
/user_rides和/card_rides中检验上车站点的状态,如果为closed或under(代表建设中),则不允许上车。
- 在
-
商务车厢信息:增加商务车厢的信息,如乘坐商务车厢,价格翻倍。
- 在
/lines中增加business_carriage字段,表示是否有商务车厢。 - 在
/stations中增加business_carriage字段,表示是否有商务车厢。 - 在
/card_rides中增加business_carriage字段,表示是否乘坐商务车厢, 如若乘坐商务车厢,价格翻倍。 - 在
/user_rides中增加business_carriage字段,表示是否乘坐商务车厢, 如若乘坐商务车厢,价格翻倍。
- 在
-
多参数搜索乘车记录功能:通过地铁站、乘车人、时间段等实现$1$~$n$ 参数搜索乘车记录功能。具体使用方法如下:
-
/queryuser:查询用户乘车记录。你可以在 POST 请求的表单数据中包含以下参数:business_carriage:商务车厢from_station:起始站to_station:终点站user_id:用户 IDon_the_ride:是否在乘车price:价格time:时间(格式为 "YYYY-MM-DDTHH:MM:SS")
-
/querycard:查询卡片乘车记录。你可以在 POST 请求的表单数据中包含以下参数:business_carriage:商务车厢from_station:起始站to_station:终点站card_id:卡片 IDon_the_ride:是否在乘车price:价格time:时间(格式为 "YYYY-MM-DDTHH:MM:SS")
-
-
获取指定用户的所有行程:获取指定用户的所有行程。
- 请求路径:
/user_rides/user/<user_id> - 请求方法:
GET
- 请求路径:
-
获取所有用户或创建用户:获取所有用户的列表或创建新的用户。
- 请求路径:
/users - 请求方法:
GET,POST - 描述:对于POST方法,需要在Body字段中按照字典的格式添加每一个字段所对应的信息。(
user_id_numbernamephonegenderdistrict) - 对于GET方法,我们进行分页,在GET参数中添加
page和elem_per_page字段,表示当前的页数和每页长度;返回值为这样的形式:{ "page": "1", "total": "2000", "result": { RESPONSE } }
- 请求路径:
-
获取所有卡或创建卡:获取所有卡的列表或创建新的卡。
- 请求路径:
/cards - 请求方法:
GET,POST - 描述:对于POST方法,需要在Body字段中按照字典的格式添加每一个字段所对应的信息(
card_numbermoneycreate_time) - 对于GET方法,我们进行分页,在GET参数中添加
page和elem_per_page字段,表示当前的页数和每页长度;返回值形式与前文所述一致。
- 请求路径:
-
获取指定卡的所有行程:获取指定卡的所有行程。
- 请求路径:
/card_rides/card/<card_id> - 请求方法:
GET
- 请求路径:
-
其他功能:分页功能的实现。
-
我们发现,所提供的数据量是很大的,尤其对于
Users、Cards及其对应记录,数据量达到万级。 -
因此,为了防止通信成本过高、前端解析耗时过长,我们为
Stations、Users、Cards三个在前端中直接列出全体列表的大数据表的相关API做了分页处理,以下以对Users库的相关解析逻辑为例,阐述我们通过分页操作实现大数据管理的实践。
def list_all_users_controller(): elem_per_page = int(request.args.get("elem_per_page", 10)) page = int(request.args.get("page", 1)) offset = (page - 1) * elem_per_page users = Users.query.all() response = [] for user in users[offset : offset + elem_per_page]: response.append(user.toDict()) return jsonify({ "page": page, "total": len(users), "result": response, })
-
-
ORM映射:
- 使用SQLAlchemy实现ORM映射,将数据库表格映射为Python类,实现对数据库的操作;这也便利了Postgres和MySQL的自如切换。
- 在
/backend/models.py中定义了Line、Station、User、Card、CardRide、UserRide等类,分别对应数据库中的各个表格。 - 在
/backend/controllers.py中实现了对数据库的增删改查操作。
-
连接池:
- 使用SQLAlchemy实现连接池,提高数据库的访问效率。
- 在
/backend/config.py中配置了数据库连接字符串,实现了连接池。
-
Flask后端框架封装:
- 使用Flask框架实现后端服务器,实现了对请求的响应。
- 在
/backend/app.py中实现了Flask应用程序的主要运行逻辑。
-
代码包管理:
-
后端层面
- 使用Python的包管理工具,将代码封装为多个包,如
models、urls等,方便管理。 - 在
/backend/__init__.py中实现了包的初始化。 - 添加了
requirements.txt,方便其他用户配置PyPi包环境。
- 使用Python的包管理工具,将代码封装为多个包,如
-
前端层面
- 使用NPM/Yarn进行包管理,较好地实现了代码复用。
-
-
套接字编程和RESTFul API支持:
- 使用Flask框架实现了套接字编程,实现了对请求的响应。
- 通信范式按照
RESTFul API规范进行设计,确保了设计的通用性、规范性和可扩展性。- 通过
PUT、GET、POST、DELETE的指令和FormData参数传送,实现了API的良好实现。
- 通过
- 在
/backend/app.py中实现了Flask应用程序的主要运行逻辑。
我们实现了一个优雅、实用性强、代码规范的网页界面,基于Vue开发。
页面整体效果如下:
![]() |
![]() |
|---|
我们总结出该前端界面的以下特点,并因此认为这是一个具有良好效果的数据库应用系统管理界面:
- 基于包管理的现代开发方式,使用了Vite(项目基座)、Vue(基础框架)、Tailwind(原子CSS复用)、ElementPlus(UI组件风格)、Axios(后端数据请求)、DayJS(日期处理)等流行的NPM库,使得代码可读性强、实现效果流畅。
- 支持后端API的所有功能,可以很好的进行展示和测试。
- 是一套完整、自洽的系统,使用流畅,包含各种主要数据结构的列表展示和操作,并考虑到了多处的性能优化,使得只需要使用这套系统,就可以快速完整执行所有操作。
- 美观优雅、风格统一,自定义程度高,整体使用ElementPlus统一设计风格,并通过GoldenLayout实现多个界面的自由组织。
-
用户权限:
- 使用Postgres实现用户权限的配置,限制用户对数据库的访问权限。
- 首先在Postgres中执行以下代码
CREATE USER read_user WITH PASSWORD '123456'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_user; CREATE USER write_user WITH PASSWORD '123456'; GRANT ALL PRIVILEGES ON DATABASE project1 TO write_user; GRANT INSERT ON lines TO write_user; GRANT UPDATE ON lines TO write_user; GRANT DELETE ON lines TO write_user;
- 在
/backend/config.py中配置了数据库连接字符串,实现了用户权限的配置。 - 在
/backend/app.py中利用SQLAlchemy配置了数据库连接字符串,实现了用户权限的配置。app.read_engine = create_engine('postgresql://read_user:123456@localhost/project1') app.write_engine = create_engine('postgresql://write_user:123456@localhost/project1')
- 在
/backend/controllers.py中实现了对数据库的增删改查操作,实现了用户权限的配置。 - 我们定义了两个路由
/read_user和/write_user,分别用于读取和写入用户数据。 - 对于
/read_user路由,我们在GET请求中调用read_user_read_controller()函数来读取用户数据,而在POST请求中调用read_user_write_controller()函数来写入用户数据。 - 对于
/write_user路由,我们在GET请求中调用write_user_read_controller()函数来读取用户数据,而在POST请求中调用write_user_write_controller()函数来写入用户数据。 - 如果接收到的请求方法不是GET或POST,我们将返回"Method is Not Allowed"的错误信息和405状态码。
- 测试时,你可以使用工具(如Postman)来发送GET或POST请求,查看返回的结果是否符合预期。
- 根据测试结果来看,我们成功地实现了用户权限的配置,限制了用户对数据库的访问权限。
![]() |
![]() |
![]() |
![]() |
-
触发器:
-
我们使用SQLAlchemy的事件监听功能来实现触发器,对数据库的操作进行自动化处理。具体来说,我们在插入新的
Line或Station记录等事件之前,设置了一些默认值。 -
以下以这两种情况为例:
-
对于
Line模型,我们在插入新的线路记录之前,如果没有指定business_carriage(商务车厢),则默认为0。这是通过before_insert事件监听器和default_business_carriage静态方法实现的。代码如下:@staticmethod def default_business_carriage(mapper, connection, target): if target.business_carriage is None: target.business_carriage = 0 event.listen(Line, 'before_insert', Line.default_business_carriage)
-
对于
Station模型,我们在插入新的站点记录之前,如果没有指定status(站点状态),则默认为opening。这是通过before_insert事件监听器和default_status静态方法实现的。代码如下:@staticmethod def default_status(mapper, connection, target): if target.status is None: target.status = 'opening' event.listen(Station, 'before_insert', Station.default_status)
-
-
这种方法的优点是,我们可以在不改变数据库结构的情况下,对数据进行预处理和验证,提高了数据的一致性和完整性。
-
Flask默认包含的服务器组件高并发能力比较孱弱。为了解决这一问题,我们搜索了对于Python服务器的高并发解决方案,并决定使用Tornado作为服务器终端。运行Tornado服务器的指令是python tor.py。
Tornado是一个Python网络库,主要用于非阻塞网络连接的开发,是一个轻量级的网络框架。Tornado的特点是拥有一个高效的网络并发处理能力,特别适合用于处理长连接、WebSocket 和其他需要与每个用户保持持续连接的应用。其核心特点是非阻塞异步IO库。这意味着你可以同时处理数以千计的连接。
我们使用siege进行压力测试,具体使用的语句为:siege -c <THREADS_NUM> -r 10 -b http://127.0.0.1:5000/stations/5。
以下是高并发的测试结果,可以看到,当线程数超过500时,Tornado可以明显提升服务器可用性。









