简介这是一份面向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 是稳的如果源码里写的是Flask1.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(usernameadmin).first(): admin User(usernameadmin, passwordadmin123, roleadmin) db.session.add(admin) # 基础分类 for name in [饮料, 零食, 日用品]: if not Category.query.filter_by(namename).first(): db.session.add(Category(namename)) 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_keyTrue) barcode db.Column(db.String(32), uniqueTrue, nullableFalse) name db.Column(db.String(64), nullableFalse) price db.Column(db.Float, nullableFalse) # 售价 cost db.Column(db.Float, default0.0) # 进价用于算毛利 stock db.Column(db.Integer, default0) alert_stock db.Column(db.Integer, default10) # 低于此值预警 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(totaltotal, discountdiscount, pay_amountpay_amount, member_idmember_id) db.session.add(order) db.session.flush() # 拿到 order.id for p, qty in order_items: p.stock - qty db.session.add(OrderItem(order_idorder.id, product_idp.id, qtyqty, pricep.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-01MySQL 下同样可用。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连接串加?charsetutf8SQLite 本身默认 UTF-8乱码多半是源码文件用了 GBK 保存或者模板渲染时没声明charset。检查templates/base.html里有没有meta charsetUTF-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(usernameadmin, passwordgenerate_password_hash(admin123), roleadmin) # 登录校验时 user User.query.filter_by(usernameform.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(barcode6901, name测试可乐, price3.0, stock100)) 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逻辑说明用内存库跑测试不碰真实数据。第一条测页面能开第二条测扣库存第三条测库存不足拦截。参数上TESTINGTrue让 Flask 把异常抛出来而不是吞掉方便定位。这三条过了基本功能就不会有大毛病。从那以后我每次改完收银相关代码都强制先跑一遍冒烟测试再手动点省得来回翻车。希望帮到你。本文还有配套的精品资源点击获取