
简介这是一份基于微信小程序的校园外卖系统数据库课程设计完整源码包面向计算机相关专业学生、数据库课程设计者以及初涉小程序与后端交互开发的开发者。系统围绕学生客户、商家、学生配送员三类角色构建完整业务闭环包括商品浏览、购物下单、订单状态跟踪、评分评价、地址与个人资料维护、商家商品管理、派单接单及配送等环节相关数据库设计脚本和初始化数据可直接用于课程设计与答辩演示。压缩包约2.37MB共123个文件主要类型涵盖png/jpg界面素材、js与json逻辑配置、wxml与wxss页面架构、sql初始化脚本、py辅助脚本及md说明文档覆盖前端展示、后端逻辑、数据持久化与项目说明等模块。目前已有280人学习下载适合快速复用为数据库课程设计参考也可作为理解外卖场景中数据表关系、订单状态机与前后端交互的入门样例。1. 校园外卖系统当数据库课设先想清楚值不值再看怎么搭很多计算机专业的同学看到「基于JavaScriptpython的微信小程序校园外卖系统数据库课程设计.zip」这个标题第一反应是找代码、跑起来、截图交差。但作为一门数据库课程设计真正值钱的不是小程序页面长什么样而是那张 ER 图、表结构设计和订单状态流转逻辑。校园外卖业务恰好把用户、商家、菜品、购物车、订单、订单明细这几类典型关系全占齐了非常适合拿来讲清楚「数据库增删改查之外设计本身为什么重要」。你可能是第一次接触微信小程序Python 也刚搭好环境没关系这条路线就是为了让你在有限时间内把数据库、后端接口和小程序页面串成一条完整链路答辩时每一步都能讲明白。适合谁呢主要三类人一是在选课设题目、想评估这个题工作量的学生二是拿到了某个压缩包但代码跑不起来、需要知道系统内部结构和改哪里的人三是想快速搭一个前后端分离演示项目、又不打算投入太多时间的入门开发者。后面所有内容都围绕一个目标把「数据库课程设计」这六个字落到实处而不是做一个只能点按钮的玩具。2. 数据库是课设的灵魂从 ER 图到建表 SQL 的完整推导做校园外卖系统最忌讳一上来就写接口结果表结构互相矛盾订单表里塞了菜品名、商家电话放进用户表、订单总金额不知道是前端传的还是后端算的。数据库课程设计评分的重点恰恰看表结构是否合理所以先把静态模型推翻一遍再谈代码。2.1 六张核心表用户、商家、菜品、购物车、订单、订单明细缺一不可先理清业务边界。校园外卖里的角色分三类学生买家、商家、平台管理员。很多课设把用户和商家合并成一张 user 表、用 role 字段区分写起来简单但答辩时容易被追问「用户的收货地址和商家的营业时间怎么共存」。更稳妥的常见做法是分开建表各自管各自的信息用户表存 openid、昵称、手机号、余额商家表存店名、描述、营业时间。菜品表和订单明细表的关系是重点。菜品表存的是「当前菜单」商家一旦改价或下架菜品历史订单里的价格不应该跟着变。所以订单明细表里必须冗余 dish_name 和 price 快照而不是只存 dish_id 让前端去联表查当前价。这是整个课设最值钱的设计点规范化与字段冗余怎么权衡。你答辩时主动讲「订单明细保留历史快照是为了防止菜品改价影响历史订单」老师就知道你理解为什么不能一味追求第三范式。还要记得在订单主表里放 merchant_id。虽然订单明细能反查到商家但订单列表页按商家维度统计的场景非常多主表冗余一个商家 ID 能少做一次 JOIN。下面是按 MySQL 8 写的建表 SQL默认你已经启动了 MySQL 服务CREATE DATABASE IF NOT EXISTS campus_delivery DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE campus_delivery; -- 用户表买家 CREATE TABLE user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL UNIQUE COMMENT 微信小程序 openid, nickname VARCHAR(50) DEFAULT , avatar_url VARCHAR(255) DEFAULT , phone VARCHAR(20) DEFAULT , balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 模拟余额用于演示支付, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 商家表 CREATE TABLE merchant ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, description VARCHAR(255) DEFAULT , image_url VARCHAR(255) DEFAULT , open_time VARCHAR(20) DEFAULT 09:00-21:00, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 菜品表 CREATE TABLE dish ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, merchant_id BIGINT UNSIGNED NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, image_url VARCHAR(255) DEFAULT , status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_merchant (merchant_id, status) ) ENGINEInnoDB; -- 购物车项 CREATE TABLE cart_item ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, dish_id BIGINT UNSIGNED NOT NULL, quantity INT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_dish (user_id, dish_id) ) ENGINEInnoDB; -- 订单主表 CREATE TABLE orders ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT UNSIGNED NOT NULL, merchant_id BIGINT UNSIGNED NOT NULL, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2配送中 3已完成 -1已取消, remark VARCHAR(255) DEFAULT , address VARCHAR(255) NOT NULL DEFAULT COMMENT 收货地址快照, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME NULL, KEY idx_user_time (user_id, create_time), KEY idx_status (status) ) ENGINEInnoDB; -- 订单明细表 CREATE TABLE order_item ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id BIGINT UNSIGNED NOT NULL, dish_id BIGINT UNSIGNED NOT NULL, dish_name VARCHAR(100) NOT NULL COMMENT 菜品名快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时价格快照, quantity INT NOT NULL DEFAULT 1, KEY idx_order (order_id) ) ENGINEInnoDB;建表逻辑里有几个参数要专门解释。第一所有金额字段都用 DECIMAL(10,2)不碰 FLOAT浮点数的二进制表示会让 13.8 变成 13.8000000001后面避坑章会细说。第二cart_item 上加了 UNIQUE KEY uk_user_dish (user_id, dish_id)同一用户对同一道菜只保留一条记录重复加购变成 UPDATE quantity quantity 1避免先查后插的并发重复。第三orders 表用 order_no 做 UNIQUE不把自增 id 直接暴露给前端防止别人通过订单 id 枚举你的业务量。2.2 订单状态机与金额冗余课程设计拿高分的两个设计细节订单状态是这个系统里最容易画错的图。很多网上代码里 status 字段 0 和 1 来回切没有支付时间、没有取消逻辑答辩时老师问「用户下单后一直不付钱这单什么时候算完」就答不上来。常见做法是定义一条清晰流转链待支付(0) → 已支付(1) → 配送中(2) → 已完成(3)用 -1 表示已取消。为什么用负数表示取消因为WHERE status 0就能筛出所有有效订单正数位留给未来可能的退款、售后状态。状态机还要配套时间字段。pay_time 记录支付动作发生的时间配合 create_time 可以算「超时未支付订单」。课程设计里可以在 Python 后端写一个简单的循环线程定时任务扫描创建超过 15 分钟仍为 0 的订单改成 -1 并回补库存。这段代码量不大但能体现你对业务闭环的思考属于性价比很高的加分项。金额冗余同样值得展开orders.total_amount 和 order_item 的 SUM(price * quantity) 是重复存储违反第三范式。但订单列表页几乎不关心明细每次都现算 SUM 在小数据量下没感觉数据量大了就是灾难。所以保留冗余字段用「下单事务里由后端从明细汇总写入」保证一致性。答辩时主动说出这是空间换时间老师不会觉得你不懂范式反而认为你理解范式的适用边界。-- 查看某个订单的金额是否与明细一致这是答辩现场可以跑的验证语句 SELECT o.id, o.total_amount AS 订单主表金额, SUM(oi.price * oi.quantity) AS 明细实际汇总 FROM orders o LEFT JOIN order_item oi ON oi.order_id o.id GROUP BY o.id, o.total_amount HAVING 订单主表金额 明细实际汇总;这条 SQL 查出来的任何一行都代表数据不一致课程设计里你的演示数据应该返回空结果。这个验证思路可以写进实验报告体现你做过一致性校验。2.3 外键与索引的现实取舍逻辑外键更适合演示型课设上面的建表 SQL 完全没写 FOREIGN KEY 约束会让学生刚学完数据库原理的同学不适应。解释一下物理外键在 InnoDB 里确实保证引用完整性但课设演示时经常要手工插测试数据物理外键会强制你按顺序插入删数据还会被父表约束绊住。更重要的是很多公司生产库刻意不用物理外键靠应用层保证一致性理由就是减少锁竞争、方便分库分表。这里用逻辑外键完全站得住脚但要在设计文档里明确写出来别让老师以为你忘了建。索引则是另一回事。课设数据量小索引效果看不出来但建对索引能在「设计合理性」上多拿分。常见几个位置dish 表的 (merchant_id, status) 联合索引覆盖「按商家查上架菜品」orders 表的 (user_id, create_time) 联合索引覆盖「我的订单按时间倒序」order_item 的 order_id 单列索引查明细避免全表扫描。这些索引都对应真实查询路径不是为了凑数。表结构设计完你已经完成这个课设最核心的部分ER 图、关系模式、建表 SQL、状态机说明。接下来要做的就是让 JavaScript 和 Python 把这张静态模型变成能点的页面。3. JavaScript 与 Python 分工小程序前端与后端接口的最小闭环表结构定了下面进入代码。这个题目的技术栈是 JavaScript 写微信小程序端、Python 写后端服务。小程序页面逻辑用的是 JavaScript但 JavaScript 不只写前端Python 这边用 Flask 写 API 是课设最常见的组合。3.1 技术选型课设后端用 Flask 还是 Django前端为什么不用 uniapp我一般建议课程设计优先选 Flask。理由很实际一个 app.py 能写完所有接口数据库用 PyMySQL 写原生 SQL 很容易讲清楚调试时打印日志也直观。Django 的优势是自带 ORM、Admin 后台和用户体系适合老师强制要求「必须有后台管理页面」的场合但目录结构复杂中间件和版本兼容问题容易让新手卡半天。对比一下边界如果课设要求明确写了要后台管理选 Django 用它的 Admin 直接管商家和菜品如果只要求小程序下单流程能跑通选 Flask开发速度快一倍。也有同学问能不能用 uniapp 开发微信小程序答案是可以但 uniapp 会引入 Vue 语法和编译链路标题里写的是「微信小程序」用原生框架最不容易出错也最贴合 JavaScript 这个关键词。环境准备只要 Python 3.8 以上版本安装 flask 和 pymysql 两个包就能开始。目录结构大致是这样campus-delivery/ ├── app.py # Flask 入口所有接口 ├── db.py # 数据库连接与查询封装 ├── requirements.txt # 依赖清单 ├── miniprogram/ # 微信小程序前端目录 │ ├── app.js │ ├── app.json │ └── pages/ │ ├── index/ # 商家列表 │ ├── menu/ # 菜品列表 │ ├── cart/ # 购物车 │ ├── order-confirm/ # 订单确认与支付 │ └── order-list/ # 我的订单3.2 登录闭环wx.login 拿到 codePython 后端返回 openid 和 user_id小程序没有传统的 session 机制登录靠 wx.login 获取临时 code后端拿 code 去微信接口换 openid。课设阶段可以简化但闭环要完整避免答辩被问「你怎么知道当前用户是谁」。先看小程序端// miniprogram/pages/login/login.js const app getApp() Page({ onLoad() { this.login() }, login() { // wx.login 拿临时 code有效期五分钟只能用一次 wx.login({ success: ({ code }) { wx.request({ url: http://127.0.0.1:5000/api/login, method: POST, data: { code }, success: (res) { // 重点同时存 token、openid、user_id后续请求都带上 wx.setStorageSync(token, res.data.token) wx.setStorageSync(openid, res.data.openid) wx.setStorageSync(user_id, res.data.user_id) wx.switchTab({ url: /pages/index/index }) }, fail: (err) { console.error(登录接口调用失败, err) } }) } }) } })这段代码的关键参数是 data.code它是一次性的短期身份凭证。后端拿 code 调微信的 jscode2session 接口换 openid。课设期间如果没注册小程序 AppID可以留一个调试开关临时写死 openid但答辩前一定要记得切回来。后端对应实现# app.py from flask import Flask, request, jsonify import pymysql, uuid, hashlib app Flask(__name__) # 课设调试开关True 时不依赖真实微信 AppID DEBUG_OPENID oX7YzTest001 def get_db(): return pymysql.connect( host127.0.0.1, userroot, passwordyourpassword, databasecampus_delivery, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) app.route(/api/login, methods[POST]) def login(): code request.json.get(code, ) if DEBUG_OPENID: openid DEBUG_OPENID else: # 真实场景用 code appid secret 调微信接口换 openid # openid get_openid_from_wechat(code) pass token hashlib.md5(uuid.uuid4().hex.encode()).hexdigest() sql INSERT INTO user (openid, nickname) VALUES (%s, %s) ON DUPLICATE KEY UPDATE nickname VALUES(nickname) with get_db() as conn: with conn.cursor() as cur: cur.execute(sql, (openid, 小程序用户)) conn.commit() # 查出真正的自增 id前端后续下单都靠它 with conn.cursor() as cur: cur.execute(SELECT id FROM user WHERE openid %s, (openid,)) user_id cur.fetchone()[id] return jsonify({openid: openid, token: token, user_id: user_id})这里有几个容易忽略的点。第一pymysql.connect 里的 charset 参数必须是 utf8mb4不是 utf8否则 emoji 字符会报错。第二INSERT ... ON DUPLICATE KEY UPDATE 依赖 user.openid 唯一索引实现「首次登录插入、再登录更新」的幂等效果比先 SELECT 再 INSERT 更简洁。第三登录接口一定要返回 user_id这是前端后续所有业务操作的身份主键。如果你只把 openid 存前端后端的 JOIN 会非常别扭——很多网上代码就是在这里埋的雷。3.3 商家列表与菜品列表把数据库的增删改查变成 RESTful 接口登录做完接下来做一个完整的接口闭环。以「按商家查菜品」为例前端需要一个商家列表页点进商家后展示上架菜品。后端接口如下app.route(/api/merchants) def merchants(): with get_db() as conn: with conn.cursor() as cur: cur.execute( SELECT id, name, description, image_url, open_time FROM merchant ORDER BY id ) rows cur.fetchall() return jsonify({code: 0, data: rows}) app.route(/api/dishes) def dishes(): merchant_id request.args.get(merchant_id, typeint) if not merchant_id: return jsonify({code: 1, msg: missing merchant_id}), 400 sql SELECT id, name, price, stock, image_url FROM dish WHERE merchant_id %s AND status 1 with get_db() as conn: with conn.cursor() as cur: cur.execute(sql, (merchant_id,)) rows cur.fetchall() return jsonify({code: 0, data: rows})小程序端用 wx.request 调接口注意每个页面要处理 loading 和错误提示// miniprogram/pages/menu/menu.js Page({ data: { dishes: [], loading: false }, onLoad(options) { this.merchantId Number(options.merchant_id) this.loadDishes() }, loadDishes() { this.setData({ loading: true }) wx.request({ url: http://127.0.0.1:5000/api/dishes, data: { merchant_id: this.merchantId }, success: (res) { if (res.data.code 0) { this.setData({ dishes: res.data.data }) } else { wx.showToast({ title: res.data.msg || 加载失败, icon: none }) } }, fail: (err) { wx.showToast({ title: 网络错误, icon: none }) }, complete: () this.setData({ loading: false }) }) } })到这里你已经有一条完整链路数据库表 → Python 接口 → 小程序界面。这也是课程设计评审最看重的「数据流」讲法。接下来把这条链路用在最核心的订单流程上让数据真正流转起来。4. 用订单流程串起前后端购物车、下单、支付与订单列表的联调实操系统能不能通过验收看的是点菜、下单、支付、查订单这条主路径能不能通。这一章按业务顺序走一遍每段都对应真实数据库操作。4.1 购物车存本地还是存后端课程设计建议存数据库购物车有两条路线。第一条是纯前端方案把 { dishId, quantity } 存在 wx.setStorageSync 里下单时随请求传后端。优点是代码量小缺点是换设备丢数据、后端拿不到用户购物车状态答辩时数据流讲起来单薄。第二条是后端方案存进 cart_item 表增删改查都在后端完成。课设建议走第二条。理由很直白题目里带「数据库」三个字要尽量多展示数据库操作cart_item 表让老师看到独立的购物车实体和关联查询。前端本地缓存可以做角标展示的辅助但权威数据以后端为准。加购接口的 SQL 用唯一约束配合原子更新app.route(/api/cart/add, methods[POST]) def add_cart(): data request.json user_id data.get(user_id) dish_id data.get(dish_id) quantity int(data.get(quantity, 1)) sql INSERT INTO cart_item (user_id, dish_id, quantity) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE quantity quantity VALUES(quantity) with get_db() as conn: with conn.cursor() as cur: cur.execute(sql, (user_id, dish_id, quantity)) conn.commit() return jsonify({code: 0})这个写法的巧妙之处在于不用先查购物车里有没这道菜直接 INSERT 撞唯一键后走 UPDATE天然支持并发。前端每次加购调接口成功后同步本地数量。注意加购前要查菜品状态下架菜不能加——前端校验是体验后端校验才是安全两处都要有。4.2 下单事务为什么创建订单必须把多个 SQL 包在一个事务里购物车有三样东西用户点结算后端要做的事按顺序是创建订单主记录、把购物车明细搬进订单明细、计算总金额、扣减菜品库存、清空购物车。这五件事任何一件失败都不该留下半个订单所以必须包在数据库事务里要么全成功要么全回滚。用 PyMySQL 手动控制事务的写法from datetime import datetime app.route(/api/orders, methods[POST]) def create_order(): data request.json user_id data.get(user_id) merchant_id data.get(merchant_id) address data.get(address, ) remark data.get(remark, ) conn get_db() try: with conn.cursor() as cur: # 1. 创建订单主表先写状态为 0待支付 order_no CF datetime.now().strftime(%Y%m%d%H%M%S) str(user_id) cur.execute( INSERT INTO orders (order_no, user_id, merchant_id, address, remark, status) VALUES (%s, %s, %s, %s, %s, 0), (order_no, user_id, merchant_id, address, remark) ) order_id cur.lastrowid # 2. 明细快照从购物车 JOIN 菜品表搬入保留菜名和价格 cur.execute( INSERT INTO order_item (order_id, dish_id, dish_name, price, quantity) SELECT %s, d.id, d.name, d.price, ci.quantity FROM cart_item ci JOIN dish d ON d.id ci.dish_id WHERE ci.user_id %s AND d.merchant_id %s , (order_id, user_id, merchant_id)) # 3. 总金额从明细汇总不信任前端传值 cur.execute( UPDATE orders SET total_amount ( SELECT SUM(price * quantity) FROM order_item WHERE order_id %s ) WHERE id %s , (order_id, order_id)) # 4. 扣库存条件里带 stock qty原子防超卖 cur.execute( UPDATE dish d JOIN ( SELECT dish_id, SUM(quantity) AS qty FROM cart_item WHERE user_id %s GROUP BY dish_id ) ci ON ci.dish_id d.id SET d.stock d.stock - ci.qty WHERE d.stock ci.qty , (user_id,)) if cur.rowcount 0: raise RuntimeError(库存不足事务回滚) # 5. 清空购物车 cur.execute(DELETE FROM cart_item WHERE user_id %s, (user_id,)) conn.commit() return jsonify({code: 0, order_id: order_id, order_no: order_no}) except Exception as e: conn.rollback() return jsonify({code: 1, msg: str(e)}), 500 finally: conn.close()这段代码是整篇课设的业务核心要能逐行解释。第一步 cur.lastrowid 拿到刚插入订单的自增 id供后续明细使用这是事务内才能可靠拿到的值。第二步用 INSERT ... SELECT 把购物车和菜品表 JOIN 后一次性搬入明细同时完成菜品名和价格快照的复制省去 Python 循环。第三步重新算一遍明细求总价总金额不由前端传后端从明细算出防止用户篡改价格参数这是答辩必问点。第四步扣库存用 UPDATE ... JOIN 配合 WHERE d.stock ci.qty一条语句完成「判断库存够不够」和「扣减」两个动作rowcount 为 0 说明有菜不够卖抛异常触发回滚。第五步清空购物车放在最后保证用户重试时购物车还在。这里最重要的业务参数是事务边界commit 只在全部 SQL 成功后执行任何一步异常都 rollback。你可以故意在扣库存后抛一个异常演示回滚效果订单表、订单明细表都不会留下半截数据。这是答辩现场效果很好的实验。4.3 模拟支付与状态流转不接微信支付也能讲清楚支付流程真实小程序接入微信支付要商户号、证书、回调验签课设阶段没必要。常见做法是两个模拟通道余额支付和模拟微信支付。余额支付演示 UPDATE user SET balance balance - ? 的原子扣减模拟微信支付弹一个确认框确认后调后端把订单状态从 0 改成 1记录 pay_time。状态流转代码与真实系统一致只是不产生真实扣款。支付接口实现app.route(/api/orders/int:order_id/pay, methods[POST]) def pay_order(order_id): data request.json user_id data.get(user_id) pay_method data.get(pay_method, mock) conn get_db() try: with conn.cursor() as cur: # 条件里带 status 0 防止重复支付rowcount 为 0 说明状态不对 cur.execute( UPDATE orders SET status 1, pay_time NOW() WHERE id %s AND user_id %s AND status 0, (order_id, user_id) ) if cur.rowcount 0: raise RuntimeError(订单不存在或已支付) if pay_method balance: # 先查余额够不够再扣款 cur.execute( SELECT balance FROM user WHERE id %s, (user_id,) ) row cur.fetchone() cur.execute( SELECT total_amount FROM orders WHERE id %s, (order_id,) ) total cur.fetchone()[total_amount] if not row or row[balance] total: raise RuntimeError(余额不足) cur.execute( UPDATE user SET balance balance - %s WHERE id %s, (total, user_id) ) conn.commit() return jsonify({code: 0, status: 1}) except Exception as e: conn.rollback() return jsonify({code: 1, msg: str(e)}), 500 finally: conn.close()这里用「先 SELECT 再 UPDATE」而不是一条 UPDATE 带条件可以减小锁的粒度也更符合 Python 代码的阅读习惯。核心思想是支付动作必须同时校验「订单状态」和「余额充足」而且要在一个事务里完成。余额扣减用balance balance - %s而不是先读出余额再算好写回避免并发下把别人刚充值进去的钱覆盖掉。4.4 订单确认页交互地址填写与支付方式的单选框实现后端联调通了前端交互也要跟上。下单确认页一般有三个输入收货地址、备注、支付方式。支付方式用微信小程序的 radio-group 实现正好是「微信小程序单选框」的典型场景。!-- miniprogram/pages/order-confirm/order-confirm.wxml -- view classconfirm-card view classcard-title收货地址/view input classaddress-input placeholder请输入宿舍楼栋和门牌号 bindinputonAddressInput value{{address}}/ /view view classconfirm-card view classcard-title支付方式/view radio-group classpay-group bindchangeonPayMethodChange label classpay-option radio valuebalance checked{{payMethod balance}} color#07c160/ text余额支付当前余额 ¥{{balance}}/text /label label classpay-option radio valuemock checked{{payMethod mock}} color#07c160/ text模拟微信支付/text /label /radio-group /view button typeprimary bindtapsubmitOrder loading{{submitting}} 提交订单 /button// miniprogram/pages/order-confirm/order-confirm.js Page({ data: { payMethod: balance, balance: 0, address: , remark: , submitting: false, merchantId: 0 }, onLoad(options) { this.setData({ merchantId: Number(options.merchant_id) }) this.loadBalance() }, onAddressInput(e) { this.setData({ address: e.detail.value }) }, onPayMethodChange(e) { this.setData({ payMethod: e.detail.value }) }, loadBalance() { const userId wx.getStorageSync(user_id) wx.request({ url: http://127.0.0.1:5000/api/user/balance, data: { user_id: userId }, success: (res) { this.setData({ balance: res.data.balance }) } }) }, submitOrder() { const { address, merchantId, payMethod } this.data if (!address) { wx.showToast({ title: 请填写地址, icon: none }) return } this.setData({ submitting: true }) wx.request({ url: http://127.0.0.1:5000/api/orders, method: POST, data: { user_id: wx.getStorageSync(user_id), merchant_id: merchantId, address, remark: this.data.remark }, success: (res) { if (res.data.code 0) { this.payOrder(res.data.order_id, payMethod) } else { wx.showToast({ title: res.data.msg, icon: none }) } }, complete: () this.setData({ submitting: false }) }) }, payOrder(orderId, payMethod) { wx.request({ url: http://127.0.0.1:5000/api/orders/${orderId}/pay, method: POST, data: { user_id: wx.getStorageSync(user_id), pay_method: payMethod }, success: (res) { if (res.data.code 0) { wx.redirectTo({ url: /pages/order-list/order-list }) } else { wx.showToast({ title: res.data.msg, icon: none }) } } }) } })注意 submitOrder 里传给后端的 user_id 是登录时存好的自增 id不是 openid。这个问题前面强调过很多教程在这里翻车前端把 openid 当 user_id 传后端 JOIN 全断。登录接口多返回一个 user_id 就是为这一步准备的。单选按钮的 checked 绑定 payMethod 判断切换时通过 e.detail.value 更新数据流非常清晰这段代码就是「微信小程序项目实例」里下单页的标准写法。5. 校园外卖系统课设避坑指南5 个让学员当场翻车的典型故障从开发到答辩你大概率会撞上下面五个问题。每一条按「现象 → 原因 → 解决」写清楚照着排查比自己瞎试快得多。5.1 现象小程序 request 报错 url not in domain list页面白屏原因微信开发者工具默认校验合法域名你用 http://127.0.0.1:5000 访问本地后端接口域名不在小程序后台配置的 request 合法域名列表里请求被直接拦截。这个限制小程序发布后也存在只是课设阶段可以绕过。解决开发者工具右上角「详情」→「本地设置」→ 勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」。还要注意真机预览时同样要勾选「不校验域名」才能跑通。这是我每次带课设都会先提醒的一步很多人连后端接口调试都卡在这里。5.2 现象user 表出现多条相同 openid 记录用户身份串号原因最常见的是你中途换了小程序 AppID。wx.login 换取的 openid 和 AppID 绑定换一个测试号就换来不同 openid或者后端 DEBUG_OPENID 开关没关所有人都登录成同一个账号。如果 user 表里 openid 加了唯一索引但还出现重复那一定是建表脚本和代码用的不是同一个库。解决课设期间统一用一个测试号 AppID不要来回切后端调试开关答辩前必须关掉或改成从配置读取。如果已经插了脏数据先清掉 user 表再重新登录。这条故障的隐蔽性在于“表面能登录用户身份全乱了”。5.3 现象订单金额显示 13.8000000001对不上账原因数据库金额字段建成了 FLOAT 或 DOUBLE浮点数在二进制中无法精确表示 0.1、0.2 这类小数或者 Python 端用 float 直接算金额后写库。MySQL 的 FLOAT 是近似存储DECIMAL 才是精确定点数这是教科书必考但课设最容易忽略的点。解决把金额字段全部改成 DECIMAL(10,2)同时改掉建表脚本Python 端金额运算用 decimal.Decimal 替代 float。改完重新插入测试数据再算一遍总价确认显示两位小数。这个坑是「数据库课程设计」里最值得写进实验报告的一条它是知识型错误不是代码 bug老师一看就知道你懂不懂浮点数原理。5.4 现象并发下单同一道菜库存从 1 变成 -1原因典型的并发超卖。下单逻辑里先 SELECT stock 判断是否有库存再 UPDATE dish SET stock stock - 1这两个操作之间不是原子的。线程 A 查到库存为 1线程 B 也查到 1A 扣成 0B 再扣成 -1。这是网上流传代码里最高发的翻车点。解决用第四章里的原子扣减语句UPDATE dish SET stock stock - 1 WHERE id ? AND stock 0让数据库自己保证判断和扣减在同一行执行整个下单流程包在事务里。想现场演示这个问题可以写一个 Python 多线程脚本同时发 20 个请求你会看到没有原子扣减时库存出现负数加上条件后最多只扣到 0。这个验证方法下一章还会用到。5.5 现象部署到服务器后中文变问号菜品名和地址全乱码原因三处字符集不一致。MySQL 建库没指定 utf8mb4或者连接串少了 charset或者 Flask 返回 JSON 时把中文转成 \uXXXX。最典型的场景是本地 SQLite 正常、换 MySQL 就乱码本质是库、表、连接三个层级里某一个用了默认 latin1。解决建库建表统一用 utf8mb4连接串补上 charsetutf8mb4Flask 端设置app.config[JSON_AS_ASCII] False避免中文被转义。改完重启后端并重新插数据乱码问题一次解决。这个坑要写在安装部署文档里因为换一台机器就可能踩中。6. 让课设从「能跑」到「能讲」索引验证、并发脚本与答辩收尾如果你还有两天时间别急着加新功能先做三件性价比最高的事。第一用 EXPLAIN 验证索引是否生效。打开 MySQL 命令行执行EXPLAIN SELECT * FROM orders WHERE user_id 101 ORDER BY create_time DESC;观察 type 列是 ref 还是 ALL。如果是 ALL说明联合索引没被命中检查是不是把 user_id 和 create_time 拆成了两个单列索引——联合索引遵循最左前缀原则user_id 必须在前面。这个实验跑完设计文档里就能写「经 EXPLAIN 验证订单列表查询走 idx_user_time 索引」比任何文字都有说服力。第二用并发脚本验证下单事务。写一个不超过五十行的 Python 脚本用 threading 加 requests 模拟二十个用户同时对同一道菜下单观察两个结果库存最终是否为 0、orders 表里有没有总金额对不上的脏数据。如果库存出现负数反推事务没写对如果一切正常把脚本输出截图放进报告这是「系统经过并发验证」的铁证也直接呼应了前面防超卖的原子扣减。第三把数据库连接改成连接池。现在每调一次接口就 get_db() 建一条连接MySQL 默认最大连接数 151演示时多开几个页面就可能报 too many connections。用 dbutils.PooledDB 复用连接参数建议 pool_size 设 5、max_connections 设 20演示规模足够。这一行改动花十分钟却能让系统稳定性评价高一个档次老师基本都会追这个问题。我每次带数据库课设都会先让学生把 ER 图画完、把订单状态机讲明白再让他们碰代码。这个顺序救过不少翻车现场——代码可以重写表结构错了等于地基错了。按这条路径做完你手里这套校园外卖系统就不仅是能跑的压缩包而是一份每个设计决策都能解释清楚的作品。希望帮到你。本文还有配套的精品资源点击获取