ARTICLE DETAIL

资讯详情

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

微信小程序校园外卖系统:三端订单状态流转的数据库设计实战

微信小程序校园外卖系统:三端订单状态流转的数据库设计实战 简介一份面向数据库课程设计与微信小程序开发的完整校园外卖系统项目围绕学生顾客、商家、配送员三类角色实现闭环业务学生可浏览在售商品、下单、跟踪订单状态、完成后评分评价并维护地址头像电话等个人资料商家可维护商品、接单、派单给兼职配送员并查看经营统计配送员可查看派单信息并完成配送。压缩包共123个文件、约2.37MB包含js、wxml、wxss等小程序前端页面与逻辑代码json配置文件sql数据库脚本py辅助脚本以及png/jpg界面截图目录结构清晰能直接对照学习前后端交互与数据库表设计。通过这份资源读者可理清小程序端与数据表之间的对应关系掌握订单状态机、权限角色拆分和数据库设计方法sql脚本和界面截图尤其适合课程设计答辩前快速梳理功能与数据流。已有280人学习下载适合需要完成数据库课程设计、小型电商系统开发练习或小程序前后端联调的开发者参考。1. 微信小程序校园外卖系统数据库课设也能把三端状态流转做成亮点微信小程序校园外卖系统听起来是普通课程设计但真正让人卡住的不是页面多而是订单状态怎么在三类角色之间流转。这套基于 JavaScript Python 的数据库课程设计把学生客户、商家、学生配送员放在同一套数据模型上客户下单后能看见“制作中、派单中、接单中”商家负责接单和派单配送员抢单后把外卖送到客户手里。拿到手的压缩包里有小程序前端源码、Python 后端接口、SQL 脚本和一堆设计参考图片适合正在做外卖/交易类课设、或者想快速看懂“小程序后端数据库”怎么串起来的人。2. 数据库设计先于代码订单状态机与九张核心表我拿到一个课设资源一般先不跑代码先翻 SQL 脚本和.DS_Store旁边的图片素材。为什么因为前端页面是给人看的数据库才是系统的骨架。这套校园外卖系统的核心矛盾是一个订单要经过客户、商家、配送员三个角色每个角色对同一订单有不同的操作权限如果表结构没设计好后面所有接口都在打补丁。下面按我自己的建模顺序来拆。2.1 角色模型与权限边界用户表是最顶层的主数据。我一般不在课程设计里把客户表、商家表、配送员表拆成三张而是用一张users表加role字段1 学生客户2 商家3 学生配送员。理由有三个第一三个角色共用的字段手机号、头像、昵称不用重复建三份第二登录时一次查询就能拿到角色前端跳转逻辑简单第三外卖场景里同一个用户理论上可以既是学生又是配送员单表加角色天然支持这种身份切换。权限边界在接口层做不在表里做。比如POST /api/business/dispatch的处理器里先取当前用户 role如果role ! 2直接返回“无权限”。表结构里不去设计用户-角色映射表是因为课程设计不需要引入 RBAC 的复杂度过度设计会让答辩老师追问半天。2.2 订单状态流转接下来是订单状态机。摘要里提到客户能看到制作中、派单中、接单中实际上完整状态还要补上待接单和已完成不然流程断不开。下面这张状态流转表建议直接贴在课设文档里。状态值状态名谁触发下一个状态-1已取消客户/商家终点0待接单客户下单11制作中商家接单22派单中商家制作完成发布派单33接单中配送员接单44已完成配送员送达/客户确认终点这张表最好在答辩时第一个讲。状态流转必须单向推进不允许从 1 直接跳到 3否则统计信息会乱。前端展示的“制作中、派单中、接单中”只是其中三个节点数据库里一定要留足整个链条不然配送员抢单前和抢单后的状态没法区分。2.3 表结构设计与字段说明这个项目我拆出来的核心表是九张users、shop、category、goods、cart、orders、order_items、deliveries、reviews。每张表解决一个明确问题表名作用关键字段users用户表id, username, password, role, nickname, avatar, phone, addressshop店铺表id, user_id, name, notice, statuscategory商品分类表id, shop_id, name, sortgoods商品表id, shop_id, category_id, name, price, stock, image_urlcart购物车表id, user_id, goods_id, quantityorders订单主表id, order_no, user_id, shop_id, total_amount, stateorder_items订单明细表id, order_id, goods_id, goods_name, price, quantitydeliveries配送表id, order_id, courier_id, dispatch_time, accept_time, statusreviews评价表id, order_id, user_id, rating, content订单明细一定要单独建表因为商品价格会变。下单时要把商品名称、单价、数量都存快照不能只关联 goods_id否则商家改价后历史订单全乱。reviews 和 deliveries 上都要加唯一约束保证一个订单最多一条评价、一条配送记录。2.4 ER 关系与建表脚本示例下面是核心表的简化建表脚本。课程设计阶段不需要把每张表都堆上去但这几张卡住的是状态流转和统计查询。CREATE DATABASE IF NOT EXISTS campus_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE campus_order; CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 课程设计常用md5双层加密, role TINYINT NOT NULL COMMENT 1学生客户 2商家 3学生配送员, nickname VARCHAR(32) DEFAULT , avatar VARCHAR(255) DEFAULT , phone VARCHAR(20) DEFAULT , address VARCHAR(255) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE goods ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, shop_id INT UNSIGNED NOT NULL, category_id INT UNSIGNED DEFAULT 0, name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, image_url VARCHAR(255) DEFAULT , status TINYINT DEFAULT 1, sales_count INT DEFAULT 0, KEY idx_shop (shop_id) ) ENGINEInnoDB; CREATE TABLE orders ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT UNSIGNED NOT NULL COMMENT 下单的客户id, shop_id INT UNSIGNED NOT NULL, total_amount DECIMAL(10,2) NOT NULL, state TINYINT NOT NULL DEFAULT 0 COMMENT -1取消 0待接单 1制作中 2派单中 3接单中 4已完成, address VARCHAR(255) NOT NULL, phone VARCHAR(20) NOT NULL, remark VARCHAR(255) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME DEFAULT NULL, KEY idx_user (user_id), KEY idx_shop_state (shop_id, state) ) ENGINEInnoDB; CREATE TABLE order_items ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id INT UNSIGNED NOT NULL, goods_id INT UNSIGNED NOT NULL, goods_name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, KEY idx_order (order_id) ) ENGINEInnoDB; CREATE TABLE deliveries ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id INT UNSIGNED NOT NULL, courier_id INT UNSIGNED NOT NULL, dispatch_time DATETIME DEFAULT NULL, accept_time DATETIME DEFAULT NULL, finish_time DATETIME DEFAULT NULL, status TINYINT DEFAULT 0 COMMENT 0未接 1配送中 2已完成, UNIQUE KEY uk_order (order_id) ) ENGINEInnoDB; CREATE TABLE reviews ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id INT UNSIGNED NOT NULL, user_id INT UNSIGNED NOT NULL, shop_id INT UNSIGNED NOT NULL, rating TINYINT NOT NULL DEFAULT 5, content VARCHAR(255) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order (order_id) ) ENGINEInnoDB;代码里的几个点需要说明order_no用唯一业务编号不直接暴露自增 id避免被遍历下订单state用 TINYINT 而不是字符串查询快、占空间少但前端必须维护状态字典deliveries和reviews的UNIQUE KEY uk_order是数据完整性底线保证一个订单不会被两个配送员同时接走也保证客户只能评一次。字段注释一定要写因为在 MySQL Workbench 生成 ER 图时注释会直接变成图里标签答辩加印象分。注意state 状态值一旦在客户端写死之后不要随意调整顺序否则已有订单的状态语义会错位。宁可一开始多设计两个状态也不要后面补。3. Python 后端Flask 接口与订单派单的请求封装数据库设计完接下来就是后端接口。这套资源里的后端是用 Python 写的我按自己惯用的 Flask 路线来拆因为课程设计不需要 Django 那种全家桶Flask 单文件就能跑给小程序提供 JSON 接口刚刚好。整个后端可以压缩成三个文件db.py管连接utils.py管统一返回app.py管路由。3.1 为什么用 Flask 而不是 DjangoFlask 的优点是轻量、路由直观、虚拟环境好配。Django 自带 Admin、ORM、认证体系但对课设来说反而像戴着镣铐跳舞如果老师问“这个表是你设计的吗”你得解释 Django 那套迁移机制容易把问题扯远。Flask 加 pymysql 是课程设计的经典组合代码量少逻辑透明。我一般会把后端目录组织成backend/ app.py db.py utils.py requirements.txt static/ goods/ uploads/static/goods放商品图片static/uploads放用户头像。这样图片和代码分开部署时不会混淆。3.2 统一返回格式与数据库连接小程序的wx.request本身就能接 JSON但如果不做统一包装每个接口返回结构不一样前端要写一堆 if 判断。我习惯所有接口都返回{code, msg, data}前端只看code是否为 0。# db.py import pymysql def get_conn(): return pymysql.connect( host127.0.0.1, port3306, userroot, password123456, databasecampus_order, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor, # 返回字典前端不用自己映射字段 autocommitTrue )这段连接参数里charsetutf8mb4必须写否则中文乱码问题会一直跟着你。DictCursor让查询结果直接是字典列表接口返回给小程序时少一层转换。autocommitTrue省去了手动 commit但涉及订单插入和库存扣减时建议手动管理事务。# utils.py from flask import jsonify def ok(dataNone, msgok): return jsonify({code: 0, msg: msg, data: data}) def fail(msgerror, code1): return jsonify({code: code, msg: msg, data: None})这样封装以后业务接口里return ok()或者return fail()就行了前端拦截器统一处理错误提示。3.3 关键接口实现核心接口集中在三个动作客户下单、商家接单/派单、配送员抢单。先看下单接口# app.py from flask import Flask, request from db import get_conn from utils import ok, fail import uuid app Flask(__name__) app.route(/api/order/create, methods[POST]) def order_create(): data request.get_json() goods_id data.get(goodsId) quantity data.get(quantity) address data.get(address) phone data.get(phone) # 简化的价格获取实际要用事务包住订单和明细插入 conn get_conn() with conn.cursor() as cur: cur.execute(SELECT shop_id, price FROM goods WHERE id%s, (goods_id,)) goods cur.fetchone() if not goods: return fail(商品不存在) order_no uuid.uuid4().hex[:16] total goods[price] * quantity cur.execute( INSERT INTO orders (order_no, user_id, shop_id, total_amount, state, address, phone) VALUES (%s, %s, %s, %s, 0, %s, %s), (order_no, 1, goods[shop_id], total, address, phone) ) order_id cur.lastrowid cur.execute( INSERT INTO order_items (order_id, goods_id, goods_name, price, quantity) VALUES (%s, %s, %s, %s, %s), (order_id, goods_id, goods[name], goods[price], quantity) ) return ok({orderNo: order_no, state: 0})这段代码先查商品拿价格和店铺 id再生成订单号和订单明细。顺序不能反否则金额不可信。状态初始值固定为 0也就是“待接单”。接下来是状态流转中最容易翻车的点——乐观锁app.route(/api/business/accept, methods[POST]) def business_accept(): data request.get_json() order_id data.get(orderId) conn get_conn() with conn.cursor() as cur: updated cur.execute( UPDATE orders SET state1 WHERE id%s AND state0, (order_id,) ) if updated: return ok() return fail(订单状态已变化请刷新)关键在WHERE id%s AND state0没有这个条件两个请求同时进来就会把状态覆盖掉。商家派单接口同理条件改成state1更新到 2配送员抢单接口条件改成state2更新到 3。每一步都走“当前状态 - 下一个状态”的校验。3.4 权限校验与参数校验课程设计里权限没必要做太复杂写一个装饰器就行from functools import wraps def require_role(*roles): def wrapper(fn): wraps(fn) def inner(*args, **kwargs): # 实际应从请求头解析出用户这里简化 user {id: 1, role: 1} if user[role] not in roles: return fail(无权限) return fn(*args, **kwargs) return inner return wrapper商家接口上标require_role(2)配送员接口标require_role(3)。这样权限边界保持在接口层而不是靠前端隐藏按钮。参数校验至少判断关键字段非空比如下单接口缺了address要直接拒绝否则数据库会写入一堆残缺订单。提示token 逻辑在课设里可以从简但别用明文密码连接数据库。密码至少做一层 md5答辩演示时也显得专业。4. 微信小程序前端JavaScript 页面逻辑与状态渲染后端接口有了小程序端要做的就是把 REST 数据变成人话。这个微信小程序项目实例是经典原生写法不依赖 uni-app 或 Taro好处是答辩时能让老师直接看懂目录结构。JavaScript 页面逻辑就分两块请求封装和状态渲染。4.1 小程序目录结构与 app.json 配置目录结构建议按角色分页面miniprogram/ pages/ index/ # 客户首页商品浏览 order/ # 客户订单列表 user/ # 用户中心 merchant/ # 商家工作台 courier/ # 配送员任务列表 utils/ request.js # wx.request 封装 status.js # 订单状态字典 app.js app.json app.wxssapp.json只需要注册页面和窗口样式{ pages: [ pages/index/index, pages/order/order, pages/user/user, pages/merchant/merchant, pages/courier/courier ], window: { navigationBarTitleText: 校园外卖, navigationBarBackgroundColor: #FF7043, navigationBarTextStyle: white }, style: v2, sitemapLocation: sitemap.json }如果你想把商家端做成自定义导航栏还需要设置navigationStyle: custom。这时要自己计算顶部导航栏高度状态栏高度用wx.getWindowInfo().statusBarHeight加上胶囊按钮高度。不同机型差异很大iPhone X 以上和普通安卓可能差 20px我一般直接拿wx.getMenuButtonBoundingClientRect()的返回值去动态撑开而不是写死 44px。4.2 wx.request 请求封装与本地缓存所有页面都不直接调wx.request先封装成一个 JavaScript 函数返回 Promise// utils/request.js const BASE_URL http://127.0.0.1:5000/api function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method, data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) ? Bearer wx.getStorageSync(token) : }, success(res) { const body res.data if (body.code 0) { resolve(body.data) } else { wx.showToast({ title: body.msg, icon: none }) reject(body) } }, fail: reject }) }) } module.exports { request, BASE_URL }封装以后页面代码只需要await request(/goods/list)异常和 toast 都集中处理。token 用wx.setStorageSync存本地如果要控制登录有效期我会再存一个expires时间戳读取时和Date.now()比较这就是微信小程序设置缓存时间的常见做法。注意BASE_URL在真机调试时要改成局域网 IP后面避坑章节会细说。4.3 商品列表与下单逻辑客户首页的控制器代码很短// pages/index/index.js const { request } require(../../utils/request) Page({ data: { goods: [] }, onLoad() { this.loadGoods() }, async loadGoods() { const list await request(/goods/list?shopId1, GET) this.setData({ goods: list }) }, async createOrder(e) { const goodsId e.currentTarget.dataset.id const price e.currentTarget.dataset.price wx.showModal({ title: 下单, content: 确认购买 price 元商品, success: async (res) { if (res.confirm) { const result await request(/order/create, POST, { goodsId, quantity: 1, address: wx.getStorageSync(address), phone: wx.getStorageSync(phone) }) wx.navigateTo({ url: /pages/order/order?orderNo result.orderNo }) } } }) } })e.currentTarget.dataset.id是从 wxml 的>// utils/status.js const ORDER_STATUS { -1: { text: 已取消, color: #999 }, 0: { text: 待接单, color: #F56C6C }, 1: { text: 制作中, color: #E6A23C }, 2: { text: 派单中, color: #409EFF }, 3: { text: 接单中, color: #67C23A }, 4: { text: 已完成, color: #909399 } } module.exports { ORDER_STATUS }在订单页 wxml 里用wx:for循环渲染通过state映射文案和颜色view wx:for{{orderList}} wx:keyid text stylecolor: {{ORDER_STATUS[item.state].color}} {{ORDER_STATUS[item.state].text}} /text /view注意 wxml 不能直接使用 js 文件导入的变量需要在onLoad里把ORDER_STATUS合并到data中。三端页面都能复用这套字典商家看到“待接单”就显示“接单”按钮配送员看到“派单中”就显示“抢单”按钮客户只读状态。这样整个系统的状态展示不会有二义性。5. 避坑与排查三端联调最容易翻车的六个位置联调阶段才是课设最耗时间的地方前端跑起来不等于整个链路通。下面六条是我拆这类项目时反复踩过的坑每一条都按现象、原因、解决讲清楚直接抄进你的排错笔记就行。5.1 图片路径写死导致商品图裂掉现象首页商品图大面积裂开控制台报 404。原因解压后素材里有2.jpg、-1.jpg、33.jpg这类命名杂乱的图片前端直接写src/img/2.jpg后端根本不在这个目录。解决把素材统一放到backend/static/goods/数据库存/static/goods/2.jpg这种相对路径小程序侧用BASE_URL image_url拼完整地址。不要把-1.jpg这种带横线的文件名硬编码到代码里转存时顺手重命名一次后面省很多事。5.2 订单状态被覆盖两个角色同时操作同一订单现象商家刚点“接单”配送员也点“抢单”结果订单状态变成空白或跳过了制作中。原因前端用wx.navigateTo带订单状态给详情页后端UPDATE orders SET state3 WHERE id?没判断当前状态后提交的请求把前一个覆盖了。解决所有状态更新都带当前状态条件比如UPDATE orders SET state1 WHERE id? AND state0。如果返回影响行数为 0前端统一提示“订单状态已变化请刷新”不要静默失败。5.3 本地联调 wx.request 永远 fail现象开发者工具请求http://127.0.0.1:5000报request:fail。原因小程序默认要求合法域名本机 IP 不在白名单里。解决开发者工具右上角“详情-本地设置”勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。用真机演示时把BASE_URL改成局域网 IP并且后端app.run(host0.0.0.0, debugTrue)不然手机请求不到电脑上的 Flask 服务。5.4 中文乱码发布商品变问号现象商家输入“黄焖鸡套餐”小程序端看到“”。原因建库时用了默认 latin1pymysql 连接没指定charset。解决数据库连接串写charsetutf8mb4建库语句加DEFAULT CHARACTER SET utf8mb4。如果已经建好的库执行ALTER TABLE goods CONVERT TO CHARACTER SET utf8mb4表结构和 connection 必须统一。这个坑最隐蔽因为有时候命令行查是正常的但小程序端解析就乱。5.5 头像上传后刷新就裂现象wx.chooseMedia选完头像后能显示但退出页面再进来就裂。原因小程序tempFilePaths返回的是临时路径只在本次会话有效。解决用wx.uploadFile上传到后端/api/upload/avatar后端把文件保存到static/uploads/返回一个永久 URL 存数据库。数据库里不要存wxfile://tmp_xxx这种临时路径血泪教训真机测试时特别容易翻车。5.6 订单时间差 8 小时现象前端显示 12:00数据库存的却是 04:00。原因MySQL 的DATETIME不带时区服务器是 UTCPython 的datetime.now()取的是系统 UTC 时间小程序端new Date(2024-01-01T04:00:00Z)又会自动转成本地时间两边对不上。解决后端统一用本地时区生成时间或者在 MySQL 连接串里加time_zone08:00。最省事的方案是前端不解析时间字符串数据库返回什么就显示什么但课设文档里最好写清楚时间策略老师追问时能答上来。6. 验证与答辩演示用一组测试数据走通全流程前面把表结构、后端接口、小程序渲染和常见坑都过了一遍最后说下怎么验证。课程设计答辩最怕的是现场演示时状态卡住所以我会准备一套固定账号和固定流程提前跑通不给老师留提问的空间。6.1 准备三端测试账密角色账号密码预期入口学生客户student123456首页商品、下单、订单列表商家merchant123456商品管理、订单接单/派单、统计学生配送员courier123456派单列表、抢单、送达6.2 端到端演示顺序步骤操作预期状态1客户登录下单一杯奶茶待接单2商家登录点“接单”制作中3商家制作完成点“派单”派单中4配送员登录点“抢单”接单中5配送员送达点“完成”已完成6客户进入订单详情评分评价成功状态不变这个流程覆盖了所有状态节点也覆盖了三个角色的操作边界。演示时从客户账号开始其他两个账号提前登录好不要在答辩现场频繁切换切换太慢就会被评委怀疑系统不稳定。6.3 统计信息验证商家端能看到统计信息后台其实就是一条聚合 SQLSELECT DATE(create_time) AS day, COUNT(*) AS order_count, SUM(total_amount) AS amount FROM orders WHERE shop_id 1 AND state 4 GROUP BY day ORDER BY day DESC;这条语句统计的是“已完成”订单如果状态机没走完比如订单卡在派单中这个数字就永远不对。所以演示前先跑一遍 6.2 的流程让至少一单状态变成 4统计数字才好看。这套流程我拆过好几遍每次准备预览图或线上版其实页面切换时经常因为网络慢状态没刷出来白屏几秒评委就会追问。从那以后我每次演示前都强制走一遍三端全流程先清空数据库再重新造一轮数据确保每个状态按钮都能点。希望帮到你。本文还有配套的精品资源点击获取
返回列表