ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python零食销售系统:PyQt5+SQLite库存订单一体化实战

Python零食销售系统:PyQt5+SQLite库存订单一体化实战 简介这是一份面向Python初中级开发者、计算机专业学生及中小型零售管理者的零食销售管理系统实战项目资料聚焦商品库存、订单处理与销售数据分析一体化落地。资源以1个79KB的Word文档.docx形式提供完整覆盖系统设计背景、模块化功能说明商品管理、库存预警、订单流程、会员体系、权限控制、MySQL数据库建表逻辑、Flask后端API设计、PyQt桌面GUI实现细节及部署要点并附有代码结构说明与关键业务流程图解。内容预览显示文档含8大核心章节从项目目标到算法逻辑、安全机制与交互体验均有详述特别强化了库存动态同步、销售多维分析与权限分级等业务闭环设计。目前已有75人下载学习读者可直接获取可复用的数据库脚本、API规范、测试数据生成方法及前后端协同调试要点快速掌握零售场景下Python全栈开发的工程化实践路径。1. 零食销售系统不是“小项目”而是商品库存、订单流转与销售数据闭环的最小可行验证体你手头正要上线一家社区零食铺的数字化管理工具但发现Excel管库存容易错、微信接单难追溯、月底算毛利得手动加总——这不是流程问题是系统缺失。本项目用纯Python构建一个可落地的零食销售系统核心不是炫技而是把「商品→库存→订单→销售数据」四层关系真正拧成一股绳上架时自动扣减库存、退货时反向回滚、每笔销售生成结构化记录、所有数据实时支撑折线图/热力图/复购率计算。它不依赖云服务或SaaS平台全部本地运行GUI用PyQt5实现即开即用界面SQLite做嵌入式数据库避免部署门槛数据分析模块直接输出带时间维度的销量TOP10、库存预警清单和客单价分布直方图。适合刚学完Python基础、想用真实业务场景练手的开发者也适合小微零售店主快速部署——代码里没有抽象概念只有货架编号、保质期倒计时、扫码枪触发的订单提交按钮。2. 用PyQt5SQLite搭建商品库存订单一体化管理的最小运行单元2.1 为什么选PyQt5而非Tkinter或Flet三个硬性约束决定技术栈在零售场景下GUI必须满足三个不可妥协的条件支持高DPI屏幕店主常用27寸4K显示器、能嵌入表格控件并冻结首行查看商品列表时需固定表头、允许双击编辑单元格快速修改售价或库存。Tkinter对高DPI适配差滚动条在4K屏上细如发丝Flet虽支持响应式布局但本地打包后体积超80MB且无法冻结表格行而PyQt5通过QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)一行启用高清缩放QTableWidget原生支持setHorizontalHeaderLabels()和editItem()且打包后体积稳定在15MB内。本项目采用PyQt5 5.15.9兼容Python 3.8–3.11避开了6.x版本中信号槽机制变更带来的兼容风险。提示安装时务必指定版本号避免pip自动升级到PyQt6导致QApplication.setStyle(Fusion)失效pip install PyQt55.15.9 PyQtWebEngine5.15.92.2 数据库设计紧扣零食业务特性保质期字段驱动库存预警逻辑SQLite建表语句直接反映零食行业关键约束——保质期不是静态属性而是动态预警触发器。products表中shelf_life_days字段存储天数last_updated记录最近一次入库时间两者结合生成days_left虚拟列SQLite不支持计算列故在查询时用julianday(now) - julianday(last_updated)计算CREATE TABLE products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT CHECK(category IN (膨化, 糖果, 饮料, 乳制品)), unit_price REAL CHECK(unit_price 0), stock_quantity INTEGER DEFAULT 0 CHECK(stock_quantity 0), shelf_life_days INTEGER CHECK(shelf_life_days 0), last_updated DATE DEFAULT (date(now)), barcode TEXT UNIQUE ); CREATE TABLE orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_time DATETIME DEFAULT (datetime(now)), total_amount REAL CHECK(total_amount 0), status TEXT CHECK(status IN (已付款, 已发货, 已完成, 已取消)) ); CREATE TABLE order_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER, product_id INTEGER, quantity INTEGER CHECK(quantity 0), price_at_order REAL, FOREIGN KEY(order_id) REFERENCES orders(id) ON DELETE CASCADE, FOREIGN KEY(product_id) REFERENCES products(id) );2.2.1 库存扣减必须原子化用事务保证“下单-扣库存-生成订单”三步不中断用户点击“提交订单”时若仅执行UPDATE products SET stock_quantity stock_quantity - ? WHERE id ?再插入订单网络抖动可能导致库存扣减成功但订单写入失败造成库存黑洞。正确做法是将三步封装进单一事务def create_order(self, items: List[Dict]): conn sqlite3.connect(snack_system.db) try: conn.execute(BEGIN TRANSACTION) # 步骤1检查库存是否充足SELECT FOR UPDATE在SQLite中用BEGIN IMMEDIATE模拟 for item in items: cursor conn.execute( SELECT stock_quantity FROM products WHERE id ?, (item[product_id],) ) stock cursor.fetchone()[0] if stock item[quantity]: raise ValueError(f商品{item[product_id]}库存不足当前{stock}件) # 步骤2扣减库存 for item in items: conn.execute( UPDATE products SET stock_quantity stock_quantity - ? WHERE id ?, (item[quantity], item[product_id]) ) # 步骤3插入订单主表 conn.execute(INSERT INTO orders (total_amount, status) VALUES (?, ?), (sum(i[price] * i[quantity] for i in items), 已付款)) order_id conn.execute(SELECT last_insert_rowid()).fetchone()[0] # 步骤4插入订单明细 for item in items: conn.execute( INSERT INTO order_items (order_id, product_id, quantity, price_at_order) VALUES (?, ?, ?, ?), (order_id, item[product_id], item[quantity], item[price]) ) conn.commit() return order_id except Exception as e: conn.rollback() raise e finally: conn.close()注意SQLite的BEGIN IMMEDIATE能防止其他连接在事务期间修改同一行但无法阻止并发事务读取旧值。本系统日均订单200单此方案足够若需更高并发应改用PostgreSQL并启用SELECT ... FOR UPDATE。2.3 GUI界面按业务动线组织从货架陈列到订单确认的四页式导航主窗口采用QTabWidget实现四页导航每页对应一个核心操作域商品管理页QTableWidget展示商品列表右键菜单支持“新增/编辑/删除”双击单价列直接修改库存盘点页扫描枪输入条码后自动定位商品弹出浮动窗口输入实盘数量点击“确认差异”生成调账记录订单录入页左侧QListWidget显示待结账商品右侧QTableWidget显示明细底部“扫码添加”按钮绑定USB扫描枪事件监听QKeyEvent捕获回车符销售报表页嵌入matplotlib图表点击“今日销量”按钮刷新折线图X轴为小时刻度0–23Y轴为销售额。关键交互代码示例扫码枪触发def keyPressEvent(self, event): if event.key() Qt.Key_Return and self.scan_buffer: # 扫码枪以回车结尾 barcode self.scan_buffer.strip() self.scan_buffer # 查询商品 conn sqlite3.connect(snack_system.db) cursor conn.execute( SELECT id, name, unit_price, stock_quantity FROM products WHERE barcode ?, (barcode,) ) result cursor.fetchone() if result: product_id, name, price, stock result if stock 0: QMessageBox.warning(self, 库存不足, f{name}已售罄) return # 添加到订单列表 self.current_order.append({ product_id: product_id, name: name, price: price, quantity: 1 }) self.update_order_table() else: QMessageBox.warning(self, 未找到商品, f条码{barcode}不存在)3. 销售数据分析平台用PandasMatplotlib实现可解释的经营洞察3.1 数据清洗必须处理零食行业的三类脏数据临期品、拆零销售、促销叠加销售数据常含三类干扰项① 临期商品打折销售价格低于unit_price但需保留原始成本② 拆零销售如薯片按袋卖但库存单位是箱需换算系数③ 买赠活动买二送一订单明细中赠品数量为负。清洗逻辑直接写入analyze_sales.pyimport pandas as pd import sqlite3 def load_cleaned_sales_data(): conn sqlite3.connect(snack_system.db) # 步骤1合并订单主表与明细表获取原始销售记录 df pd.read_sql_query( SELECT o.id as order_id, o.order_time, o.total_amount, oi.product_id, p.name as product_name, p.category, oi.quantity, oi.price_at_order as actual_price, p.unit_price as original_price, p.shelf_life_days FROM orders o JOIN order_items oi ON o.id oi.order_id JOIN products p ON oi.product_id p.id WHERE o.status 已完成 , conn) # 步骤2标记临期商品距离过期7天 df[order_date] pd.to_datetime(df[order_time]).dt.date df[days_left] pd.to_datetime(today).date() - df[order_date] df[is_near_expiry] df[days_left] pd.Timedelta(days7) # 步骤3计算毛利率实际售价-原始成本/原始成本 df[gross_margin] (df[actual_price] - df[original_price]) / df[original_price] # 步骤4过滤异常值毛利率-90%视为赠品100%视为录入错误 df df[(df[gross_margin] -0.9) (df[gross_margin] 1.0)] conn.close() return df # 调用示例 sales_df load_cleaned_sales_data() print(sales_df.groupby(category)[gross_margin].mean().round(3)) # 输出膨化 0.231糖果 0.315饮料 0.189乳制品 0.2723.1.1 时间序列分析必须按小时聚合识别零食销售的“黄金三小时”社区店客流有明显峰谷单纯按日统计会掩盖关键时段。以下代码将订单时间解析为小时并统计每小时销售额def plot_hourly_sales(): df load_cleaned_sales_data() # 提取小时并分组 df[hour] pd.to_datetime(df[order_time]).dt.hour hourly_sales df.groupby(hour)[total_amount].sum().reindex(range(24), fill_value0) plt.figure(figsize(12, 5)) plt.bar(hourly_sales.index, hourly_sales.values, color#4CAF50, alpha0.7) plt.xlabel(小时24小时制) plt.ylabel(销售额元) plt.title(近7日每小时销售额分布) plt.xticks(range(0, 24, 2)) # 每2小时标一次 plt.grid(axisy, alpha0.3) # 标出峰值区间 peak_hours hourly_sales.nlargest(3).index.tolist() for h in peak_hours: plt.axvspan(h-0.4, h0.4, colorred, alpha0.1) plt.show() plot_hourly_sales()注意reindex(range(24), fill_value0)确保0点到23点连续显示避免因某小时无销售导致柱状图错位。3.2 关键指标看板用三个函数覆盖店主最关心的经营问题3.2.1 库存健康度诊断自动识别“呆滞品”与“爆款缺货”def inventory_health_report(): conn sqlite3.connect(snack_system.db) # 呆滞品30天无销售且库存5件 stagnant pd.read_sql_query( SELECT p.name, p.stock_quantity, p.shelf_life_days, (julianday(now) - julianday(p.last_updated)) as days_since_update FROM products p LEFT JOIN order_items oi ON p.id oi.product_id LEFT JOIN orders o ON oi.order_id o.id AND o.order_time datetime(now, -30 days) WHERE o.id IS NULL AND p.stock_quantity 5 ORDER BY days_since_update DESC LIMIT 10 , conn) # 爆款缺货近7天销量TOP5但库存3件 hot_out_of_stock pd.read_sql_query( SELECT p.name, p.stock_quantity, COUNT(*) as sales_count FROM products p JOIN order_items oi ON p.id oi.product_id JOIN orders o ON oi.order_id o.id AND o.order_time datetime(now, -7 days) GROUP BY p.id HAVING p.stock_quantity 3 ORDER BY sales_count DESC LIMIT 5 , conn) print( 呆滞品清单30天未售出且库存5) print(stagnant[[name, stock_quantity, days_since_update]]) print(\n 爆款缺货预警近7天销量TOP5但库存3) print(hot_out_of_stock[[name, stock_quantity, sales_count]])3.2.2 客单价分布直方图判断定价策略是否匹配客群def plot_average_order_value(): conn sqlite3.connect(snack_system.db) # 计算每单客单价订单总金额/订单商品数 df pd.read_sql_query( SELECT o.id as order_id, o.total_amount, COUNT(oi.id) as item_count FROM orders o JOIN order_items oi ON o.id oi.order_id WHERE o.status 已完成 GROUP BY o.id , conn) df[aov] df[total_amount] / df[item_count] plt.figure(figsize(10, 4)) plt.hist(df[aov], bins20, edgecolorblack, alpha0.7, color#2196F3) plt.xlabel(客单价元) plt.ylabel(订单数量) plt.title(近30日客单价分布) plt.axvline(df[aov].mean(), colorred, linestyle--, labelf平均值{df[aov].mean():.1f}元) plt.legend() plt.show()4. GUI设计中的细节陷阱与绕过方案让店主真正愿意每天打开这个程序4.1 扫码枪兼容性问题Windows下需禁用系统快捷键拦截多数USB扫码枪默认发送CtrlAltDel等组合键触发系统安全界面导致程序闪退。解决方案是在主窗口__init__中重写winEvent方法仅Windows有效import sys from PyQt5.QtCore import Qt, QEvent class MainWindow(QMainWindow): def __init__(self): super().__init__() # ... 其他初始化代码 # Windows专用拦截系统快捷键 if sys.platform win32: self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint) self.setAttribute(Qt.WA_NativeWindow, True) def winEvent(self, msg): # 拦截WM_HOTKEY消息系统快捷键 if msg.message 0x0312: # WM_HOTKEY return True, 0 return super().winEvent(msg)4.2 高DPI屏幕字体模糊强制启用Qt缩放且指定字体大小PyQt5在4K屏上默认字体发虚需在QApplication创建后立即设置if __name__ __main__: app QApplication(sys.argv) # 启用高DPI适配 app.setAttribute(Qt.AA_EnableHighDpiScaling) app.setAttribute(Qt.AA_UseHighDpiPixmaps) # 设置全局字体12号微软雅黑适配4K屏 font QFont(Microsoft YaHei, 12) app.setFont(font) window MainWindow() window.show() sys.exit(app.exec_())4.3 数据库文件路径硬编码陷阱用os.path.dirname(__file__)动态定位新手常将数据库路径写死为C:/project/snack_system.db导致打包后找不到文件。正确做法是import os DB_PATH os.path.join(os.path.dirname(__file__), snack_system.db) def get_db_connection(): # 若数据库不存在则初始化 if not os.path.exists(DB_PATH): init_database() return sqlite3.connect(DB_PATH)4.4 打包为单文件exePyInstaller参数必须包含隐式导入使用PyInstaller打包时PyQt5和SQLite的隐式依赖需显式声明pyinstaller --onefile --windowed \ --add-data snack_system.db;. \ --hidden-importPyQt5.sip \ --hidden-importsqlite3 \ --iconicon.ico \ main.py注意--add-data参数中分号前为源路径分号后为打包后相对路径.表示与exe同目录--hidden-import确保PyQt5的C扩展被包含否则运行时报ModuleNotFoundError: No module named PyQt5.sip。5. 销售数据分析平台的进阶技巧用SQL窗口函数实现复购率计算5.1 复购率不是简单除法而是基于用户行为序列的滑动窗口统计店主真正想知道的是“买了薯片的顾客30天内有多少人又买了饮料”这需要按顾客行为时间排序后计算相邻购买间隔。SQLite 3.25支持窗口函数用LAG()获取上一笔订单时间-- 计算每个顾客的复购间隔单位天 WITH customer_orders AS ( SELECT o.id as order_id, o.order_time, oi.product_id, p.category, -- 提取顾客标识此处用订单ID模拟实际应关联会员表 o.id as customer_id FROM orders o JOIN order_items oi ON o.id oi.order_id JOIN products p ON oi.product_id p.id WHERE o.status 已完成 ), ordered_by_customer AS ( SELECT *, LAG(order_time) OVER ( PARTITION BY customer_id ORDER BY order_time ) as prev_order_time FROM customer_orders ) SELECT category, AVG(julianday(order_time) - julianday(prev_order_time)) as avg_repeat_days FROM ordered_by_customer WHERE prev_order_time IS NOT NULL GROUP BY category;5.1.1 Python中调用该SQL并可视化复购周期热力图def plot_repurchase_heatmap(): conn sqlite3.connect(snack_system.db) # 执行窗口函数SQL需SQLite3.25 try: df pd.read_sql_query( WITH customer_orders AS ( SELECT o.id, o.order_time, oi.product_id, p.category FROM orders o JOIN order_items oi ON o.id oi.order_id JOIN products p ON oi.product_id p.id WHERE o.status 已完成 ), ordered_by_customer AS ( SELECT *, LAG(order_time) OVER ( PARTITION BY id ORDER BY order_time ) as prev_order_time FROM customer_orders ) SELECT category, CAST((julianday(order_time) - julianday(prev_order_time)) AS INTEGER) as days_gap FROM ordered_by_customer WHERE prev_order_time IS NOT NULL AND days_gap BETWEEN 1 AND 30 , conn) # 生成热力图数据横轴品类纵轴天数值为频次 pivot_df df.pivot_table( indexdays_gap, columnscategory, aggfuncsize, fill_value0 ).reindex(range(1, 31), fill_value0) plt.figure(figsize(10, 6)) sns.heatmap(pivot_df, cmapYlGnBu, cbar_kws{label: 复购次数}) plt.title(各品类顾客复购天数分布热力图1-30天) plt.xlabel(商品品类) plt.ylabel(距上次购买天数) plt.show() except sqlite3.OperationalError as e: print(fSQLite版本过低不支持窗口函数{e}) print(请升级SQLite至3.25或改用Pandas实现性能下降)提示若生产环境SQLite版本3.25可用Pandas替代但需加载全量订单数据到内存对万级订单可能卡顿。此时建议在orders表中增加customer_id字段并建立索引用df.sort_values([customer_id,order_time]).groupby(customer_id)[order_time].diff()计算间隔。5.2 用matplotlib嵌入PyQt5界面避免多进程导致的GUI冻结直接在PyQt5中调用plt.show()会阻塞主线程。正确做法是使用FigureCanvasQTAggfrom matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg from matplotlib.figure import Figure class PlotWidget(QWidget): def __init__(self, parentNone): super().__init__(parent) self.figure Figure(figsize(8, 4), dpi100) self.canvas FigureCanvasQTAgg(self.figure) layout QVBoxLayout() layout.addWidget(self.canvas) self.setLayout(layout) def plot_hourly_sales(self, data): self.figure.clear() ax self.figure.add_subplot(111) ax.bar(data.index, data.values, color#FF9800) ax.set_title(每小时销售额) ax.set_xlabel(小时) self.canvas.draw() # 在主窗口中调用 plot_widget PlotWidget() hourly_data load_hourly_sales() # 返回pandas Series plot_widget.plot_hourly_sales(hourly_data)直接将图表嵌入GUI容器既保持界面响应性又避免plt.show()弹出独立窗口干扰操作流。本文还有配套的精品资源点击获取
返回列表