1. 先把这个标题拆明白:flask和django出现在同一个标题里到底怎么做
相信不少人在找项目参考的时候都见过这类标题——“Python flask flask二手车交易管理系统的设计与实现-vue pycharm django”,关键词又长又绕,flask出现两次,后面还跟着django,新手很容易被搞懵:到底用flask还是用django?这个系统到底是什么架构?pycharm又起什么作用?
我最初看到这个标题时也愣了一下,后来仔细梳理了一下,发现这类标题其实是多个模板来源拼凑出来的产物:要么是参考了不同课设项目的命名习惯,要么是搜索引擎关键词堆砌之后的结果。真正动手做的时候,你必须先做一道判断题——这套系统到底按哪条技术路线走。
我的结论是:如果标题核心是“flask”且明确带着“vue”,那后端主框架就选定Flask,前端用Vue做前后端分离;django可以作为对比项存在,但不作为本项目的主实施路径。 理由很直接:Flask配合Vue做API后端非常顺,路由简洁、ORM选型自由、部署包也小,非常适合管理类系统的中期规模开发。而且PyCharm对Flask项目的支持很成熟,断点调试、环境变量管理、数据库工具都是开箱即用,这对一个人完成整个项目来说是实打实的效率优势。
下面这篇文章,我会以“二手车交易管理系统”为业务背景,把从需求拆分、数据库设计、Flask接口编写、Vue页面搭建,到PyCharm联调排错、上线的完整过程梳理一遍。文章目标只有一个:让你照着这套思路,能真把一个能跑、能演示、能交差的管理系统做出来,而不是停留在“看了一堆教程但不知道从哪下手”的状态。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 写代码之前必须想清楚的几件事:这套系统到底在管什么
很多人一上来就建Flask项目、写接口,结果写到一半发现页面、角色、状态全对不上,只能推翻重来。二手交易系统的业务边界其实比想象中清晰,我建议你先把下面这三件事画在纸上。
2.1 业务模块不是随便堆的,二手车交易核心就这五块
这个系统表面上叫“管理系统”,但千万别把它做成一个普通CRUD。二手车交易的特殊性在于——车辆有生命周期,交易有流程,角色有权限。我按实际业务收敛后,后台功能只需要五块:
| 业务模块 | 核心动作 | 说明 |
|---|---|---|
| 车辆管理 | 发布、编辑、上下架、删除 | 车辆状态是整个系统的核心 |
| 车辆检索 | 列表、条件筛选、排序、详情 | 车辆品牌、价格区间、车龄、里程 |
| 交易管理 | 下单、支付、确认成交、取消 | 订单状态必须可追踪 |
| 用户管理 | 注册、登录、个人中心、角色分配 | 常见角色:普通用户、车商/卖家、管理员 |
| 系统辅助 | 收藏、浏览记录、统计看板 | 提升系统完整性,演示时加分 |
这五块做下来,系统在功能上就是完整的。不需要再塞什么“新闻发布”“留言板”之类的功能,那只会分散你的精力。
2.2 三种角色谁管什么,决定了接口权限怎么设计
很多课设系统只有登录和退出,接口所有人都能调,这在演示时会被老师或面试官一句话问住:任何人都能下架别人的车辆吗?任何用户都能修改订单价格吗?
为了避免这种情况,我最终设计了三种角色:
- 普通用户(buyer):浏览车辆、收藏车辆、下单购车、查看自己订单。
- 卖家/车商(seller):在普通用户基础上,增加车辆发布、编辑、上下架操作,只能操作自己的车辆。
- 管理员(admin):用户管理、车辆审核与强制下架、平台订单总览、简单统计。
这套权限模型写起来不复杂,无非是在接口层加一个装饰器判断。关键在于你要把角色概念融入数据库设计和前端路由,页面也要跟着角色显示或隐藏,这个后文会展开。
2.3 车辆状态和订单状态是两套状态机,别混在一起
这是我给这个项目定义的最重要的约束。二手车业务里,车和订单是两条独立生命周期:
- 车辆状态:
在售 / 已预订 / 已售出 / 已下架 - 订单状态:
待支付 / 已支付 / 已完成 / 已取消
一个典型场景:买家下单后,车辆从“在售”变成“已预订”,此时其他用户还能看车但不能重复下单;买家支付后,车辆变成“已售出”;如果订单取消,车辆回到“在售”。这个状态流转逻辑,比单纯做增删改查有价值得多,也更容易打动看你项目的人。
3. 数据库表结构怎么设计才不会被老师说“像作业”
管理系统的地基就是表结构。二手交易系统的表不需要太多,但关系要理得清楚。我最终保留了五张核心表:用户表、车辆表、订单表、收藏表、图片表。下面我逐一说明关键字段和设计原因。
3.1 用户表:不要明文存密码,角色字段要留扩展余地
用户表是最基础的,但也是最容易出问题的地方。第一次做项目时我图省事,密码直接明文存,后来被指出问题才换成了哈希存储。
python复制class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(128), nullable=False)
role = db.Column(db.String(16), default='buyer') # buyer / seller / admin
phone = db.Column(db.String(20))
avatar_url = db.Column(db.String(255))
create_time = db.Column(db.DateTime, default=datetime.utcnow)
注意几点:
password_hash用 Werkzeug 自带的generate_password_hash和check_password_hash生成,不要自己写“加密算法”。role字段用字符串而不是数字,理由是代码可读性好,接口里判断user.role == 'admin'比user.role == 2直观得多。create_time统一用 UTC 存储,展示时再转本地时区,避免后面出现“差8小时”的诡异bug。
3.2 车辆表:车况字段宁可多备,也不要后面加字段
车辆信息是整个系统的门面。二手车的信息维度比新车多,买家最关心的是车龄、里程和价格。我的车辆表字段如下,你可以直接参考:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| car_no | varchar(64) | 车架号,唯一 |
| brand | varchar(32) | 品牌,如“丰田” |
| model | varchar(64) | 车型,如“凯美瑞 2.0G” |
| year | int | 上牌年份 |
| mileage | float | 行驶里程(万公里) |
| price | decimal(10, 2) | 售价 |
| color | varchar(16) | 外观颜色 |
| engine_desc | varchar(128) | 发动机排量/变速箱描述 |
| status | varchar(16) | 在售 / 已预订 / 已售出 / 已下架 |
| seller_id | int | 发布者用户id |
| description | text | 车况描述 |
| create_time / update_time | datetime | 创建和更新时间 |
car_no 也就是车架号,用唯一索引,可以防止同一辆车被重复录入。status 字段要建索引,因为列表页最频繁的查询就是“按状态筛选在售车辆”。mileage 用 float 或 decimal 都行,存的是类似“5.2”“8.6”这样的数字,前端展示时拼上“万公里”即可。
3.3 订单表:订单号生成规则和价格快照别忽略
订单表的核心不只是记录“谁买了谁的车”,还要记录成交价格快照。为什么要有快照?因为车辆价格可能被卖家修改,如果订单表不存价格,展示历史订单时价格就对不上了。
python复制class Order(db.Model):
__tablename__ = 'order'
id = db.Column(db.Integer, primary_key=True)
order_no = db.Column(db.String(32), unique=True, nullable=False, index=True)
seller_id = db.Column(db.Integer, nullable=False)
buyer_id = db.Column(db.Integer, nullable=False)
car_id = db.Column(db.Integer, nullable=False)
price = db.Column(db.Numeric(10, 2), nullable=False) # 成交时价格快照
status = db.Column(db.String(16), default='pending') # pending / paid / done / canceled
create_time = db.Column(db.DateTime, default=datetime.utcnow)
pay_time = db.Column(db.DateTime)
finish_time = db.Column(db.DateTime)
order_no 我推荐用时间戳加随机数生成,比如 time.strftime('%Y%m%d%H%M%S') + str(random.randint(1000, 9999)),不依赖数据库自增主键,方便接口层直接返回给前端。测试过程中,如果遇到重复订单号,加一个随机四位数字就基本解决了。
3.4 收藏表:联合唯一约束防止重复收藏
收藏功能本身不难,难在细节。两个用户收藏同一辆车没问题,但同一个用户重复收藏同一辆车应该被数据库拒绝。所以收藏表要建联合唯一索引:
python复制class Favorite(db.Model):
__tablename__ = 'favorite'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, nullable=False)
car_id = db.Column(db.Integer, nullable=False)
create_time = db.Column(db.DateTime, default=datetime.utcnow)
__table_args__ = (
db.UniqueConstraint('user_id', 'car_id', name='unique_user_car'),
)
这一步做完,后面接口里连“是否已收藏”的判断都可以通过查表完成,逻辑非常干净。
4. Flask后端这样搭,接口写起来又快又不容易乱
数据库设计完,就可以动手写Flask了。这一个项目里我没有选择Flask内置的 app.route 一把梭,而是用了蓝图 + 工厂模式,因为车辆、订单、用户三块业务的接口越来越多,全堆在一个文件里后期没法维护。
4.1 项目结构先安排好,越到后期越香
我推荐的Flask项目结构长这样,后面写代码、加功能都不需要大规模改动:
code复制car_trade/
├── run.py # 程序入口
├── config.py # 配置项:数据库、JWT密钥等
├── app/
│ ├── __init__.py # 创建Flask app,注册蓝图
│ ├── models.py # 所有SQLAlchemy模型
│ ├── utils.py # 通用工具:分页、响应封装、装饰器
│ ├── api/
│ │ ├── auth.py # 登录注册相关接口
│ │ ├── car.py # 车辆列表、详情、发布、上下架
│ │ ├── order.py # 下单、支付、取消、确认
│ │ └── favorite.py # 收藏接口
run.py 只做一件事:启动服务。app/__init__.py 里注册全部蓝图。关键代码如下:
python复制def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
CORS(app) # 跨域配置,前后端分离必须
from app.api import auth, car, order, favorite
app.register_blueprint(auth.bp, url_prefix='/api/auth')
app.register_blueprint(car.bp, url_prefix='/api/cars')
app.register_blueprint(order.bp, url_prefix='/api/orders')
app.register_blueprint(favorite.bp, url_prefix='/api/favorites')
return app
使用蓝图的好处是每个模块的 bp 相互独立,你甚至可以拉一个同学来写订单模块而你写车辆模块,互不干扰。
4.2 接口怎么写:拿“车辆列表”举例
车辆列表是访问量最大的接口,也是最能体现接口设计水平的接口。我的做法是:
- 支持分页参数
page和size; - 支持按品牌
brand、价格区间min_price和max_price、车辆状态status、关键词keyword筛选; - 返回结果统一格式:
{ "code": 0, "data": {...}, "msg": "success" }。
python复制@car_bp.route('', methods=['GET'])
def get_car_list():
page = request.args.get('page', 1, type=int)
size = request.args.get('size', 12, type=int)
brand = request.args.get('brand', '', type=str)
min_price = request.args.get('min_price', 0, type=float)
max_price = request.args.get('max_price', 0, type=float)
query = Car.query.filter(Car.status == 'on_sale')
if brand:
query = query.filter(Car.brand == brand)
if min_price > 0:
query = query.filter(Car.price >= min_price)
if max_price > 0:
query = query.filter(Car.price <= max_price)
pagination = query.order_by(Car.create_time.desc()).paginate(
page=page, per_page=size, error_out=False
)
data = {
'items': [car.to_dict() for car in pagination.items],
'total': pagination.total,
'page': page,
'size': size,
}
return jsonify(code=0, data=data, msg='success')
注意这里用了个 car.to_dict() 方法,它定义在模型内部,用来把查询结果转成JSON可序列化的字典。这是一个很实用的小技巧,避免了每个接口里都手写字段拼装。
4.3 车辆状态流转:下单和取消要用事务锁住
前面提到订单有状态机,那代码层面怎么保证并发下状态不混乱?举个例子:两个买家几乎同时下单同一辆车,如果接口不做处理,两笔订单都会创建成功,车辆状态也会被覆盖成两个不同的值。
解决方案是使用数据库行锁:
python复制@db.session.begin_nested()
def create_order(buyer_id, car_id):
# 使用 with_for_update() 锁定车辆记录,防止并发重复下单
car = Car.query.filter_by(id=car_id).with_for_update().first()
if not car or car.status != 'on_sale':
raise BizError('该车辆已下架或已售出')
order = Order(
order_no=generate_order_no(),
seller_id=car.seller_id,
buyer_id=buyer_id,
car_id=car.id,
price=car.price,
status='pending'
)
db.session.add(order)
car.status = 'reserved' # 在售 -> 已预订
db.session.flush()
return order
with_for_update() 是PostgreSQL和MySQL都支持的语法,效果是这条车辆记录在这一事务期间被锁住,其他人必须等事务提交后才能操作。在演示时,你可以开两个浏览器窗口同时下单同一辆车,验证只有一个订单能成功,这就是事务的价值。
4.4 登录认证用JWT,前后端分离的标配
Flask里做JWT认证,我推荐 flask-jwt-extended 这个库,步骤少、坑也少。登录成功后签发access_token,前端每次请求在 Authorization: Bearer <token> 里带上它;所有需要登录的接口在蓝图中统一加 @jwt_required() 装饰器。
python复制@auth_bp.route('/login', methods=['POST'])
def login():
data = request.get_json()
user = User.query.filter_by(username=data.get('username')).first()
if not user or not check_password_hash(user.password_hash, data.get('password')):
return jsonify(code=1, msg='用户名或密码错误'), 400
token = create_access_token(
identity=str(user.id),
additional_claims={'role': user.role, 'username': user.username}
)
return jsonify(code=0, data={'token': token, 'role': user.role}, msg='登录成功')
JWT里装载角色信息后,封装一个 require_role('seller') 装饰器,就能做到“只有卖家能发布车辆”“只有管理员能强制下架”这样的权限控制。这个设计在项目答辩时很加分,因为它是完整权限链路的体现,而不是用户名密码校验完就结束。
5. Vue前端页面:用组件拼出后台管理系统
后端接口全通以后,前端就是把这套数据关系用页面还原出来。我推荐使用Vue 3 + Element Plus的组合,组件库成熟、页面风格统一,不用花时间写UI样式。前端项目用Vite初始化非常快,这里不赘述脚手架步骤,重点说页面规划和交互。
5.1 页面路由按角色划分,不能所有页面都裸奔
前端路由要和权限模型对齐。我的路由规划如下:
| 路径 | 页面 | 访问角色 |
|---|---|---|
/login /register |
登录注册 | 游客 |
/cars |
车辆列表 | 所有登录用户 |
/cars/:id |
车辆详情 | 所有登录用户 |
/cars/publish |
发布车辆 | seller, admin |
/orders |
我的订单 | 所有登录用户 |
/favorites |
我的收藏 | 所有登录用户 |
/admin |
管理看板 | admin |
前端做权限拦截不需要太复杂,路由配置里加一个 meta.roles 字段,在路由守卫里判断当前用户的角色是否在该列表中即可。
js复制// router/index.js
const routes = [
{
path: '/cars/publish',
component: PublishCar,
meta: { roles: ['seller', 'admin'] }
}
]
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (!token && to.path !== '/login' && to.path !== '/register') {
next('/login')
} else {
next()
}
})
当然只做前端拦截是不够的,后端接口仍然要校验角色,前端拦截只是让体验更友好。
5.2 车辆发布表单的几个细节
车辆发布页面是卖家最常用的功能,表单字段很多,提交时最容易出问题的是图片上传。
图片上传有两种方案:一种是传base64编码字符串,另一种是FormData文件上传。我推荐用第二种,因为base64会让请求体积膨胀约33%,图片一多接口响应非常慢。
html复制<el-form :model="carForm" label-width="100px">
<el-form-item label="品牌">
<el-input v-model="carForm.brand" />
</el-form-item>
<el-form-item label="车型">
<el-input v-model="carForm.model" />
</el-form-item>
<el-form-item label="上牌年份">
<el-input-number v-model="carForm.year" :min="2000" :max="2025" />
</el-form-item>
<el-form-item label="售价">
<el-input-number v-model="carForm.price" :min="0" :precision="2" />
</el-form-item>
<el-form-item label="车辆图片">
<el-upload
:action="`${baseURL}/api/cars/upload`"
:headers="{ Authorization: `Bearer ${token}` }"
:on-success="handleUploadSuccess"
list-type="picture"
>
<el-button>上传图片</el-button>
</el-upload>
</el-form-item>
</el-form>
handleUploadSuccess 函数里取到后端返回的图片URL,把它拼到 carForm.images 数组里,发布车辆时一次提交到后端。这里要注意一点:token 必须手动塞到上传请求头里,因为 el-upload 组件不会自动读取LocalStorage里的token,这是新手最容易踩的坑。
5.3 车辆列表页的筛选交互
列表页的核心是筛选功能与后端接口参数的联动。我采用了“搜索条件变化后重新调接口”的方式,数据状态维护在Pinia或组件本地状态里都行:
js复制async function fetchCars() {
loading.value = true
try {
const res = await axios.get('/api/cars', {
params: {
page: page.value,
size: size.value,
brand: filterForm.brand,
min_price: filterForm.min_price,
max_price: filterForm.max_price
}
})
carList.value = res.data.data.items
total.value = res.data.data.total
} finally {
loading.value = false
}
}
筛选区用 el-select 做品牌下拉框,价格区间用两个 el-input-number,点击“查询”按钮触发 fetchCars()。这个实现方式很直白,逻辑都在前端,后端完全无状态,前后端职责就分得很清楚。
6. PyCharm下的联调与排错:我踩过的三个高频坑
写代码是一个节奏,联调又是另一个节奏。整个项目开发中,最耗时间的不是写接口,而是“接口通了页面崩了”或者“页面报了奇奇怪怪的错”。下面这三个坑,我在不同项目里都见过,甚至自己都踩过,值得单独拉一章讲。
6.1 Postman里接口正常,浏览器里死活不通:跨域CORS问题
前后端分离架构下,前端运行在localhost:5173,后端运行在localhost:5000,端口不同就属于跨域。Postman之所以“正常”,是因为它不执行浏览器的同源策略,而浏览器会拦截跨域响应。
解决办法很简单:在后端实例化Flask之后加一行 CORS(app),或者更精确地配置允许的来源:
python复制CORS(app, resources={
r"/api/*": {
"origins": ["http://localhost:5173"]
}
})
这样配置后,浏览器发起的带跨域请求就会带上 Access-Control-Allow-Origin 响应头。如果你请求里带了JWT的Authorization头,默认的简单请求不支持,记得把 allow_headers 打开:
python复制CORS(app, supports_credentials=True)
CORS是个非常典型的问题,也是很多课设项目演示时“浏览器白屏、控制台一片红”的元凶。我建议联调一开始就加上,不要在排错阶段浪费时间。
6.2 订单时间显示差8小时:UTC与本地时区混用
二手车交易系统里,订单创建时间、成交时间都是要展示给用户看的重要信息。一开始我把所有时间字段都用 datetime.utcnow 存储,数据库中确实存的是UTC时间。但前端直接把这个时间字符串渲染出来后,所有订单都变成了“下单时间比北京时间慢8小时”。
我的修复方案分两层:
- 后端:数据库仍然存UTC,但在模型序列化时统一转成本地时间。在
to_dict()方法里做转换:
python复制def to_dict(self):
return {
...
'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time else '',
'pay_time': self.pay_time.astimezone(timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S') if self.pay_time else '',
}
- 前端:在后端返回ISO格式时间字符串的前提下,尽量不在前端做时区转换逻辑,统一显示后端转好的字符串。
这里容易犯的错误是,后端转一半、前端又转一遍,结果时间反而错乱了。定一个原则:接口层统一输出可读字符串,前端不处理时区计算。
6.3 高并发下订单状态被覆盖:事务与锁的正确使用
前面在订单接口部分已经提到 with_for_update(),这里我再说明一下为什么不能省略。
刚开始我没有加锁,测试流程是:用户A下单成功,用户B马上下单同一辆车,结果两笔订单都创建成功了,车辆状态最后变成 reserved,但订单表里出现两条 pending 记录。原因是两个请求几乎同时到达,接口先查车状态都是 on_sale,然后各自插入订单,这个“查状态”和“改状态”之间没有原子性保证。
加上 with_for_update() 之后,查询车辆记录时这一行就被行级锁锁定,第二个请求必须等待第一个请求的事务提交后才能读到记录,此时车的状态已经是 reserved,第二个请求就会走“该车辆不可下单”的逻辑。
这个坑不一定在演示时暴露,但如果项目要部署上线,并发场景就会真实出现。提前把事务边界做好,能省掉后续一堆脏数据清理工作。
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
| 浏览器请求接口报跨域 | 前后端分离端口不同 | 后端配置 CORS,允许前端域名访问 |
| 下单时间比实际晚8小时 | 数据库存UTC未转本地 | 后端序列化时统一转北京时间 |
| 同一辆车产生两笔订单 | 查询与更新非原子操作 | 用 with_for_update() 行锁保护状态 |
7. 部署上线时最容易忽视的几件事
本地开发跑通,和真正部署到服务器上跑起来,中间还隔着不少距离。这里我不展开服务器购买和基础环境配置,重点说三个和这个项目直接相关的问题。
7.1 Flask自带的开发服务器不能直接用于上线
很多人习惯 python run.py 直接启动服务,但Flask的开发服务器单线程、性能低,而且不适合承载真实请求。正确的做法是用Gunicorn作为WSGI服务器:
bash复制pip install gunicorn
gunicorn -w 4 -b 127.0.0.1:5000 run:app
-w 4 表示启动4个worker进程,这个数量在普通云服务器上够用了。注意run:app指的是run.py文件里的app变量,如果你的入口是create_app()工厂函数,需要写成:
bash复制gunicorn -w 4 -b 127.0.0.1:5000 "run:create_app()"
如果你在Windows上开发,Gunicorn跑不起来是正常的,它依赖UNIX特性。本地调试用python run.py,部署到Linux服务器再用Gunicorn,分工明确。
7.2 前端打包后交给Nginx托管
Vue项目开发时用Vite开发服务器,线上则需要先构建静态文件:
bash复制npm run build
构建完成后会生成 dist 目录。把 dist 目录里的文件放到服务器Nginx的静态目录下,再做一个反向代理,让 /api 开头的请求转发到Gunicorn监听的 127.0.0.1:5000:
nginx复制server {
listen 80;
server_name your_server_ip;
root /var/www/car_trade/dist;
index index.html;
location /api {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
try_files $uri $uri/ /index.html;
}
}
try_files 这一行很关键,它解决了Vue Router在history模式下刷新404的问题。不配置的话,你从列表页点到详情页再刷新,Nginx会直接返回404。
7.3 上线前要过一遍的安全基线
我不建议给这个系统搞一套复杂的纵深防御,但下面四条是底线,漏掉任何一条都不行:
- JWT密钥:不要用代码里的默认字符串,用环境变量注入,部署时启动命令或
.env文件配一个随机生成的长串。 - 数据库连接信息:不要硬编码在
config.py里,数据库密码要从环境变量读取。 - 管理员账号:初始化数据的默认管理员密码要强制修改,否则别人用默认密码登录系统直接看到所有用户数据。
- 图片上传校验:后端除了验证登录状态,还要校验文件类型和大小,只允许jpg/png,大小限制在5MB以内,防止恶意上传大文件拖垮服务器。
安全相关的内容,项目文档里提一句“生产环境通过环境变量管理密钥”会让整个项目的完成度提升一个档次。
写着写着我发现,这个项目真正难的地方不在于某一个技术点,而在于你能否把“业务逻辑”和“技术实现”对应起来。二手车交易管理系统的核心,是用好车辆状态机和订单状态机,围绕着两个状态去设计表、写接口、做页面,整个系统就顺理成章地立起来了。
最后再分享一个我做课设和项目时的习惯:用PyCharm打开项目后,先把 run.py 跑通,再用Postman把 登录→发布车辆→车辆列表→下单→支付→订单状态 这条主链路走一遍,再开始写前端页面。接口先于页面验证完,后面联调基本就是水到渠成的事,不会出现“页面死活调不通但分不清是前端还是后端问题”的处境。照着这套思路做,你的系统不仅能跑,而且经得起追问。
