简介:这是一份面向Python初学者与课程设计学习者的超市管理系统源码包,围绕员工管理、客户管理、登录验证与商品库存记录等日常运营场景展开,适合用来练习GUI界面搭建、文件持久化存储与模块化组织方式。压缩包共13个文件,约8KB,以7个txt数据与配置文本、4个py功能模块、1个md说明文档和1个log日志文件为主,其中py文件分别承担员工、顾客、登录窗口与登录验证等职责,txt多用于保存账户、商品及顾客信息,md提供项目说明,log记录运行与警告信息。目前已有332人学习下载。通过阅读这套代码,读者可以理解Tkinter界面设计、登录校验、异常处理与logging日志记录等关键环节,并借鉴employee、customer等模块的拆分思路,快速搭建自己的管理系统课程作业或练手项目。
1. 从一份 Python 超市管理系统源码说起:它到底能跑通哪些真实业务
如果你正在找一份能直接跑起来、代码结构清晰、业务闭环完整的 Python 超市管理系统源码,那这份资源大概率能省掉你从零搭架子的一周时间。它不是那种只写了增删改查的玩具 Demo,而是把商品建档、库存预警、收银结算、会员折扣、销售流水、日结报表这几条主线都串了起来。技术栈是 Python + Flask + SQLite,前端用 Jinja2 模板加少量原生 JS,没有重型前端框架,对新手友好,对熟手也方便二次改造。适合谁?一是课程设计或毕设需要完整业务链的同学,二是想拿一个中小型 MIS 系统练手 Flask 分层设计的开发者,三是小超市老板想低成本搞一套能用的收银后台。我拆过不少同类源码,很多项目跑起来就报错,这份的依赖和初始化脚本相对完整,坑主要集中在数据库路径和编码上,后面会细说。
2. 环境搭建与数据库初始化:把 SQLite 表结构一次建对
2.1 依赖安装与项目结构速览
拿到源码包后,第一件事不是急着python app.py,而是先看目录。常见结构是app.py作为入口,models.py放 ORM 模型,views/下按模块拆蓝图,static/和templates/分别放静态资源和页面,instance/或data/放 SQLite 文件。我一般会先建虚拟环境,避免污染全局包。
python -m venv venv # Windows 下激活 venv\Scripts\activate # macOS / Linux 下激活 source venv/bin/activate pip install flask flask-sqlalchemy flask-login # 如果源码里有 requirements.txt,直接: pip install -r requirements.txt这里有个参数要留意:flask-sqlalchemy的版本和 Flask 版本有绑定关系。Flask 2.3 以上配 SQLAlchemy 3.x 是稳的,如果源码里写的是Flask==1.1.2,那 SQLAlchemy 得降到 2.4 附近,否则Model.query会报AttributeError。我一般会先pip show flask确认版本,再决定要不要改 requirements。
2.2 建表与初始数据:别让空库把你劝退
SQLite 的好处是零配置,坏处是路径写错就默默建到别处去了。源码里常见两种初始化方式:一种是db.create_all()在应用启动时自动建表,另一种是单独一个init_db.py脚本。我建议先跑初始化脚本,把商品分类、默认管理员、几个测试商品塞进去,不然登录进去一片空白,你都不知道是代码坏了还是没数据。
# init_db.py 典型写法 from app import app, db from models import User, Category, Product with app.app_context(): db.create_all() # 建表,已存在则跳过 # 默认管理员,密码记得改 if not User.query.filter_by(username='admin').first(): admin = User(username='admin', password='admin123', role='admin') db.session.add(admin) # 基础分类 for name in ['饮料', '零食', '日用品']: if not Category.query.filter_by(name=name).first(): db.session.add(Category(name=name)) db.session.commit() print('初始化完成')逻辑说明:with app.app_context()是 Flask 应用上下文,没有它db对象拿不到配置。db.create_all()只建缺失的表,不会改已有表结构,所以如果你改了模型字段,得删库重来或者手动ALTER TABLE。参数上,password明文存储是这类源码的通病,后面避坑章节会讲怎么换成哈希。初始数据里商品可以只放两三条,够你验证收银流程就行。
提示:SQLite 文件默认在
instance/目录下,如果代码里写的是相对路径sqlite:///supermarket.db,实际生成位置取决于启动时的工作目录。我习惯改成绝对路径,省得找不到库。
3. 核心业务模块拆解:商品、库存、收银三条线怎么串
3.1 商品管理与库存预警的实现逻辑
商品模块看着简单,但字段设计直接决定后面收银顺不顺。这份源码里Product模型一般包含barcode、name、price、cost、stock、category_id、alert_stock这几个字段。其中alert_stock是库存预警阈值,收银扣减库存后,如果stock <= alert_stock,后台首页会标红提醒。这个设计比单纯记库存数要实用,很多同类源码漏了这层。
# models.py 片段 class Product(db.Model): id = db.Column(db.Integer, primary_key=True) barcode = db.Column(db.String(32), unique=True, nullable=False) name = db.Column(db.String(64), nullable=False) price = db.Column(db.Float, nullable=False) # 售价 cost = db.Column(db.Float, default=0.0) # 进价,用于算毛利 stock = db.Column(db.Integer, default=0) alert_stock = db.Column(db.Integer, default=10) # 低于此值预警 category_id = db.Column(db.Integer, db.ForeignKey('category.id'))参数说明:barcode加唯一索引,扫码枪录入时不会重复建档。price和cost用 Float 在金额计算上会有浮点误差,常见做法是改用Numeric(10, 2)或者存整数分,但这份源码为了简单用了 Float,你在做日结报表时可能会看到0.30000000000000004这种数,心里有数就行。库存扣减我一般会放在收银事务里,和订单写入同一个db.session.commit(),避免卖了货库存没减。
3.2 收银结算与会员折扣的代码路径
收银是这套系统的核心。流程通常是:扫码或输入条码 → 查商品 → 加入购物车(前端 JS 维护)→ 选会员 → 算折扣 → 提交订单 → 扣库存 → 写流水。会员折扣一般按等级来,比如普通会员 1.0、银卡 0.95、金卡 0.9。这里有个容易翻车的地方:折扣算完的金额要四舍五入到分,不然日结对不上。
# views/cashier.py 结算逻辑简化版 @app.route('/checkout', methods=['POST']) def checkout(): data = request.get_json() items = data.get('items', []) # [{product_id, qty}] member_id = data.get('member_id') # 可为 None total = 0.0 order_items = [] for it in items: p = Product.query.get(it['product_id']) if not p or p.stock < it['qty']: return jsonify({'code': 1, 'msg': f'{p.name} 库存不足'}) total += p.price * it['qty'] order_items.append((p, it['qty'])) discount = 1.0 if member_id: m = Member.query.get(member_id) discount = m.discount if m else 1.0 pay_amount = round(total * discount, 2) # 关键:保留两位 order = Order(total=total, discount=discount, pay_amount=pay_amount, member_id=member_id) db.session.add(order) db.session.flush() # 拿到 order.id for p, qty in order_items: p.stock -= qty db.session.add(OrderItem(order_id=order.id, product_id=p.id, qty=qty, price=p.price)) db.session.commit() return jsonify({'code': 0, 'pay_amount': pay_amount})逻辑说明:db.session.flush()把订单写进事务但没提交,目的是拿到自增order.id给明细用。round(total * discount, 2)是必须的,否则前端显示和数据库存的可能差一分钱。库存检查放在循环里逐条判断,比先查后扣要安全,但高并发下仍有超卖风险,小超市单机用没问题,真要抗并发得上行锁或乐观锁。会员折扣从Member表读,没会员就按原价,这个分支要测到。
3.3 销售流水与日结报表的查询写法
流水表Order一般记id、total、pay_amount、member_id、created_at。日结报表就是按日期分组求和。SQLite 的日期函数和 MySQL 略有不同,用func.date()比较稳。
from sqlalchemy import func # 日结:按天汇总营业额和单数 rows = db.session.query( func.date(Order.created_at).label('day'), func.count(Order.id).label('order_count'), func.sum(Order.pay_amount).label('revenue') ).group_by(func.date(Order.created_at)).all() for r in rows: print(r.day, r.order_count, round(r.revenue, 2))参数说明:func.date()在 SQLite 下把2024-06-01 12:30:00截成2024-06-01,MySQL 下同样可用。func.sum返回的是 Decimal 或 Float,打印前round一下。如果created_at字段没设默认值,插入订单时得手动datetime.now(),否则分组出来全是 NULL。我见过有人忘了这茬,报表一片空白,查了半天以为是 SQL 写错了。
4. 避坑与排查:这份源码最容易翻车的五个地方
4.1 现象:启动报No module named 'flask_sqlalchemy'→ 原因:虚拟环境没激活或装错解释器 → 解决:确认which python指向 venv,重装依赖
这个坑新手命中率极高。你在终端pip install了,但 IDE 里跑的时候用的还是系统 Python。解决就一句话:在 IDE 里把解释器切到 venv 下的python.exe或bin/python。我一般会在项目根目录放一个.env或者直接看pip -V的路径对不对。
4.2 现象:登录后页面 500,日志报sqlite3.OperationalError: no such table: user→ 原因:没跑初始化脚本,或数据库路径不一致 → 解决:确认SQLALCHEMY_DATABASE_URI指向的库文件存在且已建表
常见情况是app.py里配置的是sqlite:///supermarket.db,而init_db.py里导入的 app 配置不同,结果建到了另一个文件。统一做法是把数据库 URI 写进config.py,两边都从同一个配置读。另外,删库重来之前先备份,别问我怎么知道的。
4.3 现象:收银提交后库存没减 → 原因:扣库存代码在commit之后,或异常被吞 → 解决:把库存扣减和订单写入放同一事务,检查try/except是否rollback
有些源码为了“健壮”,在结算外面包了try/except但 except 里只print不rollback,结果订单写了一半,库存没动。正确做法是except里db.session.rollback()再返回错误。你可以在扣库存后手动抛个异常测试,看订单和库存是不是一起回滚。
4.4 现象:金额出现0.30000000000000004→ 原因:Float 浮点精度 → 解决:展示和存储时round(x, 2),或改用Numeric
前面提过,Float 做金额计算必然有精度问题。临时方案是所有输出前round,彻底方案是把price、pay_amount改成db.Numeric(10, 2)。改字段类型后记得重建表,SQLite 不支持直接改列类型。
4.5 现象:中文商品名存进去变乱码 → 原因:SQLite 连接编码或文件编码不一致 → 解决:确保 Python 文件 UTF-8,连接串加?charset=utf8
SQLite 本身默认 UTF-8,乱码多半是源码文件用了 GBK 保存,或者模板渲染时没声明charset。检查templates/base.html里有没有<meta charset="UTF-8">,以及app.py开头有没有# -*- coding: utf-8 -*-。我一般统一用 UTF-8 无 BOM,省心。
5. 二次改造与验证:把明文密码换掉,再跑一遍全流程
5.1 用 Werkzeug 做密码哈希,别让 admin123 裸奔
这份源码默认明文存密码,拿来练手可以,真要放到局域网用就得改。Flask 自带的werkzeug.security就够,不用额外装包。
from werkzeug.security import generate_password_hash, check_password_hash # 建用户时 admin = User(username='admin', password=generate_password_hash('admin123'), role='admin') # 登录校验时 user = User.query.filter_by(username=form.username).first() if user and check_password_hash(user.password, form.password): login_user(user)逻辑说明:generate_password_hash默认用 pbkdf2,每次生成的盐不同,所以同一个密码存进去的值不一样,这是正常的。check_password_hash负责比对。改完之后,原来明文存的用户得手动重置密码,或者写个一次性脚本批量哈希。参数上不用调,默认强度够用。
5.2 全流程验证清单:从建档到日结走一遍
改完代码别急着说“好了”,按这个顺序跑一遍,能挡住九成问题:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 用 admin 登录 | 进入后台首页,库存预警区正常显示 |
| 2 | 新建商品,条码填唯一值 | 列表出现该商品,库存为初始值 |
| 3 | 收银台扫码/输入条码,加购两件 | 购物车金额正确 |
| 4 | 选会员结算 | 应付金额按折扣计算,保留两位 |
| 5 | 回商品列表看库存 | 库存已扣减,低于阈值标红 |
| 6 | 查日结报表 | 当天营业额和单数与实际一致 |
| 7 | 退出重登 | 会话正常,密码哈希校验通过 |
这张表我每次改完收银逻辑都会走一遍,尤其是第 4 和第 5 步,事务没处理好这里必露馅。第 6 步如果数字对不上,先看created_at是不是 UTC 时间,SQLite 默认存 UTC,你本地是东八区的话日期可能差一天,报表按func.date分组就会错位。解决办法是在写入时用本地时间,或者查询时做时区转换。
5.3 一个我常留的后门:用 pytest 写三条冒烟测试
手动点页面费时间,我一般会补三条最简测试,改完代码跑一下,比肉眼靠谱。
# test_smoke.py import pytest from app import app, db from models import Product @pytest.fixture def client(): app.config['TESTING'] = True app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' with app.app_context(): db.create_all() db.session.add(Product(barcode='6901', name='测试可乐', price=3.0, stock=100)) db.session.commit() yield app.test_client() def test_login_page(client): assert client.get('/login').status_code == 200 def test_checkout_reduces_stock(client): resp = client.post('/checkout', json={ 'items': [{'product_id': 1, 'qty': 2}] }) assert resp.get_json()['code'] == 0 p = Product.query.get(1) assert p.stock == 98 def test_checkout_insufficient_stock(client): resp = client.post('/checkout', json={ 'items': [{'product_id': 1, 'qty': 999}] }) assert resp.get_json()['code'] == 1逻辑说明:用内存库跑测试,不碰真实数据。第一条测页面能开,第二条测扣库存,第三条测库存不足拦截。参数上TESTING=True让 Flask 把异常抛出来而不是吞掉,方便定位。这三条过了,基本功能就不会有大毛病。从那以后我每次改完收银相关代码,都强制先跑一遍冒烟测试再手动点,省得来回翻车。希望帮到你。
本文还有配套的精品资源,点击获取