简介:这是一套围绕咖啡店业务场景的完整前后端项目,基于微信小程序与Java后端开发,适合小程序开发初学者、Java后端学习者和需要完成课程设计/毕业设计的学生使用。压缩包共1904个文件,约16MB,以js、ts、wxss、wxml等前端小程序代码为主,同时包含java/class后端接口、json与xml配置、md说明文档及sql数据库脚本,结构清晰,方便对照学习前后端交互、商品展示、下单支付与订单管理等完整流程。内容涵盖商品、用户、订单、帖子等核心功能模块,前端覆盖页面、组件与工具,后端包含控制器、实体类与工具类,能清晰看到微信小程序调用Java接口的完整链路。其中还包含说明文档与配置文件,可帮助读者快速搭建运行环境,并参考接口设计、页面样式和业务逻辑进行二次开发或改造。已有90人学习浏览。无论用于课程实践还是项目复盘,这份代码都能作为可运行的咖啡店小程序参考样板。
1. 咖啡店.zip:一套能直接复现的 Flask 收银点单小系统
前阵子翻旧硬盘,翻出一个“咖啡店.zip”,解压后发现是一套用 Flask 和 SQLite 写得比较完整的收银后台。它能管点单、库存、日报表和员工权限,前后端不分离,也不依赖 Docker 或云服务,装上 Python 依赖就能本地跑。对正在做课程设计、想完整走一遍增删改查的在校生,以及想快速给小店弄个试运行收银后台的从业者来说,这个包比从零写省事得多。这篇笔记我按复现顺序拆:先看目录和数据库,再把服务跑起来,然后演示二次开发加一个员工排班功能,最后把部署调试中真正容易翻车的地方和备份方案一起列出来。
2. 拆解“咖啡店.zip”:目录布局、四张核心表与权限控制
2.1 解压之后先看什么:从入口到模板的文件职责
解压后是一个标准 Flask 项目结构,长这样:
coffee_shop/ ├── app.py # 应用入口,注册蓝图、初始化数据库 ├── requirements.txt # 依赖清单 ├── config.py # 全局配置:端口、数据库路径、密钥 ├── models.py # SQLAlchemy 模型定义 ├── utils/ │ └── auth.py # 登录态与角色校验装饰器 ├── views/ │ ├── auth.py # 登录、登出、修改密码 │ ├── cashier.py # 收银台:点单、下单、打印小票 │ └── admin.py # 管理端:菜品、库存、报表 ├── templates/ │ ├── base.html # 公共模板,导航栏和侧边栏 │ ├── login.html │ ├── menu.html # 菜单展示与点单页面 │ └── dashboard.html # 今日营收和品类排行 ├── static/ │ ├── css/ │ └── js/ └── init_db.py # 建表 + 写入默认用户和菜品数据读代码的顺序建议是config.py -> models.py -> app.py -> init_db.py。config.py 控制了数据库文件放在 instance 目录下,端口默认 5000。models.py 是所有表的起点,后面管理端页面大部分操作都是围绕这四张表做查改。views 目录下三个模块按角色拆开,收银员和管理员看到的路由不同,但共用同一套登录态。
init_db.py 单独放而不是在 app.py 里自动建表,原因很实际:课程设计和演示场景下,经常需要重置数据重新开始。如果你希望每次启动都自动建表,可以把它挪到 app.py 的with app.app_context(): db.create_all()里,但生产环境不要这么干。
2.2 数据库模型:四张核心表如何支撑一次完整交易
整个业务围绕四张表展开,模型代码如下:
from flask_sqlalchemy import SQLAlchemy from datetime import datetime db = SQLAlchemy() class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) role = db.Column(db.String(16), default='staff') # staff / admin class Dish(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(128), nullable=False) price = db.Column(db.Float, nullable=False) stock = db.Column(db.Integer, default=0) status = db.Column(db.Boolean, default=True) # True 上架 / False 停售 class Orders(db.Model): id = db.Column(db.Integer, primary_key=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) total_amount = db.Column(db.Float, nullable=False) status = db.Column(db.String(16), default='pending') # pending / done / cancel class OrderItem(db.Model): id = db.Column(db.Integer, primary_key=True) order_id = db.Column(db.Integer, db.ForeignKey('orders.id')) dish_id = db.Column(db.Integer, db.ForeignKey('dish.id')) quantity = db.Column(db.Integer, default=1)这四张表的关系按使用场景理解:顾客选菜时操作 Dish,提交订单时生成一条 Orders,同时把每一道菜的数量写入 OrderItem。User 表负责登录和角色识别,admin 报表页根据 OrderItem 和 Dish 做关联聚合。
| 表名 | 核心字段 | 主要用途 |
|---|---|---|
| User | username, password_hash, role | 登录、权限判断 |
| Dish | name, price, stock, status | 菜单展示、库存扣减 |
| Orders | created_at, total_amount, status | 订单头,记录总价和状态 |
| OrderItem | order_id, dish_id, quantity | 订单明细,统计品类销量 |
price 用 Float 存,是这套代码里一个隐患:浮点数算总价时可能出现 0.1 + 0.2 不等于 0.3 的情况。如果只是课程设计或小店内部用,问题不大;要是接入支付网关,最后一定要改成Numeric(10, 2)。另一个隐患在 OrderItem 上——SQLite 默认不启用外键约束,插入一个不存在的 dish_id 也不会报错,后面排班功能里我要专门处理这个坑。
2.3 权限控制:同一个登录入口怎么区分收银员和管理员
User 表里有个 role 字段,取值是 staff 或 admin。登录成功后 session 里会保存 user_id 和 role,之后每个受保护接口通过装饰器校验:
from functools import wraps from flask import session, jsonify def role_required(*roles): def wrapper(fn): @wraps(fn) def decorated(*args, **kwargs): if not session.get('user_id'): return jsonify({'code': 401, 'msg': '请先登录'}), 401 if session.get('role') not in roles: return jsonify({'code': 403, 'msg': '无权限访问'}), 403 return fn(*args, **kwargs) return decorated return wrapper使用方式是在视图函数上标@role_required('admin')或@role_required('staff', 'admin')。*roles接收多个值,只要 session 里保存的角色命中其中任意一个就放行。这样收银台路由只允许 staff 以上访问,菜品管理只放行 admin,登录接口不用装饰器。
这套手写装饰器够用,但不适合大型项目。真实商业环境建议换 flask-login 或写 JWT 中间件,因为 session 在跨端场景下不好维护。这个小系统里它足够清晰,能让新手直接看到权限控制的最小写法,调整起来也方便。
3. 搭建运行环境:用虚拟环境、依赖安装和初始化数据库三步跑通
3.1 为什么第一步是创建虚拟环境
直接往系统 Python 里装 Flask 会埋下隐患:机器上多个项目同时需要不同版本的 Flask、pandas 或 SQLAlchemy,时间一长就把全局环境搞脏了。别问我是怎么知道的,有次给某个旧的 Flask 项目修 bug,顺手升级了全局的 SQLAlchemy,结果另一个项目当场跑不起来。从那以后我每次拿到新项目都先开虚拟环境。
在项目根目录执行:
cd coffee_shop python -m venv venvWindows 下激活:
venv\Scripts\activatemacOS 或 Linux 下激活:
source venv/bin/activate激活后命令行前面会出现(venv)标识,pip 安装的包都会进这个隔离环境。如果本机装了多个 Python 版本,建 venv 时显式指定版本:python3.10 -m venv venv,这套代码在 Python 3.8 到 3.11 下测试过,表现稳定。
3.2 从 requirements.txt 安装依赖并理解版本选择
依赖清单维护在 requirements.txt 里:
| 库名 | 版本 | 用途 |
|---|---|---|
| Flask | 2.2.5 | Web 框架与开发服务器 |
| Flask-SQLAlchemy | 3.0.5 | ORM 与数据库迁移 |
| pandas | 2.0.3 | 排班 Excel 读取、报表聚合 |
| openpyxl | 3.1.2 | .xlsx 文件底层解析引擎 |
安装命令:
pip install -r requirements.txt版本是我实测后钉住的。不要无脑升级到最新版:Flask 3.x 某些 API 有行为变更,pandas 2.1 以上对 Python 3.8 的兼容没老版本稳。如果有依赖装不上,通常是因为 pip 默认源太慢,临时切到国内镜像源执行pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple就能解决。
3.3 初始化数据库并写入默认菜单
首次运行前要执行 init_db.py,它会删掉旧库、重建四张表、写入一个管理员账号和几道测试菜品。核心代码:
# init_db.py from app import app, db from models import User, Dish with app.app_context(): db.drop_all() db.create_all() boss = User(username='boss', password_hash='pbkdf2:sha256:260000$xxx', role='admin') db.session.add(boss) db.session.add(Dish(name='美式咖啡', price=18.0, stock=100)) db.session.add(Dish(name='拿铁', price=22.0, stock=80)) db.session.commit()运行方式:
python init_db.py这里有两件事要解释。第一,db.drop_all()会把所有数据清空,这个脚本只能用于首次初始化,日常上线千万别执行,我下面会专门讲备份方案。第二,password_hash里存的是 werkzeug 生成的哈希,不是明文。如果登录时报密码错误,用 Python 交互窗口执行:
from werkzeug.security import generate_password_hash pwd = generate_password_hash('123456') print(pwd)把输出的值替换到 init_db.py 里再执行,就能得到明文 123456 的账号。
3.4 启动应用并完成第一笔订单测试
启动开发服务器:
python app.py默认地址是http://localhost:5000,看到如下日志就说明 Flask 正常:
* Running on http://127.0.0.1:5000 * Restarting with stat浏览器打开登录页,用 boss 身份登录,走到收银台页面,点几道菜品并提交订单。验收一个系统是否真正跑通,我按下面三个节点自查:
- 登录后菜单是否展示上架菜品,停售菜品不出现在点单列表。
- 提交订单后,厨房屏或订单列表能看到 status 为 pending 的单子。
- 管理员页面里库存数按购买数量自动扣减。
如果跑到第二或第三步发现页面报 500,优先查看终端里的异常堆栈,八成是数据库没有初始化,或者 config.py 里的密钥没设置导致 session 报错。
4. 二次开发:加入“员工排班”模块的完整流程
4.1 先设计排班表:字段粒度与关联方式
拿这套代码做课程设计,通常会遇到导师要求加独立功能。我以“员工排班”为例演示怎么从设计到落地。排班要记录三个关键信息:哪位员工、哪一天、值哪个班次。新建模型:
class Shift(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) date = db.Column(db.String(10), nullable=False) shift_type = db.Column(db.String(16), nullable=False) # morning / eveningdate 字段用字符串YYYY-MM-DD而不是 DateTime,这是刻意的。排班查询大多按天或按周范围来做,字符串在 SQLite 里能直接比较排序,还能避免时区干扰。不过这里必须打开 SQLite 外键约束,否则会写入不存在的 user_id:
PRAGMA foreign_keys = ON;在 Flask 里通常放在db.engine.execute('PRAGMA foreign_keys = ON'),或者在连接建立后执行。否则Shift(user_id=999)也能保存成功,报表里就会关联出一个空白员工。
4.2 后端路由:接收 Excel 文件并批量写入
排班数据量一大,逐条在网页上填不现实。我一般让用户上传 xlsx 文件,后端用 pandas 解析。视图代码:
# views/admin.py import pandas as pd from flask import Blueprint, request, jsonify from models import Shift, db @bp.route('/shift/import', methods=['POST']) def import_shift(): file = request.files.get('file') if not file: return jsonify({'code': 400, 'msg': '未选择文件'}), 400 df = pd.read_excel(file.stream) df = df.dropna(subset=['员工ID', '日期', '班次']) for _, row in df.iterrows(): shift = Shift( user_id=int(row['员工ID']), date=str(row['日期']), shift_type=str(row['班次']).strip() ) db.session.add(shift) db.session.commit() return jsonify({'code': 0, 'msg': f'成功导入 {len(df)} 条排班'}), 200逻辑分三步:取出上传文件、pandas 读成 DataFrame、按行写入 Shift 表。dropna这一步值注意,Excel 表格末尾经常有空行,读者会把 NaN 当数据传进数据库,导致非空字段报错。int(row['员工ID'])会把 Excel 里的“00123”变成 123,如果员工 ID 前面有补零需求,这里要改成字符串处理。
批量写入 1000 条以内没问题。如果排班表一个月有上万条记录,逐条 commit 会慢,改用db.session.bulk_save_objects能提升性能,但相应要自己处理校验逻辑。
4.3 前端页面:上传表单与回显排班结果
页面端只需一个表单,关键是enctype和accept别写错:
<form action="/shift/import" method="post" enctype="multipart/form-data"> <input type="file" name="file" accept=".xlsx,.xls"> <button type="submit">导入排班</button> </form>enctype="multipart/form-data"没有它,文件内容不会传到后端。accept=".xlsx,.xls"能在选择文件时过滤非 Excel 类型,但只是前端提醒,后端仍需校验扩展名。我一般在后端加一行:
if not file.filename.endswith(('.xlsx', '.xls')): return jsonify({'code': 400, 'msg': '仅支持 Excel 文件'}), 4004.4 重复导入与数据校验
第一次做这个功能时,我直接在循环里db.session.add(shift)然后 commit,结果同一个导入文件被点了两次,排班表里出现双份数据。后来改成先查库再决定方式:
existing = Shift.query.filter_by(user_id=row['员工ID'], date=str(row['日期'])).first() if existing: existing.shift_type = str(row['班次']).strip() else: db.session.add(Shift(...))这是“有则更新、无则插入”的常见做法。对排班场景合理,因为你希望批量导入覆盖旧排班而不是叠加。这个逻辑对报表型数据不适用,销售明细不能覆盖只能追加,所以二次开发时先想清楚业务语义。
另外,Excel 里的日期在 pandas 读取后可能变成 Timestamp 对象,直接str()会得到2025-05-01 00:00:00而不是2025-05-01。处理方式是:
date_str = row['日期'].strftime('%Y-%m-%d') if hasattr(row['日期'], 'strftime') else str(row['日期'])这个小细节直接影响排班查询,我在第三次跑的时候才想到,算是一条隐蔽的踩坑经验。
5. 避坑指南:部署与调试中五个典型问题
5.1 端口被占用导致启动失败
现象:执行python app.py后终端弹OSError: [Errno 98] Address already in use。
原因:上次启动的 Flask 进程没退出,或者本机 5000 端口被其他服务占用。
解决:换端口启动:
python app.py -p 5001或者找出占用端口的进程并结束:
lsof -i :5000 kill -9 <PID>Windows 下用netstat -ano | findstr :5000找 PID,然后taskkill /PID <PID> /F。另外 Flask 开发模式默认会启动 reloader,它会同时拉起两个进程,调试时容易误杀一个另一个还在占端口,直接pkill -f app.py更省事。
5.2 SQLite 数据库文件被锁
现象:内网小型服务器上跑,订单稍多就报sqlite3.OperationalError: database is locked。
原因:SQLite 同一时刻只允许一个进程写库。Flask 调试模式的 reloader 启动两个进程,或者后台脚本也在写同一个 db 文件,就会出现锁冲突。
解决:切换为单进程运行,不开启 reloader。SQLite 层面执行:
PRAGMA journal_mode = WAL;WAL 模式让读写并发提升不少,根本性的解决办法是换 PostgreSQL,但小店量级真没必要。记住一个结论:SQLite 适合读多写少的轻量系统,并发写超过 100 个请求就得换数据库。
5.3 中文乱码
现象:Windows 下运行,控制台日志中文变成问号,网页上菜单名显示乱码。
原因:Windows 控制台默认 GBK 编码,Flask 输出 UTF-8 字符串时被截断。网页乱码则多半是模板没有声明 UTF-8 字符集。
解决:在 app.py 顶部强制 stdout 编码:
import sys sys.stdout.reconfigure(encoding='utf-8')模板文件里加上:
<meta charset="UTF-8">5.4 静态资源 404,CSS 和 JS 加载失败
现象:页面 HTML 正常,但浏览器控制台一堆 404,样式完全裸奔。
原因:模板里用了写死的绝对路径/static/css/style.css,如果 Flask 的部署环境加了网关前缀或蓝图的子路由嵌套,路径就会找错。
解决:改用url_for动态生成:
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">这也是测试环境没问题、部署到生产环境就样式全丢的经典原因两处之一,另一处是没执行 collectstatic。
5.5 日报表数据跨日错位
现象:今天早上 8 点下的单,统计到了昨天报表里。
原因:Orders.created_at用的是datetime.utcnow(),存的是世界标准时间,东八区早上 8 点实际是当天 0 点之前。
解决:本地部署或单店场景直接改成:
created_at = db.Column(db.DateTime, default=datetime.now)如果需要国际化,统一存 UTC,查询时再转换时区。对一家咖啡店来说,用本地时间更直观,省去报表前处理。
这五个坑几乎覆盖了一套小系统从开发到上线的常见故障链路。遇到问题先看终端日志,再看数据库连接和路径,最后检查静态资源和时区,别一上来就怀疑代码逻辑。
6. 给“咖啡店.zip”上一道保险:自动备份与一分钟恢复
系统能跑只是第一步,数据没了才是真正的灾难。小店的订单记录、菜品配置、员工排班都写在一个 SQLite 文件里,一块硬盘挂了就全没了。我习惯在项目里放一个 backup.py,作用是把 db 文件复制到带时间戳的备份目录:
import shutil from datetime import datetime from pathlib import Path src = Path('instance/coffee.db') dst_dir = Path('backups') if not src.exists(): raise SystemExit('数据库文件不存在,请检查路径') dst_dir.mkdir(exist_ok=True) timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') dst = dst_dir / f'coffee_{timestamp}.db' # 复制主库文件 shutil.copyfile(src, dst) # 同时复制 WAL 文件(如果存在) if Path('instance/coffee.db-wal').exists(): shutil.copyfile('instance/coffee.db-wal', f'{dst}-wal') print(f'备份完成:{dst}')重点在最后那一段:SQLite 用 WAL 模式时,最近写入的数据可能在-wal文件里,单独复制主 db 文件会丢最后几分钟的数据。所以备份时同时带走coffee.db-wal和coffee.db-shm才完整,这是很多新手最容易忽略的细节。
定时执行,Linux 上用 crontab:
0 2 * * * cd /path/to/coffee_shop && venv/bin/python backup.pyWindows 用任务计划程序,触发条件设为每天凌晨 2 点。恢复流程更简单:停掉 Flask 进程,把备份文件复制回 instance 目录并覆盖原文件,重新启动。整个过程用不上五分钟,实测有效。
从那以后我每次改这套源码,都强制自己先把备份流程走一遍,再动手改代码。改坏了也是一分钟回滚的事,总会给自己留个后悔药。希望帮到你。
本文还有配套的精品资源,点击获取