ARTICLE DETAIL

资讯详情

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

基于Python+SQLite的超市商品管理系统:进销存与预警实战

基于Python+SQLite的超市商品管理系统:进销存与预警实战 1. 项目背景与整体设计思路今年年初接了一个挺有意思的项目凯特生活超市要上一套商品管理系统项目归档编号叫hx3940。这家店不算大但SKU已经堆到两千多个之前全靠Excel表格记账采购、库存、价格经常对不上。店长想找个便宜的方案我直接推荐了Python——不为别的开发快、部署轻、需求变动时改起来不心疼。这套系统最后只用了大概两周时间就交付了核心代码量也不大但把超市日常最头疼的进销存问题基本都兜住了。如果你正在考虑给中小型超市、便利店、社区团购门店做类似的管理工具或者你本身就是Python新手想练一个完整项目这篇文章应该能给你一套可以直接抄作业的参考。我会从需求拆解、数据库设计、核心代码实现、界面布局、踩坑排查一条线讲下来代码都是实际跑过的不是教学Demo那种花架子。后面也会把仓库编号hx3940里沉淀下来的几个设计取舍讲清楚很多细节常规文档里根本不会写。1.1 为什么是Python来做超市商品管理很多人一听到“商品管理系统”第一反应是Java MySQL再挂个Spring Boot。但凯特生活超市的实际条件摆在那里店里就一台Windows收银主机配置一般没有专职IT预算有限需求还会随时变。这种情况下Java那套完整链路的学习成本、部署成本和改造成本完全没必要。Python配合自带的tkinter做桌面界面加上SQLite做本地数据库一个程序文件装到哪儿都能跑数据还能用U盘随时备份。选型的时候我还建了个对比表直接把几个候选方案摆给店长看方案开发效率部署成本适合场景缺点Python tkinter SQLite高低免安装依赖单机桌面管理、中小门店不适合高并发Web场景Java MySQL Spring中低高需要JDK和数据库服务大型连锁或云端部署轻量场景杀鸡用牛刀C# WinForm SQL Server中中需要.NET环境Windows生态场景跨平台能力弱PHP MySQL中中需要Web服务器Web化管理桌面收银场景不契合核心判断标准是“瓶颈在哪”。超市商品管理瓶颈从来不是每秒并发几千请求而是业务流程是否顺畅、数据能否对得上。Python足够胜任而且遇到要加功能比如扫码枪、Excel导入、批量改价的时候改起来是真快。1.2 功能模块划分从Excel表格到系统化的核心转变先说说接需求时的调研过程。我蹲在店里看了半天发现他们每天的操作链路大概是这样的早上理货员把新进的货搬到仓库采购拿着进货单登记营业员卖货时用手工记账本记录下班后把当天销售额填到Excel里。最要命的是过期商品——饮料和零食堆在货架上没人知道哪些快到期了只能靠定期翻物理保质期。所以我把需求整理成了八个模块覆盖从商品进店到销售出库的完整生命周期商品档案管理维护条码、名称、规格、单位、分类、进价、售价、预警库存分类管理支持二级分类方便按品类统计供应商管理记录供货商联系方式、供货品类入库管理采购入库时登记数量、进价、供应商、生产日期、到期日期出库管理销售出库或报损出库时扣减库存库存台账实时查询每个商品的当前库存、货值预警中心低库存预警和临期商品预警报表导出把查询结果导出成Excel方便店长做经营分析另外还加了个简单的登录权限管理员能增删改商品普通店员只能做入库和查询。这种权限模型在单机应用里已经够用了后面如果要扩展多门店再在users表加个role字段扩展就行。1.3 技术选型与架构分层技术栈用得很克制Python 3.10、tkinter、sqlite3、openpyxl。没有引入重量级GUI框架理由很简单——店里的机器装PyQt5或PySide2哪怕是Composer版本也要几百MB而tkinter是Python自带的环境装上就能跑零额外依赖。这个决定在打正式包的时候帮了大忙PyInstaller打包出来的exe体积只有十几MB。代码层面我做了简单的三层分离不搞花活但足够后期维护hx3940/ │ ├── main.py # 程序入口初始化数据库并启动主窗口 ├── db.py # 数据库连接与建表 ├── dao.py # 数据访问层所有SQL封装 ├── service.py # 业务逻辑层校验、预警、事务操作 ├── views/ │ ├── main_window.py # 主窗口布局 │ ├── product_view.py # 商品管理界面 │ ├── stock_view.py # 入库出库界面 │ └── report_view.py # 报表与预警界面 │ ├── utils/ │ ├── excel_export.py # Excel导出封装 │ └── path_helper.py # 打包后路径处理 │ └── data/ └── market.db # SQLite数据库文件首次运行生成这份目录结构我建议你直接照搬。很多新手做项目喜欢把所有代码堆在一个文件里看起来很短但改几天就想重写。分层之后界面出问题只改viewsSQL写错只改dao业务规则变了只改service排查效率高一个数量级。2. 数据库设计与核心表结构数据库是一个管理系统的地基。hx3940这个项目里我总共设计了六张表商品表、分类表、供应商表、入库流水表、出库流水表、用户表。表不多但每一张的字段都花过心思有几个坑是跟超市老板聊了需求之后才发现的。2.1 商品表设计条码、规格与价格字段的坑商品表是核心中的核心建表SQL如下CREATE TABLE IF NOT EXISTS product ( id INTEGER PRIMARY KEY AUTOINCREMENT, barcode TEXT UNIQUE NOT NULL, name TEXT NOT NULL, category_id INTEGER, spec TEXT DEFAULT , unit TEXT DEFAULT 件, purchase_price REAL DEFAULT 0, sale_price REAL DEFAULT 0, stock INTEGER DEFAULT 0, min_stock INTEGER DEFAULT 10, produce_date TEXT, expiry_date TEXT, supplier_id INTEGER, status INTEGER DEFAULT 1, create_time TEXT DEFAULT (datetime(now, localtime)) ); CREATE INDEX IF NOT EXISTS idx_product_name ON product(name); CREATE INDEX IF NOT EXISTS idx_product_category ON product(category_id);这里有几个关键决策全是实际踩完坑后的教训第一barcode字段用TEXT而不用INTEGER。商品条码EAN-13虽然是13位纯数字但有些内部码比如生鲜称重码可能以0开头转成整数类型会直接把前导零丢掉。更离谱的是少部分门店的散装商品条码里还有字母用到INTEGER直接报错。所以一律用TEXT加UNIQUE约束最稳。第二价格字段用REAL浮点数还是用整数分存储。我很早就知道浮点金额会有精度问题比如1.1 2.2在Python里会得到3.3000000000000003。考虑到这家店最复杂的运算也就是加加减减而且sqlite3本身存REAL也能保证常规业务不翻车我在代码里统一用round(value, 2)做了处理。如果你想做一个更严谨的方案可以把价格全部改成以“分”为单位的整数这在高并发电商系统里是标配但单机超市管理系统没必要过度设计。第三过期日期用TEXT存成“YYYY-MM-DD”格式。SQLite里没有真正的日期类型日期要么存成TEXT要么存成数字时间戳。我选择ISO格式字符串因为这种格式可以直接用字符串比较大小2024-01-01 2024-06-01 成立做临期筛选时一条SQL就搞定没必要转来转去。2.2 供应商表与分类表供应商和分类是商品的两大外挂维度。供应商表相对简单CREATE TABLE IF NOT EXISTS supplier ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, contact_person TEXT, phone TEXT, address TEXT, remark TEXT, status INTEGER DEFAULT 1 );分类表我特意做成了支持树形结构的因为超市分类很有讲究一级分类可能是“酒水”“休闲食品”“日化”下面还要细分成“白酒”“饮料”“膨化食品”之类。建表时加一个parent_idCREATE TABLE IF NOT EXISTS category ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, parent_id INTEGER DEFAULT 0 );parent_id为0表示一级分类否则是某一级分类下的子分类。不直接做固定两层是因为每家店的商品结构不一样以后出现三级分类也能兼容。实际做统计时报表里只取一级分类做汇总二级分类用于界面筛选逻辑很清晰。2.3 流水表设计入库单、出库单与库存台账流水表是整个系统里最不能省的部分。很多新手只建一张商品表直接在商品表里改库存数字改完就完了。这种设计最大的问题是三个月后对不上账根本不知道某批货是什么时候进的、从哪个供应商来的、进价多少。超市跟供应商对账是刚需全靠流水表撑起来。入库流水表CREATE TABLE IF NOT EXISTS stock_in ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL, quantity INTEGER NOT NULL, price REAL NOT NULL, supplier_id INTEGER, operator TEXT, in_time TEXT DEFAULT (datetime(now, localtime)) );出库流水表的结构类似字段是product_id、quantity、price、sale_type销售/报损、operator、out_time。如果以后要精细统计毛利出库流水表里的price字段就是售价可以跟商品表里的进价做减法。流水表设计还有一个细节我并没有在流水表里冗余“操作后库存”这个字段。因为要还原任意时间点的库存只需要把该商品所有入库数量减去所有出库数量就能算出来。但如果你的业务需要做库存走向曲线图可以再加一个balance字段每次操作后把最新库存写进去查历史又快又方便。这个属于可选优化我后面版本会根据店长的图表需求决定是否加上。3. 核心功能实现与实操代码拆解这一章直接上代码做过这个系统的人普遍反馈“照着写一遍把业务理解透胜过看十遍教程”。3.1 数据库初始化与连接工具类数据库这块用的是sqlite3官方库。这里有个非常容易被忽略的配置sqlite3默认返回的每一行是元组只能靠下标取值代码维护一大就容易乱。所以我在封装连接函数时设置了row_factoryimport sqlite3 def get_conn(): conn sqlite3.connect(data/market.db, timeout10) conn.row_factory sqlite3.Row # 让查询结果支持[name]方式访问 conn.execute(PRAGMA journal_mode WAL) conn.execute(PRAGMA foreign_keys ON) return conntimeout10的意思是如果数据库正被其他线程占用写入最多等10秒再抛异常。这个参数在桌面程序里很实用因为用户可能同时在界面里点了好几个按钮容易撞锁。建表放在db.py里程序启动时先执行一次初始化。用IF NOT EXISTS建表老数据库不会覆盖升级版本时也不会丢数据。这个习惯建议所有Python项目都养成生产环境下删表重建的下场谁经历谁知道。3.2 商品的增删改查实现商品管理的核心操作是增删改查。这里我放一个比较完整的添加商品函数把校验逻辑也一起展示因为业务系统里校验比INSERT本身更重要def add_product(barcode, name, category_id, spec, unit, purchase_price, sale_price, min_stock, supplier_id, produce_date, expiry_date): # 基础校验 if not barcode or not barcode.strip(): return False, 条码不能为空 if not name or not name.strip(): return False, 商品名称不能为空 if sale_price 0: return False, 售价必须大于0 if min_stock 0: return False, 预警库存不能为负数 conn get_conn() try: # 条码查重 row conn.execute(SELECT id FROM product WHERE barcode ?, (barcode,)).fetchone() if row: return False, 该条码已存在 conn.execute( INSERT INTO product (barcode, name, category_id, spec, unit, purchase_price, sale_price, stock, min_stock, produce_date, expiry_date, supplier_id) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?), (barcode.strip(), name.strip(), category_id, spec, unit, purchase_price, sale_price, min_stock, produce_date, expiry_date, supplier_id) ) conn.commit() return True, 添加成功 except Exception as e: conn.rollback() return False, str(e) finally: conn.close()注意这里用了带?的占位符写法。有人喜欢用f-string拼接SQL新手看看就好千万别学——用户输入里如果带了单引号或者百分号直接拼进SQL会把语句搞坏这属于经典的SQL注入漏洞哪怕单机程序也该防着。参数化是底线。修改和删除逻辑类似。删除这里有个业务逻辑如果商品已经有了入库或出库流水我禁止物理删除只把status改为0做停用。否则过几个月翻流水表里面关联的商品ID找不到名字历史数据就残了。这个决策后来帮了店长一个大忙因为他们盘库存时必须回溯三个月前的供货记录。3.3 入库出库与库存联动的原子操作入库操作最大的坑是“只改了库存表忘了写流水表”或者反过来两边没对上。解决这个问题的唯一办法是事务。把“更新库存”和“写流水”放在同一个事务里要么都成功要么都回滚。入库函数核心代码如下def stock_in(product_id, quantity, price, supplier_id, operator): if quantity 0 or price 0: return False, 数量和价格必须大于0 conn get_conn() try: # 检查商品是否存在 product conn.execute(SELECT * FROM product WHERE id ? AND status 1, (product_id,)).fetchone() if not product: return False, 商品不存在或已停用 # 同一事务内加库存 写入库流水 with conn: conn.execute(UPDATE product SET stock stock ? WHERE id ?, (quantity, product_id)) conn.execute( INSERT INTO stock_in (product_id, quantity, price, supplier_id, operator) VALUES (?, ?, ?, ?, ?), (product_id, quantity, price, supplier_id, operator) ) return True, 入库成功 except Exception as e: return False, str(e) finally: conn.close()出库逻辑换成UPDATE product SET stock stock - ?但执行前要先查当前库存如果当前库存小于出库数量直接给提示“库存不足”。这里有个小细节查询当前库存和扣减库存之间虽然是两个SQL但在with conn这个事务里SQLite的写锁会确保不会出现两个线程同时扣同一批货把库存扣成负数的情况。出库之后我还做了一次低库存预警判断如果新库存小于等于min_stock返回的提示信息会变成“出库成功请注意该商品库存已低于预警值”。这样店员在收银时不用专门跑去看预警中心操作动作本身就带了反馈。3.4 预警查询与Excel导出预警中心是整个系统里店长最满意的功能。一条SQL就能把该补货的商品筛出来def get_low_stock_products(): conn get_conn() rows conn.execute( SELECT id, barcode, name, stock, min_stock FROM product WHERE status 1 AND stock min_stock ORDER BY stock ASC ).fetchall() conn.close() return [dict(row) for row in rows]临期预警稍微复杂一点def get_expiring_products(days30): conn get_conn() threshold (datetime.now() timedelta(daysdays)).strftime(%Y-%m-%d) rows conn.execute( SELECT id, barcode, name, expiry_date, stock FROM product WHERE status 1 AND expiry_date ! AND expiry_date ? ORDER BY expiry_date ASC, (threshold,) ).fetchall() conn.close() return [dict(row) for row in rows]这个函数的巧妙之处在于因为我们的expiry_date是YYYY-MM-DD格式的TEXT所以可以用字符串比较直接筛选出30天内过期的商品。Python端用datetime计算阈值日期避免了SQLite日期函数在不同平台上时区不一致的坑。Excel导出用的是openpyxl这是我强烈推荐的一步——店长太需要把数据导出来做进一步分析或者发到工作群里了from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment def export_products_to_excel(rows, filepath): wb Workbook() ws wb.active ws.title 商品列表 headers [条码, 名称, 分类, 规格, 库存, 进价, 售价, 生产日期, 过期日期, 预警库存] ws.append(headers) header_fill PatternFill(start_color4472C4, end_color4472C4, fill_typesolid) header_font Font(boldTrue, colorFFFFFF) for cell in ws[1]: cell.fill header_fill cell.font header_font cell.alignment Alignment(horizontalcenter) for r in rows: ws.append([r.get(barcode, ), r.get(name, ), r.get(category_name, ), r.get(spec, ), r.get(stock, 0), r.get(purchase_price, 0), r.get(sale_price, 0), r.get(produce_date, ), r.get(expiry_date, ), r.get(min_stock, 0)]) # 自动调整列宽 for col in ws.columns: max_len max(len(str(cell.value or )) for cell in col) ws.column_dimensions[col[0].column_letter].width max_len 4 # 冻结首行 ws.freeze_panes A2 wb.save(filepath)这套导出逻辑里最实用的是冻结首行和自动列宽。导出后的Excel直接在手机上打开也清晰店长不需要研究格式直接拿去用。导出的数据量控制在几千行内没任何压力真正上万行的商品库更适合扔到数据库里查询而不是导Excel。4. 界面设计与用户体验打磨很多开发者写管理系统的通病是重功能轻界面觉得“能把数据存进去就行”。但实际交付时店长的反馈几乎全在界面上。hx3940这个项目里界面做得顺手用户就愿意天天用做得别扭用户宁可回去抄Excel。4.1 界面布局Tkinter也可以做出能用的后台tkinter虽然没有现代GUI框架那么好看但只要布局得当完全能用。我的主窗口布局是典型的“左侧导航 右侧内容区”结构左侧用一排tkinter.Button做功能导航商品管理、入库、出库、预警中心、报表导出右侧用Frame作为内容容器点击不同按钮时切换对应的Frame显示底部放一个状态栏Label实时显示“当前操作人XXX”和“操作提示XXX”顶部放全局搜索框输入条码或名称关键词后按回车直接查询窗口尺寸我设置成1280x800默认全屏居中打开。这个尺寸下商品表格能完整显示十列信息店员操作时不用一直拖横向滚动条体验会好很多。字体方面使用微软雅黑12号太小的字号在店里反光屏上真的看不清。所有操作按钮统一用大按钮样式宽度16个字符高度2行文字用“添 加”“修 改”“删 除”这种带空格的写法。这样做的好处是按钮看起来不那么局促点击目标也更大减少误触概率。4.2 Treeview表格的选中回填与条件高亮商品列表我用的是ttk.Treeview控件它自带表格头排序、多选、滚动条这些能力比滚动文本框强太多。有一个细节Treeview本身不支持单元格合并也不容易做不同行不同底色但可以通过tag给行加颜色。低库存行用红色高亮临期商品用黄色高亮这个功能花了几行代码就实现了tree.delete(*tree.get_children()) for row in rows: if row[stock] row[min_stock]: tags (lowstock,) elif row[expiry_date] and row[expiry_date] threshold_30d: tags (expiring,) else: tags () tree.insert(, end, values( row[barcode], row[name], row[category_name], row[stock], row[purchase_price], row[sale_price], row[expiry_date] ), tagstags) tree.tag_configure(lowstock, background#FFC7CE, foreground#9C0006) tree.tag_configure(expiring, background#FFEB9C, foreground#9C6500)每次刷新列表前先delete(*tree.get_children())清空再动态插入。几千行数据时这样做会有点慢我在界面上做了分页控制默认只加载1000行底部显示“共XX条记录当前显示前1000条”。店员平时用搜索框输入条码精确查询居多列表刷新慢的问题实际影响不大。双击表格行自动回填到表单是刚需。Treeview支持bind( )事件拿到选中的行后把每列值重新填到上面的Entry和Combobox里。编辑完点保存UPDATE语句按id去更新。这里需要额外注意双击选中的行如果被插入了中间光标焦点要重新设置到编辑按钮上不然用户敲键盘会误以为没反应。4.3 搜索交互与扫码枪的接入搜索框是整个系统里使用频率最高的控件因为超市里很多商品都有条码店员拿到货第一步必然是扫条码。扫码枪本质上是个USB键盘模拟设备扫一下就相当于快速打出一串数字再加一个回车键。在tkinter里实现扫码枪输入其实非常简单把焦点锁定在搜索框Entry上扫码后自动输入条码再触发回车事件。关键代码就一行绑定search_entry.bind(Return, lambda e: perform_search())perform_search里先精确匹配条码匹配不到再模糊匹配名称。扫码枪扫出来的条码一定是精确匹配所以只要数据库里有这条商品界面立刻定位到对应行并高亮。这套交互流程实测很顺手——店员拿着扫码枪对着商品一扫界面马上弹出库存和价格全程不用碰键盘。有一个容易踩的坑是生鲜称重标签。称重机上打印的通常是店内码内部编码规则类似于“20开头的称重码”跟普通条码完全不一样。如果直接把整段店内码拿去查商品肯定是查不到的。我在系统里做了个特殊判断如果条码以20开头且长度为18位就解析出后面的重量和价格信息否则才走商品条码精确匹配。这个坑大多数通用软件不会处理但超市项目一定会遇到。5. 常见问题与排查技巧实录项目上线后总会有各种奇奇怪怪的问题这里整理了几个hx3940开发过程中最典型、最容易把新手卡死的点每条都是血泪换来的经验。5.1 中文乱码问题中文乱码是Python桌面程序最常见的坑表现多种多样界面上的按钮文字变成方块、导出Excel里的商品名变成乱码、SQLite里存储的中文查出来是问号。排查顺序我建议这样代码文件开头确认是# -- coding: utf-8 --Python3虽然默认utf-8但加上无害控制台或日志输出用print时终端编码可能不是utf-8Windows下可以先设置sys.stdout.reconfigure(encodingutf-8)SQLite连接后设置conn.text_factory str确保不因编码问题返回bytesopenpyxl导出Excel本身没这个问题因为xlsx文件格式就是utf-8内嵌如果导出的是csv用utf-8-sig编码否则Excel打开会乱码还有个隐藏坑SQLite的TEXT字段本身不限制编码但是如果你用sqlite3命令行工具或其它旧工具看过数据再回写到应用里可能已经破坏了编码。最稳妥的办法是从一开始就在连接层统一用utf-8中途不要混用工具。5.2 SQLite的操作锁问题上线第一周店长反馈“泪点来了库存录到一半就报database is locked。”当时我脑子飞快扫过几个原因入库窗口和商品查询窗口共用了同一个数据库连接两个线程同时写入导致文件锁冲突。SQLite的并发能力很弱只支持一个写者。虽然tkinter是单线程GUI但如果你用了后台线程刷新数据或者处理Excel导出就可能同时摸到同一个连接。我的解决方案是三条原则每个线程都调用get_conn()新建独立连接用完立刻close绝不共享连接所有写操作统一在service层做不散落在界面代码里连接时加上timeout10并开启WAL模式WALWrite-Ahead Logging模式能明显改善读写并发在SQLite 3.8之后都支持。开启后读操作不会被写操作阻塞写操作排队等锁的概率也小很多。实测下来单机几百个操作毫无压力。5.3 日期与时间比较的坑有一段时间店长说明明有商品临期了预警却查不出来。我排查后发现是数据录入时的日期格式不统一早先店员用Excel导入的数据里日期有的写“2024/3/1”有的写“2024-03-01”还有的写“2024.3.1”。这三种格式在SQLite里全是合法TEXT字符串比较结果却完全混乱。解决思路分两步第一步在入库函数里强制统一格式用datetime.strptime解析后重新格式化为YYYY-MM-DD解析失败就拒绝录入并提示“日期格式不正确”第二步对已有脏数据写一个清洗脚本用正则统一替换。清洗脚本跑完后再验证一遍比较结果问题就消失了。还有个细节SQLite的date(now)返回的是UTC日期在你存储的时区是东八区时可能会相差一天。所以我代码里从来不依赖SQLite的日期函数一律用Python端datetime.now().strftime(%Y-%m-%d)生成日期再传进去时间逻辑完全交给Python控制数据库只做存储。5.4 打包exe的路径问题用PyInstaller把程序打包成exe给店里用是最后一步。很多新手踩过这个坑直接在开发环境里跑代码一切正常打包成exe后双击运行数据文件写不进去或者读不到。原因很简单PyInstaller运行时会把资源文件解压到一个临时目录而你的代码如果用相对路径data/market.db去连接数据库实际指向的是临时解压目录而不是exe所在目录。程序退出后临时目录被清空软件就变成“重启后数据全没了”这是毁灭性故障。正确的连接逻辑是基于可执行文件所在路径来拼接数据库路径import sys, os def get_app_dir(): if getattr(sys, frozen, False): # 打包成exe后当前目录是exe所在目录 return os.path.dirname(sys.executable) else: # 开发模式下当前目录是项目根目录 return os.path.dirname(os.path.abspath(__file__)) DB_PATH os.path.join(get_app_dir(), data, market.db) def get_conn(): os.makedirs(os.path.dirname(DB_PATH), exist_okTrue) conn sqlite3.connect(DB_PATH, timeout10) # ...开发模式下用__file__定位打包后用sys.executable定位两者路径不同但都能落到正确位置。同时os.makedirs保证data目录不存在时自动创建避免首次启动因为目录不存在而报错。打包命令也顺手记录一下pyinstaller -F -w -n HX3940 main.py加了-w隐藏控制台窗口避免店员不小心关掉黑窗口导致程序退出。6. 延伸扩展从能用走向好用系统交付后店长用了半个月给反馈提了一批很有价值的后续需求。这一章聊几个我已经在规划或者已经开始改的方向思路比代码更重要。6.1 扫码枪、生鲜称重标签与电子价签的接入扫码枪在第四章已经讲过接入原理本质上就是键盘输入。但后面要做的生鲜称重标签解析更值得展开。称重柜台打印的标签条码通常是“20 6位商品编码 5位重量 4位价格 校验位”的结构比如20 001234 01250 00999就是商品编码001234、重量1.250kg、价格9.99元。我准备在新增的一个解析函数里做两件事一是把称重标签的条码分隔成商品编码和销售临时价二是根据编码查库如果临时价和库内售价不同这种情况在促销时很常见系统自动弹窗提示“促销价格与基础售价不一致是否以临时价出库”。这个功能对于连锁超市来说几乎是刚需但市面上小软件很少做得到。6.2 多门店、权限与数据同步如果后面门店数量扩大当前单机SQLite架构肯定要改。我的过渡方案是先在所有业务表里加store_id字段再给每个门店配一个本地实例每天闭店后把当天入库出库流水导出成JSON文件通过共享网盘或U盘传到总部总部统一落地到汇总库里。这个方案的优点是改动量小不需要立刻上MySQL或云数据库缺点是做不到实时同步。等门店数超过五家我会直接推荐迁移到MySQL FastAPI后端前端还是保留tkinter但数据库连接改成通过API访问。这里的技术栈选择不复杂重点是数据模型仍然是那几张表核心业务逻辑不用大改。6.3 数据可视化与经营分析店长最关心的是三件事今天卖了多少、哪个商品卖得最差、毛利是多少。目前hx3940已经能根据出库流水简单汇总销售额但离真正的经营分析还差一层。我计划用matplotlib画折线图展示近三十天销售额趋势用饼图展示各品类销售占比用条形图排出滞销商品的Top20。这里要提前处理一个坑matplotlib默认字体不支持中文画出来的图全是方块必须在代码里设置import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False这个字体设置我每次都会被坑一次写在项目配置里才能一劳永逸。另外饼图和条形图的颜色最好直接用色盲友好的配色店长不一定分辨得出相近的颜色。6.4 项目复盘心得hx3940交付以后我自己复盘了一遍最大的收获不是技术细节而是“业务流程优先于技术炫技”这个判断。最开始我想用PyQt5做出很漂亮的界面想接入二维码识别、语音播报这些花活后来全部砍掉了只保留最不影响流程的核心功能。最后店里真正高频使用的也就是扫码查询、入库、出库、预警和Excel导出这五个功能。另外一点是“测试用例要用真实数据”。开发时我自己造的商品名全是“测试商品1”“测试商品2”条码也是随便编的。后来把店里真实的商品Excel导进去一试立刻暴露出单位不统一、日期格式混乱、条码位数不同这些问题。真实数据远比精心设计的测试数据靠谱建议所有做业务系统的同行在开发阶段就向客户要一份脱敏后的真实数据用来联调。还有个容易被忽视的点上线之后头两周要保持随叫随到。店员的操作习惯跟开发者的设想完全不同他们会双击、但也会误点右键会一次入库自动连点两次甚至会直接把Excel表格内容复制粘贴到输入框引起解析错误。我在前两周远程排查了几乎所有问题后来把一些误操作直接在前端做了容错处理比如入库按钮点击后立即disabled一秒防止连点造成重复入库。这种问题在代码审查里根本发现不了只有实际用户用起来才能暴露。
返回列表