ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Flask与MySQL的二手交易平台开发:从表结构到事务与索引优化

基于Flask与MySQL的二手交易平台开发:从表结构到事务与索引优化 简介一份基于 Python Flask 与 MySQL 实现的二手物品交易平台毕业设计完整源码适合计算机相关专业学生、Flask 初学者及需要快速搭建 Web 项目的开发者参考。项目覆盖用户注册登录、商品发布、商品搜索、购物车与交易等核心模块包含前端模板、静态资源、Python 业务逻辑与数据库表结构前后台功能完整可运行并二次开发。压缩包共 79 个文件大小约 1.75MB以 py/pyc 源码、html/css/js 前端文件为主另含 MySQL 的 ibd/frm 数据表文件、项目配置说明、数据库设计 PPT、使用说明文档、README 及演示图片目录结构清晰便于按模块阅读。资源已有 2140 人学习通过完整源码和配套文档读者可系统理解 Flask 路由与模板渲染、MySQL 表结构设计、用户认证与数据交互等关键环节也能借鉴其中的购物流程处理、搜索实现与安全防护思路对毕业设计或课程实训具有直接参考价值。1. 基于 Flask 和 MySQL 的二手交易平台毕业设计的核心在交易闭环毕设选题里电商类项目永远是最稳的方向之一而“二手物品交易平台”比起普通商城多了一层真实感用户既是买家也是卖家涉及用户注册登录、商品发布与下架、订单状态流转、交易金额记录。用 Python Flask 配合 MySQL 来实现技术栈主流、难度适中代码量足够撑起一篇像样的毕业设计论文。先给结论这个项目真正值钱的部分不是页面有多漂亮而是“发布商品 → 下单 → 卖家确认 → 交易完成”这条完整链路能不能顺畅跑通。本文按从零搭建的顺序拆解覆盖表结构设计、Flask 蓝图组织、MySQL 事务与索引调优、常见踩坑最后落到答辩演示和扩展方向。目标读者是正在做毕设的本科生以及想拿这个项目练手的 Python 开发者。2. 从表结构到 Flask 蓝图先把数据模型和项目骨架立住2.1 为什么用 Flask 而不是 Django 或 FastAPI很多人在选框架时会犹豫。Django 自带 admin 后台和 ORM上手快但耦合度高答辩时如果被问到“框架原理”容易答不深FastAPI 性能好、自带接口文档但国内毕设答辩组对它的熟悉度不如 Flask。Flask 介于两者之间轻量、灵活、生态成熟ORM 用 SQLAlchemy 单独引入路由和视图函数全在自己手里控制写出来的是“你自己的代码”而不是框架生成的代码。这也是为什么历届毕设里 Flask 项目占比一直不低。用 Flask 还有一个隐形红利答辩时老师大概率会追问“如果并发量上来会怎样”Flask 本身的同步阻塞模型可以成为你回答“性能瓶颈在哪、怎么优化”的引子而不是像 Django 那样把问题都藏在框架里。这比单纯“跑通功能”更值钱。2.2 二手交易平台需要哪几张表最小可行数据模型二手交易和普通商城不同它没有库存概念一件商品只有一个卖家被下单后就要锁定防止重复购买。围绕这条核心逻辑最小可行模型需要五张表用户表、商品表、订单表、收藏表、留言表。留言表可以理解为商品下的询价评论属于加分项但强烈建议保留因为“用户间互动”是二手平台与商城的核心差异点。用户表至少要有用户 ID、用户名、密码哈希、手机号、注册时间。密码不要存明文这是答辩时最容易被挑刺的点。商品表要含商品 ID、卖家 ID、标题、描述、图片路径、价格、状态在售/已锁定/已售出/下架、发布时间。这里的“状态”字段是整张表的灵魂所有交易逻辑都围绕它转。订单表记录买家 ID、商品 ID、金额、下单时间、状态待付款/待确认/已完成/已取消并在商品 ID 上加唯一约束——这是防止同一件商品被下两单的关键。收藏表和留言表相对独立可以在主流程跑通后再补。2.3 用 SQLAlchemy 定义模型代码片段与字段说明下面给出 SQLAlchemy 模型的核心写法。这里用的是 Flask-SQLAlchemy 扩展配置简单适合毕设项目。from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) username db.Column(db.String(50), uniqueTrue, nullableFalse) password_hash db.Column(db.String(128), nullableFalse) phone db.Column(db.String(20), nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow) class Product(db.Model): __tablename__ products id db.Column(db.Integer, primary_keyTrue) seller_id db.Column(db.Integer, db.ForeignKey(users.id), nullableFalse) title db.Column(db.String(100), nullableFalse) description db.Column(db.Text) price db.Column(db.Numeric(10, 2), nullableFalse) image_url db.Column(db.String(255)) status db.Column(db.String(10), defaulton_sale) # on_sale / locked / sold / off created_at db.Column(db.DateTime, defaultdatetime.utcnow) class Order(db.Model): __tablename__ orders id db.Column(db.Integer, primary_keyTrue) product_id db.Column(db.Integer, db.ForeignKey(products.id), uniqueTrue, nullableFalse) buyer_id db.Column(db.Integer, db.ForeignKey(users.id), nullableFalse) amount db.Column(db.Numeric(10, 2), nullableFalse) status db.Column(db.String(10), defaultpending) # pending / confirmed / done / cancelled created_at db.Column(db.DateTime, defaultdatetime.utcnow)这段代码有三个点需要重点说明。第一Product.status字段用字符串常量而不是布尔值因为商品生命周期有四个状态布尔表达不了。第二Order.product_id上的uniqueTrue是硬约束数据库层面直接拦截重复下单这不是靠业务代码“尽量判断”能替代的。第三db.Numeric(10,2)用于金额字段比 Float 精确不会出现0.1 0.2 0.30000000000000004这种浮点问题。2.4 按蓝图组织路由别把全部接口堆在一个文件里Flask 项目最常见的翻车方式是把所有路由写进一个app.py两千行之后自己都找不到函数。正确做法是按业务域拆分蓝图auth管注册登录、product管商品发布与列表、order管下单与状态流转、message管留言。每个蓝图一个目录内部自带models.py、views.py、__init__.py这样答辩时展示项目结构会很加分。from flask import Blueprint auth_bp Blueprint(auth, __name__, url_prefix/auth) product_bp Blueprint(product, __name__, url_prefix/product) order_bp Blueprint(order, __name__, url_prefix/order)在app.py入口统一注册from flask import Flask from extensions import db from blueprints.auth import auth_bp from blueprints.product import product_bp from blueprints.order import order_bp app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://root:passwordlocalhost/secondhand_db app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db.init_app(app) app.register_blueprint(auth_bp) app.register_blueprint(product_bp) app.register_blueprint(order_bp) if __name__ __main__: app.run(debugTrue, host127.0.0.1, port5000)注意这里有个坑SQLAlchemy 从 2.x 版本开始db.init_app(app)和db SQLAlchemy()分离是推荐写法不要在同一个文件里又建对象又 import 来回去。外层extensions.py里只放db SQLAlchemy()各蓝图里from extensions import db能有效避免循环导入。这个细节踩的人特别多后面避坑章节还会展开。3. 用 Flask 跑通用户认证与商品发布核心流程的代码实现3.1 注册登录密码哈希与 session 保持认证模块是每个毕设答辩老师必问的部分。这里的核心不是“能登录”而是“密码怎么存、登录态怎么保持、接口怎么防止越权”。密码存储用 Werkzeug 自带的generate_password_hash和check_password_hash不要自己写哈希算法更不要存明文。from werkzeug.security import generate_password_hash, check_password_hash auth_bp.route(/register, methods[POST]) def register(): data request.get_json() username data.get(username) password data.get(password) phone data.get(phone) if User.query.filter_by(usernameusername).first(): return jsonify({code: 1, msg: 用户名已存在}) user User( usernameusername, password_hashgenerate_password_hash(password), phonephone ) db.session.add(user) db.session.commit() return jsonify({code: 0, msg: 注册成功}) auth_bp.route(/login, methods[POST]) def login(): data request.get_json() user User.query.filter_by(usernamedata.get(username)).first() if user and check_password_hash(user.password_hash, data.get(password)): session[user_id] user.id session[username] user.username return jsonify({code: 0, msg: 登录成功}) return jsonify({code: 1, msg: 用户名或密码错误})这段代码里的session对象来自 Flask 自带机制默认用签名 Cookie 存数据不需要单独引入 Redis 或 JWT。毕设项目用 session 足够而且答辩时解释起来直接服务器把用户标识写进签名后的 Cookie浏览器下次请求自动带上。登录之后写一个login_required装饰器拦截未登录用户from functools import wraps def login_required(func): wraps(func) def wrapper(*args, **kwargs): user_id session.get(user_id) if not user_id: return jsonify({code: 401, msg: 请先登录}) return func(*args, **kwargs) return wrapper这里要强调一个安全细节session[user_id]取出后每次请求都要重新查库并校验用户是否存在不能只信 session 里的值。实践中曾遇到过用户被删除但 Cookie 未过期导致后续操作报 500 的情况。3.2 商品发布与列表文件上传与状态过滤商品发布模块涉及图片上传这是另一个高频踩坑点。Flask 默认不限制请求体大小但 Nginx 默认限制client_max_body_size本地开发时如果不配置上传大图会直接 413。代码层面用request.files接收文件保存到static/uploads/目录数据库只存相对路径。import os from werkzeug.utils import secure_filename UPLOAD_FOLDER os.path.join(os.getcwd(), static/uploads) ALLOWED_EXTENSIONS {png, jpg, jpeg, gif} product_bp.route(/publish, methods[POST]) login_required def publish(): title request.form.get(title) description request.form.get(description) price request.form.get(price) file request.files.get(image) if not title or not price: return jsonify({code: 1, msg: 标题和价格为必填项}) filename None if file: ext file.filename.rsplit(., 1)[-1].lower() if ext not in ALLOWED_EXTENSIONS: return jsonify({code: 1, msg: 不支持的图片格式}) # 用时间戳重命名文件防止重名覆盖 filename f{datetime.utcnow().strftime(%Y%m%d%H%M%S)}_{secure_filename(file.filename)} file.save(os.path.join(UPLOAD_FOLDER, filename)) product Product( seller_idsession[user_id], titletitle, descriptiondescription, priceprice, image_urlfilename ) db.session.add(product) db.session.commit() return jsonify({code: 0, msg: 发布成功, product_id: product.id})这段代码的关键参数有三个。第一secure_filename用于清除文件名里的特殊字符和路径分隔符防止目录穿越攻击——这是安全类答辩问题的标准答案。第二用时间戳把文件名重写成唯一值避免用户上传同名文件互相覆盖。第三ALLOWED_EXTENSIONS白名单校验在前端和后端各做一次前端控制体验、后端控制安全只依赖前端校验在开发时容易翻车。商品列表页要做的核心是分页和状态过滤。用Flask-SQLAlchemy自带的paginate方法一个参数搞定页数和每页条数product_bp.route(/list) def product_list(): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 12, typeint) status request.args.get(status, on_sale) query Product.query.filter_by(statusstatus).order_by(Product.created_at.desc()) pagination query.paginate(pagepage, per_pageper_page, error_outFalse) items [{ id: p.id, title: p.title, price: str(p.price), image_url: p.image_url, seller: p.seller.username } for p in pagination.items] return jsonify({ code: 0, items: items, total: pagination.total, pages: pagination.pages })注意per_page和page都用request.args.get的type参数强制转成 int这一步能挡住?pageabc这类非法请求。error_outFalse表示页码越界时返回空列表而不是抛 404实际体验更友好。3.3 下单与状态流转事务和锁的配合下单选了两个关键点乐观锁用状态条件更新实现事务用db.session保证原子性。先看下单代码from sqlalchemy import update order_bp.route(/create, methods[POST]) login_required def create_order(): product_id request.get_json().get(product_id) product Product.query.filter_by(idproduct_id).first() if not product or product.status ! on_sale: return jsonify({code: 1, msg: 商品不存在或已下架}) if product.seller_id session[user_id]: return jsonify({code: 1, msg: 不能购买自己的商品}) # 有条件更新status 从 on_sale 改成 locked数据库层面拦截并发 result db.session.execute( update(Product) .where(Product.id product_id, Product.status on_sale) .values(statuslocked) ) if result.rowcount 0: return jsonify({code: 1, msg: 手慢了商品已被下单}) order Order( product_idproduct_id, buyer_idsession[user_id], amountproduct.price, statuspending ) db.session.add(order) db.session.commit() return jsonify({code: 0, msg: 下单成功})这里的精妙之处在于用 SQLAlchemy 的update(Product).where(...)做条件更新。如果两个用户同时点击下单where Product.status on_sale保证只有一个请求能把状态改成locked另一个请求的rowcount为 0直接返回失败。这是数据库行级锁的天然能力比“先查再改”安全得多。后面避坑章节会详细讲 “先查后改” 为什么会在并发下翻车。订单生成后卖家需要确认发货或取消订单买家需要确认收货。状态流转路径是pending → confirmed → done或者pending → cancelled。每一步都必须在后端校验当前订单状态和操作者身份不能只靠前端隐藏按钮来控制。order_bp.route(/confirm, methods[POST]) login_required def confirm_order(): order_id request.get_json().get(order_id) order Order.query.filter_by(idorder_id).first() if not order: return jsonify({code: 1, msg: 订单不存在}) # 卖家确认限 pending 状态且操作者是卖家本人 product Product.query.get(order.product_id) if session[user_id] ! product.seller_id: return jsonify({code: 403, msg: 无权限操作}) if order.status ! pending: return jsonify({code: 1, msg: 当前状态不允许确认}) order.status confirmed db.session.commit() return jsonify({code: 0, msg: 确认成功})这段代码里没有炫技的部分但两个判断很关键第一个判断是“操作者是不是卖家”第二个是“当前状态能不能转”。缺了任何一个订单状态都会被玩坏。特别是权限判断毕设项目里最常见的笑柄就是买家能改卖家的订单状态。4. MySQL 侧的关键点事务、索引与订单状态流转4.1 连接配置与字符集UTF-8 不配好中文全部乱码Flask 连接 MySQL 时连接串里的参数直接决定很多诡异问题。最经典的是中文乱码——代码层面全是 UTF-8往 MySQL 里一存就变成问号。原因基本都是数据库字符集不是 UTF-8或者连接串里没指定。正确的连接串写法app.config[SQLALCHEMY_DATABASE_URI] ( mysqlpymysql://root:your_passwordlocalhost/secondhand_db ?charsetutf8mb4 )建库的时候也要明确指定字符集和排序规则CREATE DATABASE secondhand_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;utf8mb4和utf8的区别是utf8mb4能存四个字节的字符包括 emoji 和一些生僻汉字utf8只能存三个字节。二手交易平台的商品描述里用户很可能贴 emoji所以直接上utf8mb4不要省这个事。MySQL 8.0 默认utf8mb4,但 MySQL 5.7 及之前版本需要在建库时手动指定。4.2 索引设计哪些字段一定要加索引哪些加了反而慢索引设计是答辩时的加分项也是真实项目里必须面对的问题。二手平台这张商品表上最频繁的查询模式是“按状态过滤 按时间排序”对应 SQL 是SELECT * FROM products WHERE status on_sale ORDER BY created_at DESC LIMIT 12;复合索引(status, created_at)是这次查询的最佳选择。MySQL 在 InnoDB 引擎下联合索引最左前缀原则意味着 status 作为第一列、created_at 作为第二列能够同时满足过滤和排序不需要额外的文件排序操作。如果只加单列索引在 status 上ORDER BY created_at 还是需要单独排序。订单表上product_id已经由于唯一约束有了唯一索引这同时覆盖了“查商品对应订单”的场景。buyer_id上建议加普通索引因为用户查看“我买到的”是高频页面。不要给所有外键都加索引某些低频查询加索引后写入成本反而上升。对于一张数据量只有几千条的毕设表索引带来的性能提升其实感知不强但“索引设计思路”在答辩时是实打实的加分项。4.3 事务隔离级别为什么默认的 REPEATABLE READ 够用MySQL InnoDB 的默认事务隔离级别是 REPEATABLE READ在毕设项目里通常不用改。真正的风险点在于事务里面“先查后改”造成的并发问题。前面下单代码里的做法是直接用条件更新把状态从 on_sale 改成 locked而不是先 SELECT 再 UPDATE。如果改成先查再改两个用户同时读到 statuson_sale 后各自执行 UPDATE就会产生超卖。有时候会遇到 MySQL 在事务执行期间卡住比如某条 UPDATE 一直不返回。排查思路SELECT * FROM information_schema.innodb_trx查看当前活跃事务如果有长时间未提交的事务用KILL trx_mysql_thread_id处理。最常见的原因是代码里db.session.add()之后忘了db.session.commit()事务一直挂着锁一直不释放。5. 避坑排查Flask MySQL 项目里最常见的 5 个翻车现场5.1 pip install 后 import 报错或连接 MySQL 报 ModuleNotFoundError现象刚用pip install flask-mysql或类似包名装完import pymysql报找不到模块。原因现在很多教程混着讲 PyMySQL、MySQLdb 和 mysql-connector-python包名和 import 名不一样。还有另一种情况Python 环境变量指向的不是 pip 安装的那个环境特别是 Windows 上多个 Python 版本共存时。解决统一用 PyMySQLpip install pymysql然后确认python -c import pymysql; print(pymysql.__version__)能正常输出。如果是多 Python 环境在项目目录下建venv每次激活虚拟环境后再装依赖。关于连接驱动还要多说一句SQLAlchemy 连接 MySQL 需要额外装驱动pip install PyMySQL后在连接串里mysqlpymysql://就是告诉 SQLAlchemy 用 PyMySQL 这个驱动。不要只装 SQLAlchemy 就想直连 MySQL很多新手在这一步卡住半天。5.2 中文乱码或 MySQL 报 “Unknown character set: utf8mb4” 错现象数据库写入中文后读出来是乱码‘utf8mb4’ 字符集在某台机器上报错。原因字符集版本不匹配MySQL 5.5 以下不支持utf8mb4。现在的 MySQL 5.7 和 8.0 都支持如果你的版本偏低要么升级 MySQL要么退回utf8同时接受无法存 emoji 的代价。解决先在 MySQL 命令行里执行SHOW VARIABLES LIKE character_set_server;确认服务器端默认字符集再用上面提到的建库语句重建数据库。另外连接串里的?charsetutf8mb4一定要在 URL 的最后不要拼错位置否则连接不生效。5.3 db.session.commit() 后数据还在但重启服务数据没了现象本地开发时数据正常写入ctrlC 重启 Flask 后数据全部消失。原因极大概率你连的是 SQLite 而不是 MySQL——SQLAlchemy 在未配置SQLALCHEMY_DATABASE_URI或配置错误时会默认落到本地 SQLite 文件重启不会被清掉但如果你用的是:memory:内存数据库重启即清空。解决检查应用启动日志看实际连接的数据库路径确认app.config[SQLALCHEMY_DATABASE_URI]已经指向 MySQL用mysql -u root -p进命令行SELECT COUNT(*) FROM products;验证数据是否真正落到了 MySQL。开发时为了调试方便用 SQLite 没问题但答辩演示前必须切回 MySQL否则内存数据库一重启数据消失会当场翻车。5.4 下单成功但数据库里没有订单或商品状态没变现象前端提示下单成功但查看后台 MySQL 里既没有新订单商品状态也没变成锁定。原因commit 之前抛了异常但异常被上层 try/except 吞掉了或者代码里db.session.commit()返回了异常但你没有主动 rollbacksession 处于“脏”状态。解决在提交逻辑里显式处理异常并回滚try: db.session.add(order) db.session.commit() except Exception as e: db.session.rollback() print(fOrder commit failed: {e}) return jsonify({code: 1, msg: 下单失败请重试})这个写法不只是为了输错信息更是为了让 SQLAlchemy 的 session 对象从异常状态中恢复过来。如果不做 rollback后续任何数据库操作都可能延续这个脏 session 的状态,报出莫名其妙的错误。5.5 商品图片上传后页面显示不出来现象图片成功上传到了static/uploads但页面 img 标签 404。原因模板里用了绝对路径/static/uploads/xxx.jpg但 Flask 应用没有配置static_folder或者上传目录不在静态目录内。另一个常见原因是文件权限Linux 服务器上 uwsgi 或 nginx 用户没有读取该目录的权限。解决在创建 Flask 应用时显式指定静态目录和 URL 前缀app Flask(__name__, static_folderstatic, static_url_path/static)同时在保存上传文件后前端展示时拼接 URLimage_url url_for(static, filenamefuploads/{filename})用url_for生成路径而不是手写/static/能避免因为static_url_path被修改导致路径对不上。6. 从演示到答辩扩展方向、验证方法与演示技巧6.1 并发压测与事务边界验证毕设答辩最怕的不是功能 bug而是演示到一半崩了。建议在答辩前至少做两轮验证。第一轮是 PayPal 式的事务测试开两个浏览器窗口登录不同账号同时点击同一件商品的“立即购买”看是不是只有一个能成功下单另一个收到“手慢了”的提示。代码层面体现为条件更新的rowcount是否为 0。这个测试可以在答辩现场演示效果非常好因为大部分同学做的项目在这个场景下都会翻车。第二轮是数据一致性验证交易完成后确认买家、卖家、商品、订单四张表的状态一致没有出现“买家已付款但卖家看不到订单”的情况。6.2 可选扩展留言功能与数据可视化如果想让项目在答辩中更有分量建议在原有基础上加两个独立模块。第一个是物品留言询价功能商品详情页展示留言列表支持买家留言、卖家回复涉及两张表的联查和用户身份校验逻辑独立、工作量可控。第二个是简单的数据可视化页面用 ECharts 绘制“一周内成交量走势”“热门商品分类分布”后端提供聚合统计接口SELECT DATE(created_at) AS d, COUNT(1) AS cnt FROM orders WHERE created_at NOW() - INTERVAL 7 DAY GROUP BY DATE(created_at);如果担心 MySQL 8.0 的NOW() - INTERVAL语法不兼容可以用DATE_SUB(NOW(), INTERVAL 7 DAY)两种写法在 5.7/8.0 都能跑通。扩展建议按以下优先级排序留言功能 数据可视化 Redis 缓存 Docker 部署。留言功能直接强化“二手平台”的核心定位数据可视化适合论文里放图Redis 缓存和 Docker 部署虽然技术含金量高但毕设答辩现场展示效果有限投入产出比不高。6.3 演示时需要避开的一个习惯不要用 debugTrue 展示项目Flask 的debugTrue会在浏览器端暴露 Werkzeug 调试器任何页面异常都会显示交互式堆栈界面里面包含代码路径和环境变量更有一些安全工具可以远程执行代码。答辩演示时开着调试模式万一某个操作触发了异常展示的就是一堆英文报错和变量名给老师的观感很差。演示前把启动方式改成生产模式app.run(host127.0.0.1, port5000, debugFalse)同时把 MySQL 连接串从测试库切到演示库并且演示前先重新初始化几件测试商品保证页面不是空的。我个人的习惯是答辩前一周用一台不连外网的机器做一次完整回归从注册新账号开始发布两件商品再切换两个账号互相交易走完整个闭环。测试过程中截图存档万一现场网络出问题还能用截图兜底。这个方法救过我一次——现场投影仪的浏览器版本太老前端页面样式全乱了最后直接拿截图讲完整个交易流程。希望这个技巧也能帮到你。本文还有配套的精品资源点击获取
返回列表