164
社区成员
发帖
与我相关
我的任务
分享姓名: 程一鸣 学号: FZU:832302227, MU:23124792 GitHub ID: tomcheng9421-sketch
| 课程名称 | 软件工程 |
|---|---|
| 作业要求 | 实现前后端分离的通讯录管理系统 (前后端分离,云服务器部署) |
| 作业目标 | 掌握前后端分离开发模式,实现联系人的增删改查,熟悉云端部署流程 |
| 技术栈 | Python Flask, HTML/CSS/JS, Render, Vercel, Git |
$$TOC$$
本项目严格遵循前后端分离的开发规范,代码托管于 GitHub,前端与后端分别存放于两个独立的仓库。
本次项目复现的 PSP (Personal Software Process) 表格如下,其中“实际时间”包含了大量用于环境配置、排查网络故障和解决云端部署“踩坑”的时间。
| 阶段 | 步骤 | 内容说明 | 预估时间(h) | 实际时间(h) |
|---|---|---|---|---|
| Planning | 计划 | 明确技术栈 (Flask + JS),确定云部署方案 (Render + Vercel) | 1.0 | 1.0 |
| Development | 需求分析 | 分析通讯录功能 (增删改查、扩展字段、过滤) | 0.5 | 1.0 |
| 后端开发 | 搭建 Flask API,定义 models.py,实现 CRUD 接口 | 3.0 | 4.5 | |
| 前端开发 | 编写 index.html 布局、style.css 美化、app.js 逻辑 | 3.0 | 4.0 | |
| Testing | 本地调试 | 联调本地前后端,解决 venv 激活、contacts.db 不同步等问题 | 1.5 | 3.5 |
| Deployment | 部署准备 | 解决 git push 网络故障,清理 .idea 文件 | 0.5 | 2.0 |
| 后端部署 | 部署 Flask 到 Render,配置 PostgreSQL,解决 DATABASE_URL 环境变量问题 | 1.0 | 3.0 | |
| 部署踩坑 | (预估未包含) | (1.0) | ||
| (解决 Render 免费版 'Shell' 禁用,实现 'Plan B' 自动建表) | ||||
| 前端部署 | 部署 HTML/JS 到 Vercel,修复 API_URL 指向,解决 Failed to fetch 问题 | 0.5 | 1.5 | |
| Evaluation | 总结与报告 | 撰写博客,总结开发与部署过程中的所有“坑” | 1.0 | 1.5 |
| 合计 | 11.0 | 22.0 |
主界面采用现代化卡片式设计,集成了 Font Awesome 图标库,观感简洁。顶部为“按姓名选择”和“按标签选择”的动态筛选区域。下方为联系人列表,每个联系人以卡片形式展示其所有信息。

左侧为“添加新联系人”表单,支持填写姓名、电话、地址、邮箱,并可通过复选框选择“家人”、“朋友”等标签。

点击联系人卡片上的“修改”按钮,该联系人的信息(包括姓名、电话、地址、邮箱和已选中的标签)就可被修改。修改完成后点击“保存修改”,列表将自动刷新。

点击联系人卡片上的“删除”按钮,会弹出浏览器 confirm 对话框,防止用户误操作。确认后,前端向后端发送 DELETE 请求,成功后自动刷新列表。


顶部的两个筛选框(一个输入框,一个下拉框)实现了前端实时过滤。当用户在“按姓名选择”https://i-blog.csdnimg.cn/communtity/e4a6761f5e5e4123bf38832804997418.png "#left")
框输入文字,或在“按标签选择”下拉框中选择一项时,app.js 会立即重新渲染联系人列表,仅显示符合所有条件的联系人。
本项目在基础的“姓名”和“电话”之上,成功扩展了 address (地址), email (邮箱), tags (标签) 三个字段。所有字段均可在前后端进行增、删、改、查,并在前端卡片上清晰展示。
本系统是一个彻底的前后端分离架构,职责清晰:
前端 (Vercel):
后端 (Render):
数据库 (Render):
数据流:
通讯录系统
├── 前端 (Vercel)
│ ├── 视图 (index.html)
│ ├── 样式 (style.css)
│ └── 逻辑 (app.js)
│ ├── API_URL (连接后端)
│ ├── fetchContacts() (获取/渲染)
│ ├── Add/Edit/Delete (增删改)
│ └── Filters (过滤)
└── 后端 (Render)
├── 应用 (run.py / src/__init__.py)
│ ├── 自动创建DB (db.create_all())
│ └── CORS (跨域)
├── API 路由 (controller/concacts.py)
│ ├── /api/contacts (GET, POST)
│ └── /api/contacts/<id> (PUT, DELETE)
└── 数据库 (models.py / PostgreSQL)
└── Contact 表 (name, phone, address, email, tags)
基于扩展后的需求,我们最终的数据库模型 (在 src/models.py 中定义) 如下:
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | Integer | Primary Key | 主键ID |
| name | String(80) | Not Null | 姓名 |
| phone | String(20) | Not Null | 电话 |
| address | String(200) | Nullable | 地址 |
| String(120) | Nullable | 邮箱 | |
| tags | String(100) | Nullable | 标签 (如 "家人,朋友") |
app.js 的第一行代码是整个前后端分离项目能否在云端工作的命脉。
部署前 (本地开发):
// app.js (修改前)
const API_URL = 'http://localhost:5000/api';
部署后 (云端生产):
在部署到 Vercel 之前,必须将其修改为指向 Render 后端的公网地址,否则前端将无法找到后端,导致 Failed to fetch 错误。
// app.js (修改后)
const API_URL = 'https://eight32302227-contacts-backend.onrender.com/api';
app.js 中的 fetchContacts 函数是前端的核心,它负责从后端拉取数据、执行前端过滤、并动态生成 HTML 渲染到页面。
async function fetchContacts() {
contactsList.innerHTML = '<li>加载中...</li>';
try {
const response = await fetch(`${API_URL}/contacts`);
let contacts = await response.json();
// --- 过滤逻辑 ---
const nameFilter = filterNameInput.value.toLowerCase();
const tagFilter = filterTagSelect.value;
contacts = contacts.filter(contact => {
const matchesName = nameFilter === '' || contact.name.toLowerCase().includes(nameFilter);
const matchesTag = tagFilter === '' || (contact.tags && contact.tags.includes(tagFilter));
return matchesName && matchesTag;
});
// --- 过滤逻辑结束 ---
contactsList.innerHTML = ''; // 清空列表
if (contacts.length === 0) {
contactsList.innerHTML = '<li>暂无联系人...</li>';
return;
}
contacts.forEach(contact => {
// ... (此处省略动态创建 li 元素的 HTML) ...
contactsList.appendChild(li);
});
} catch (error) {
console.error('获取联系人失败:', error);
contactsList.innerHTML = '<li>加载联系人失败</li>';
}
}
后端的 Contact 模型定义了数据库的结构,与前端的需求完全对应。
# src/models.py
from . import db # 从 src/__init__.py 导入 db 实例
class Contact(db.Model):
__tablename__ = 'contacts'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
phone = db.Column(db.String(20), nullable=False)
# --- 新增字段 ---
address = db.Column(db.String(200), nullable=True) # 可以为空
email = db.Column(db.String(120), nullable=True) # 可以为空
tags = db.Column(db.String(100), nullable=True) # 可以为空,存储如 "家人,朋友"
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"phone": self.phone,
"address": self.address,
"email": self.email,
"tags": self.tags
}
这是本次部署中最关键的“踩坑”和“填坑”!
问题: Render 免费版禁用了 "Shell" (命令行) 功能,导致我们无法手动登录服务器执行 db.create_all() 来创建数据库表。
解决方案 (Plan B):
修改应用工厂函数,让 Flask 应用在每次启动时,自动检查并创建数据库表。
# src/__init__.py (关键代码片段)
def create_app():
# ... (前面的 app, db, CORS 配置) ...
# 5. 导入并注册蓝图
from .controller import concacts
app.register_blueprint(concacts.bp)
# 6. 确保模型被导入
from . import models
# --- B 计划:在这里添加这两行! ---
# 在应用启动时,自动在云端创建数据库表
# db.create_all() 会检查表是否存在,如果不存在,则创建。
with app.app_context():
db.create_all()
# --- B 计划结束 ---
return app
这次通讯录系统的复现,远不止是“写代码”,而是一次从零开始、贯穿本地开发、网络调试、云端部署的全栈实战。最大的收获不是来自成功,而是来自排错。
第一关:本地环境。
在项目刚开始,就遇到了 PowerShell 的“下马威”。venv 虚拟环境的激活命令在 PowerShell 下是 .\venv\Scripts\activate 而不是 source venv/bin/activate,并且还被“执行策略”(Set-ExecutionPolicy) 拦截。这让我第一次深刻理解到,工具和环境的配置是项目的第一道门槛。
第二关:数据库不同步。
在本地开发时,当我为 models.py 添加了 address, email 等新字段后,前端立刻报错 no column named address。我花了很长时间才意识到:代码改了,但 instance/contacts.db 数据库文件还是旧的! 必须手动删除旧的 .db 文件,重启 run.py,让 SQLAlchemy 重新生成表结构。这个教训让我明白,代码和数据状态必须时刻保持同步。
第三关:网络故障。
当我准备 git push 上传代码时,连续遇到了 Connection was reset 和 Failed to connect 错误。这与代码完全无关,纯粹是网络问题。通过关闭代理、切换手机热点等方式才最终推送成功。这让我学会了区分“代码错误”和“环境错误”。
第四关:云端部署“踩坑”。
这才是本次项目的“BOSS 战”。
SQLALCHEMY_DATABASE_URI must be set。原因是我在创建服务时,忘记在 Advanced (高级) 选项里添加 DATABASE_URL 环境变量。src/__init__.py 中添加 with app.app_context(): db.create_all(),让应用在每次启动时自动建表。git push 这个修改后,Render 自动重新部署,问题解决!Failed to fetch。这是因为我的 app.js 里的 API_URL 还是 localhost:5000。我必须再次修改 app.js,将其指向 Render 的公网 URL,然后再次 git push,Vercel 自动重新部署后,才最终成功。总结:
一个“简单”的通讯录,却串联起了 Git、Python、Flask、SQLAlchemy、PostgreSQL、Gunicorn、JavaScript、CORS、RESTful API、CI/CD (Vercel/Render 的自动部署) 等无数个知识点。我深刻体会到,一个真正的“软件工程师”,不仅要会写代码,更要是一个会排查环境、调试网络、解决部署问题的“侦探”。