Extreme programing 方利喆 _ 江贤晟
1. github项目访问地址以及协作次数截图


2. 项目概要
本项目实现了一个 Vue3+Flask+MySQL 前后端分离通讯录系统,在第一次作业的基础上增加了收藏联系人、导入导出excel表格、分组管理、多联系方式创建新联系人等等功能。
项目采用模块化设计,包括:
- 前端:Vue3(负责界面展示与用户交互,通过 Axios 调用后端 API)
- 后端:Python(Flask 框架,提供 RESTful API,处理业务逻辑)
- 数据库:MySQL(存储联系人数据)
- 通信方式:HTTP 协议(JSON 格式数据交互)
主要功能如下:
- 联系人管理
- 多字段支持:电话 、邮箱、微信、QQ、具体住址、小红书、抖音等等
- 联系人收藏(书签)
- 分组管理(无分组、同学、老师)
- 姓名搜索 / 条件筛选
- Excel 导出联系人
- Excel 导入联系人
3.PSP团队协作分工表格
方利喆:
| PSP 阶段 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|
| 计划 | 20 | 25 |
| 前端页面设计 | 80 | 95 |
| 交互逻辑开发 | 100 | 120 |
| 接口调用与封装 | 60 | 70 |
| 文件上传与下载交互 | 40 | 50 |
| 联调与测试 | 40 | 55 |
| 总结与文档编写 | 20 | 25 |
| 总计 | 460 | 540 |
江贤晟:
| PSP 阶段 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|
| 计划 | 20 | 20 |
| 数据库模型设计 | 60 | 70 |
| 后端 API 开发 | 120 | 150 |
| 多联系方式处理 | 50 | 60 |
| Excel 解析与生成 | 80 | 100 |
| 单元测试与调试 | 50 | 65 |
| 文档与部署配置 | 30 | 40 |
| 总计 | 410 | 505 |
4. 功能展示
由于基础功能在第一次作业中已经展示,本次作业仅展示新增功能。
4.1 收藏联系人
在新建联系人的时候可以选择是否确定为收藏联系人,也可以手动对已有联系人进行收藏,并在"显示收藏"功能中进行查看。

4.2 多联系方式新建联系人
本项目支持多联系方式添加联系人--电话 、分组、邮箱、微信、QQ、具体住址、小红书、抖音,是否收藏等等。

4.3 导出联系人excel表格
一键导出所有已存联系人信息。

4.4 导入联系人excel表格
对于有该功能需求的用户,我们给出导入数据的标准格式:


5.功能实现
5.1 收藏联系人
收藏功能通过在 Contact 模型中设置一个布尔字段 is_favorite 来标识是否收藏。当用户点击"收藏/取消收藏"时,前端调用 PATCH /api/contacts/{id}/favorite 接口,后端查出对应联系人,将其 is_favorite 值取反并保存到数据库,再返回最新状态。整个过程无需传递具体值,由服务端自动完成状态切换,简洁高效;同时,查询接口支持按 is_favorite 筛选,便于前端展示收藏列表。
切换收藏状态的接口:
python
@app.route('/api/contacts/<int:id>/favorite', methods=['PATCH'])
def toggle_favorite(id):
contact = Contact.query.get_or_404(id)
contact.is_favorite = not contact.is_favorite
db.session.commit()
return jsonify({'is_favorite': contact.is_favorite})
代码解释:
路由 :/api/contacts/<int:id>/favorite,使用 PATCH 方法表示对资源的部分更新。
参数 :路径中的 <int:id> 是要操作的联系人 ID。
逻辑流程:
- 通过
Contact.query.get_or_404(id)从数据库查找对应联系人,若不存在则返回 404 错误。 - 将该联系人的
is_favorite字段取反(True↔False),实现"切换"效果。 - 调用
db.session.commit()将更改持久化到数据库。 - 返回更新后的收藏状态(JSON 格式),供前端同步 UI。
这是一个 布尔状态切换(toggle) 操作,无需前端传入新值,后端自动翻转。
查询时支持按收藏状态筛选:
在获取所有联系人的接口中,支持通过 is_favorite 参数过滤:
python
@app.route('/api/contacts', methods=['GET'])
def get_contacts():
is_favorite = request.args.get('is_favorite') # 'true'/'false' or None
query = Contact.query.join(Contact.contact_infos).outerjoin(Group)
if is_favorite is not None:
query = query.filter(Contact.is_favorite == (is_favorite == 'true'))
# ...
说明:
- 当前端请求如
/api/contacts?is_favorite=true时,只返回已收藏的联系人。 - 这使得前端可以单独展示"收藏列表"。
5.2 多联系方式新建联系人
该功能采用"主从表"结构设计:Contact 表存储联系人基本信息(如姓名、分组),而 ContactInfo 表以一对多关系存储多个联系方式(如电话、邮箱)。创建时,先插入主记录并立即获取其自增 ID(通过 flush()),再遍历前端传入的联系方式列表,为每一条有效信息创建 ContactInfo 子记录,并关联到主联系人 ID。所有操作在同一个数据库事务中提交,确保数据完整性------若任一环节失败,整个联系人不会被创建,从而避免出现"有主记录无联系方式"或"联系方式孤立"的脏数据。
python
@app.route('/api/contacts', methods=['POST'])
def create_contact():
data = request.json # 接受请求数据
if not data.get('name'): # 校验必要字段
return jsonify({'error': '姓名不能为空'}), 400
# 创建联系人主记录
contact = Contact(
name=data.get('name'),
group_id=data.get('group_id'),
is_favorite=data.get('is_favorite', False)
)
db.session.add(contact)
db.session.flush() # 获取contact.id
# 添加联系方式
infos = data.get('contact_infos', [])
for info in infos:
if info.get('info_type') and info.get('info_value'):
contact_info = ContactInfo(
contact_id=contact.id,
info_type=info['info_type'],
info_value=info['info_value']
)
db.session.add(contact_info)
db.session.commit() # 统一提交事务,保证数据一致性
return jsonify({'message': '联系人创建成功', 'contact': contact.id}), 201
创建主联系人记录
python
contact = Contact(...)
db.session.add(contact)
db.session.flush() # 关键:立即写入数据库以获取自增 ID
flush()不提交事务,但会执行 SQL 并填充contact.id,使得后续能关联子表。
遍历并创建多条联系方式
python
infos = data.get('contact_infos', [])
for info in infos:
if info.get('info_type') and info.get('info_value'):
contact_info = ContactInfo(...)
db.session.add(contact_info)
- 每个联系方式包含类型(如
"phone"、"email")和值。 - 只有当
info_type和info_value都存在时才创建,避免空数据入库。
5.3 导入导出excel联系人表格
导出功能通过查询数据库中所有联系人及其关联的分组和多条联系方式,将数据结构化后交由 generate_excel 工具函数生成标准 Excel 文件,并通过 HTTP 响应头正确触发带中文文件名的下载;导入功能则接收用户上传的 Excel 文件,先校验格式并保存到临时目录,再通过 parse_excel 解析为结构化数据,最后在数据库事务中批量创建主联系人记录及其多条联系方式子记录。整个过程利用了 Flask 的文件处理能力、SQLAlchemy 的关系映射和事务控制,确保数据一致性与用户体验,同时通过工具函数解耦核心逻辑与文件 I/O 操作。
导出联系人excel表格:
python
@app.route('/api/contacts/export', methods=['GET'])
def export_contacts():
contacts = Contact.query.outerjoin(Group).all()
# 序列化联系人数据(包含分组和联系方式)
contacts_data = []
for contact in contacts:
contacts_data.append({
'id': contact.id,
'name': contact.name,
'group': {
'id': contact.group.id,
'name': contact.group.name
} if contact.group else None,
'is_favorite': contact.is_favorite,
'contact_infos': [
{
'info_type': info.info_type,
'info_value': info.info_value
} for info in contact.contact_infos
]
})
# 生成Excel文件(接收文件路径和文件名)
file_path, file_name = generate_excel(contacts_data)
# 修复:处理中文文件名下载,使用make_response包装
response = make_response(send_file(file_path, as_attachment=True))
# 编码文件名,避免中文乱码
response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{urllib.parse.quote(file_name)}"
return response
- 查询所有联系人及其关联数据
- 使用
outerjoin(Group)确保即使没有分组的联系人也能被导出。 contact.contact_infos利用了 SQLAlchemy 的关系(relationship)自动加载多条联系方式。
- 使用
- 构造结构化数据
contacts_data- 每个联系人包含:ID、姓名、分组信息(可能为
None)、是否收藏、以及多个联系方式(列表)。
- 每个联系人包含:ID、姓名、分组信息(可能为
- 调用工具函数生成 Excel
generate_excel(contacts_data)是自定义工具函数(位于utils.py),负责将数据写入.xlsx文件并返回文件路径和建议文件名。
- 安全返回文件供下载
- 使用
send_file(..., as_attachment=True)触发浏览器下载。 - 通过
filename*=UTF-8''{...}+urllib.parse.quote()正确编码中文文件名,避免乱码(符合 RFC 5987 标准)。
- 使用
导入联系人excel表格:
python
@app.route('/api/contacts/import', methods=['POST'])
def import_contacts():
if 'file' not in request.files:
return jsonify({'error': '未上传文件'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': '文件名不能为空'}), 400
if file and allowed_file(file.filename):
# 保存文件
filename = os.path.join(Config.UPLOAD_FOLDER, file.filename)
file.save(filename)
# 解析Excel
contacts_data, error = parse_excel(filename)
if error:
return jsonify({'error': f'解析Excel失败:{error}'}), 400
# 批量添加联系人
for contact_data in contacts_data:
group_id = contact_data.get('group_id')
contact = Contact(
name=contact_data['name'],
group_id=group_id,
is_favorite=contact_data['is_favorite']
)
db.session.add(contact)
db.session.flush()
# 添加联系方式
for info in contact_data['infos']:
contact_info = ContactInfo(
contact_id=contact.id,
info_type=info['info_type'],
info_value=info['info_value']
)
db.session.add(contact_info)
db.session.commit()
return jsonify({'message': f'成功导入{len(contacts_data)}个联系人'}), 201
else:
return jsonify({'error': '仅支持xlsx和xls格式'}), 400
-
文件上传校验
- 检查是否有文件、文件名是否为空、扩展名是否为
.xlsx或.xls(通过allowed_file()函数判断)。
- 检查是否有文件、文件名是否为空、扩展名是否为
-
保存并解析 Excel
- 文件临时保存到
UPLOAD_FOLDER。 - 调用
parse_excel(filename)(来自utils.py)读取 Excel 内容,返回结构化数据列表contacts_data和可能的错误信息。
- 文件临时保存到
-
批量创建联系人与联系方式
pythoncontacts_data为每个联系人:
- 创建
Contact主记录; - 使用
flush()获取contact.id; - 遍历其
infos列表,创建对应的ContactInfo子记录。
- 创建
-
统一提交事务
- 所有导入操作在一个事务中完成,保证原子性:要么全部成功,要么全部回滚。
6.团队分工以及贡献率
| 任务模块 | 方利喆(贡献率50%) | 江贤晟(贡献率50%) |
|---|---|---|
| 需求分析与设计 | 参与 UI/UX 设计,定义用户交互流程 | 参与数据模型设计,定义 API 接口规范 |
| 前端开发 | 负责 Vue/React 页面开发,实现联系人列表、表单、收藏、导入导出等界面 | --- |
| 后端 API 开发 | 实现部分 API 调用逻辑(如请求封装、错误处理) | 主导开发 Flask 后端:联系人增删改查、分组管理、多联系方式、收藏切换等核心接口 |
| 数据库设计 | 协助确认字段合理性 | 设计并实现 Contact、Group、ContactInfo 模型,配置关系与级联删除 |
| Excel 导入导出功能 | 调用后端接口,实现前端文件上传与下载交互 | 编写 generate_excel() 和 parse_excel() 工具函数,实现后端 Excel 解析与生成逻辑 |
| 测试与调试 | 进行前端功能测试、接口联调 | 编写后端单元测试,排查数据库事务与文件 I/O 问题 |
7.遇到的问题以及解决方式
(1)创建联系人时无法获取 contact.id 来关联联系方式
原因分析:直接 commit() 前未刷新会话,导致自增 ID 未生成。
解决方法:使用 db.session.flush() 在 commit() 前强制写入并获取 ID,但不提交事务。
(2)前后端对"收藏状态"理解不一致(true/false vs 1/0)
原因分析:类型转换错误(字符串 vs 布尔)。
解决方法:后端统一用布尔值;前端确保传 is_favorite: true/false 而非 "true"。