资讯详情

小型自选商场商品管理系统:基于Python与SQLite的库存与进销存设计

📅 2026/10/9 12:37:32 | 华诺云谱 👁 阅读
小型自选商场商品管理系统:基于Python与SQLite的库存与进销存设计
简介小型自选商场商品管理系统是一份面向计算机相关专业课程设计、毕业设计以及数据库初学者的完整实战项目。系统采用C# WinForms前端加SQL Server后端覆盖进货记录与按月统计、销售记录与日盘存月盘存、库存动态刷新、供应商信息维护以及收银台按商品编号与数量生成购物清单和收付款情况等核心环节配套库存、售货、进货、供应商四张业务表字段设计可完整还原小型商场的进销存业务链路。压缩包共189个文件体积约680KB其中60个sql文件负责建库建表、视图或存储过程及初始化数据54个cs文件实现主窗体、收银台、查询统计与后台管理逻辑24个resx与24个resources提供界面文案和资源映射另有config/exe等配置与可执行文件并附带mdf/ldf数据库文件可直接附加使用便于理解完整项目结构与快速二次开发。目前已有1025人学习下载适合用来熟悉典型进销存流程、练习C#数据库应用编程以及作为课程设计或毕业设计的参考样例。1. 小型自选商场商品管理系统先用数据库管住商品月底才对得上账小型自选商场商品管理系统解决的是小店最扎心的问题商品越来越多货架、收银台、账本三本账永远对不齐。很多店主用 Excel 记进出货记到三个月就放弃——不是不会用软件而是商品改价、进货、退货、盘点这些动作挤在一起Excel 里没有约束填错一个单元格就是一笔糊涂账。这套系统的核心就一句话把商品资料、进货、销售、库存放进同一个数据库每一次变动都留下流水让账面库存和实际货架能在同一天结算清楚。它不需要云服务器也不需要扫码枪起步一台旧电脑就能跑。适合三类人课程项目要求做完整系统的同学想给自家小店装工具的开发者以及已经在用 Excel 记账、想升级的店主。2. 先把数据模型立住商品、进货、销售三张表怎么设计才不返工拿到这个题目最常见的做法是上来就拖界面窗口画完了才发现数据没地方放或者放进去之后根本取不出来。我一般建议反着来先定表再写界面界面不过是表的投影。表设计对了后面加功能只是加 SQL 的事表错了改字段等于把系统重写一遍。下面这套模型是我在类似小项目里反复用过的四张表覆盖建档、进货、销售、盘点四个核心动作。2.1 商品表条码唯一、三个价格分开、库存直接放在商品行商品表是主数据字段不多但每个都有讲究。先看最关键的几个字段的口径字段作用口径说明barcode商品条码加 UNIQUE 约束散货可以留空但不允许非空记录重复name商品名称NOT NULL查询主要靠它category分类默认「未分类」报表按它汇总purchase_price最近一次进价成本口径用来估算毛利不代表历史成本sell_price普通售价收银默认取它member_price会员价可空为空时会员按 sell_price 结算min_stock库存预警阈值低于它就在进货提醒里列出来stock_quantity当前账面库存用 REAL 而不是 INTEGER散货按斤、按两卖时要小数三个价格为什么要分开因为进价是成本售价是收入会员价是让利口径。混在一个字段里月底算毛利的时候连成本都说不清。不少翻车现场就是这么来的促销改价改到进价上毛利直接变成负数。stock_quantity 直接放在商品表里是刻意的简化。单店、几千个 SKU 以内的规模不需要单独拆一张库存表。每次收银扣减、进货增加、盘点调整都通过流水来改这个字段既能保持简单又不会丢审计线索。2.2 两张流水表库存数字不该被直接改每一次变动都要留痕商品表里的 stock_quantity 是结果进货流水和销售流水才是原因。任何库存变动都必须先生成流水记录再更新商品表里的库存数字。这样设计的价值不是多了一张表而是当账对不上的时候你能回答三个问题这个月进了多少货、卖了多少、损耗和盘差出现在哪一批货上。只有最后的数字没有过程记录等于没有答案。purchase_items 记每一次进货包含数量、当时的进价、总金额和供应商。sale_items 记每一次销售特别注意里面存了一个 unit_price 快照——商品售价后面一定会改但历史销售记录不应该跟着改。对账、算利润、查某天卖了多少钱全部以流水为准不参考商品表的当前售价这个「快照」原则是这个系统能不能落地对账的关键。2.3 建表 SQL四张表直接落库字段口径一次说清下面是这个系统最小可用的建表 SQLSQLite 语法部署到 MySQL 时把 AUTOINCREMENT 换成 AUTO_INCREMENT 即可PRAGMA foreign_keys ON; CREATE TABLE products ( id INTEGER PRIMARY KEY AUTOINCREMENT, barcode TEXT UNIQUE, name TEXT NOT NULL, category TEXT DEFAULT 未分类, purchase_price REAL NOT NULL DEFAULT 0, sell_price REAL NOT NULL DEFAULT 0, member_price REAL, min_stock REAL DEFAULT 0, stock_quantity REAL DEFAULT 0, created_at TEXT DEFAULT (datetime(now, localtime)), updated_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE purchase_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL, quantity REAL NOT NULL, purchase_price REAL NOT NULL, total_amount REAL NOT NULL, supplier TEXT, created_at TEXT DEFAULT (datetime(now, localtime)), FOREIGN KEY (product_id) REFERENCES products(id) ); CREATE TABLE sale_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL, quantity REAL NOT NULL, unit_price REAL NOT NULL, total_amount REAL NOT NULL, is_member INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime(now, localtime)), FOREIGN KEY (product_id) REFERENCES products(id) ); CREATE TABLE stock_adjustments ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL, book_quantity REAL NOT NULL, actual_quantity REAL NOT NULL, diff REAL NOT NULL, note TEXT, created_at TEXT DEFAULT (datetime(now, localtime)), FOREIGN KEY (product_id) REFERENCES products(id) );提示SQLite 的 UNIQUE 约束允许多个 NULL 存在所以散货不填条码是合法的不用担心空值冲突。这份 SQL 的几个细节说明。barcode 加了 UNIQUE从数据库层拦住重复建档。created_at 用 datetime(now,localtime) 生成避免 UTC 时间和本地时间差 8 个小时小店报表只看本地时间。金额一律 REAL所有写入在代码里统一 round 到两位小数如果做成正式商业系统更稳的做法是以「分」为单位的 INTEGER但小型项目用 REAL 加统一精度收口就够了关键是全系统只有一个精度规则。关于进价还有一个口径要提前说清楚purchase_price 存的是「最近一次进价」不是加权平均成本。小超市想算单商品毛利用最近进价是能接受的近似想算整月实际毛利直接拿进货流水和销售流水去算。加权平均成本是可以的升级方向但第一版不要上复杂度会明显上升。3. 用 Python 和 SQLite 跑通最小系统建档校验与收银事务的实现数据模型定下来之后下一步不是写界面而是先把核心业务函数写出来。这一章用 Python 标准库 sqlite3 落代码把商品建档和收银扣库存这两个最关键的动作做成函数。界面只是换壳业务函数里的事务和校验才是系统的命根子。3.1 选型对比为什么 Python SQLite 是单店场景最省事的组合常见做法里这个题目有三种主流技术栈选择成本和边界差别很大技术栈部署成本适合规模维护难度Java MySQL需要装数据库服务、配客户端多门店、并发高较高Python SQLite标准库自带零安装单店单机、日单量几百笔低Web 前后端如 Flask Vue需要跑两个服务有远程访问需求中等我的判断是小型自选商场的典型场景是单店、单台收银电脑、SKU 几千个、日销售几百笔这个量级 SQLite 单文件数据库完全扛得住而且备份就是复制一个文件出问题好排查。Java MySQL 不是不行但对小店来说装服务、配账号、搞备份全是额外负担收益并不明显。Web 方案适合老板想远程看销售数据的情况如果只是店里用本地一个桌面程序就够了。选中 Python 还有一个现实理由sqlite3 是标准库不需要 pip 安装任何第三方包而且自带事务支持代码量小新手照着写也能跑通。下面所有函数都只依赖标准库。3.2 商品建档把校验写在业务入口而不是写在界面商品资料是第一份主数据最容易出问题的就是重复建档和价格非法。下面的 add_product 把校验收在函数入口界面再怎么花哨数据进库之前都要过这一关import sqlite3 DB_PATH shop.db def add_product(barcode, name, purchase_price, sell_price, category未分类, member_priceNone): if not name or not name.strip(): raise ValueError(商品名称不能为空) if sell_price 0: raise ValueError(售价必须大于 0) conn sqlite3.connect(DB_PATH) try: with conn: cur conn.cursor() cur.execute( INSERT INTO products (barcode, name, category, purchase_price, sell_price, member_price) VALUES (?, ?, ?, ?, ?, ?), (barcode.strip() or None, name.strip(), category, purchase_price, sell_price, member_price) ) return cur.lastrowid except sqlite3.IntegrityError: print(建档失败条码重复或数据不完整) raise finally: conn.close()这段代码里有三个值得注意的细节。第一barcode.strip() or None 把空字符串转成 NULL散货不填条码也能建档同时避免空字符串和 NULL 的语义歧义。第二所有 SQL 一律用 ? 占位符传参扫码枪扫进来的内容是外部输入绝不能拼字符串进 SQL。第三IntegrityError 拦截的是 UNIQUE 约束冲突这是重复建档的最后一道防线界面层可以提前查重但数据库层的约束才是兜底。价格校验只校验了售价大于零进价没有严格限制是因为实际业务里确实出现过供应商赠品进价为零的情况。你可以按自己的业务口径收紧但不要想当然认为进价必须大于零这个边界在真实小店会踩到。3.3 收银扣库存一个事务保证扣库存和写流水不拆家收银是系统里最核心的动作必须同时完成三件事按会员身份算出成交单价、扣减库存、写入销售流水。这三件事任何一件失败另外两件都必须撤销否则就会出现「钱收了库存没扣」或者「流水写了库存没动」这种对不上的账。SQLite 的 with conn 上下文管理器把这三步包进一个数据库事务异常时自动回滚def checkout(product_id, quantity, is_memberFalse): conn sqlite3.connect(DB_PATH) try: with conn: cur conn.cursor() cur.execute( SELECT sell_price, member_price FROM products WHERE id ?, (product_id,) ) row cur.fetchone() if row is None: raise ValueError(f商品 {product_id} 不存在) sell_price, member_price row unit_price member_price if (is_member and member_price) else sell_price total_amount round(unit_price * quantity, 2) cur.execute( UPDATE products SET stock_quantity stock_quantity - ?, updated_at datetime(now, localtime) WHERE id ? AND stock_quantity ?, (quantity, product_id, quantity) ) if cur.rowcount 0: raise ValueError(库存不足本次收银已取消) cur.execute( INSERT INTO sale_items (product_id, quantity, unit_price, total_amount, is_member) VALUES (?, ?, ?, ?, ?), (product_id, quantity, unit_price, total_amount, int(is_member)) ) # with 块正常结束自动提交异常则自动回滚 except Exception as e: print(收银失败事务已回滚:, e) raise finally: conn.close()注意with conn 在进入时开启事务正常走完自动 commit异常一路抛出去就 rollback。不需要在 except 里再写 conn.rollback()重复写反而容易产生「回滚了两次」的困惑。这段代码有两个关键点。第一扣库存的 UPDATE 带了 stock_quantity ? 条件如果库存不够数据库不会执行更新cur.rowcount 返回 0代码主动抛异常。这就是防止库存变成负数的最后一道闸门比先 SELECT 再判断更可靠因为 SELECT 和 UPDATE 之间有并发窗口。第二unit_price 在写入 sale_items 时做了一次快照把成交单价存进流水。之后商品调价历史销售记录不受影响月底对账只认流水。4. 从能跑到能用进货、会员价与盘点三个老板一定会问的功能收银跑通只是系统能用了小店能不能真正用起来取决于三个高频场景进货怎么入库、会员和促销怎么处理、月底盘点怎么调账。这三个功能如果不在第一版做进去老板用两周就会退回 Excel。这一章逐个补齐。4.1 进货入库先写进货流水再改商品表成本和库存才能同时更新进货的动作是两件事往 purchase_items 插入一条进货记录同时把商品表的 stock_quantity 增加对应数量。次序有讲究先插流水再改库存一旦中间出错事务回滚两边都不会留下半截数据def purchase(product_id, quantity, purchase_price, supplier): if quantity 0: raise ValueError(进货数量必须大于 0) if purchase_price 0: raise ValueError(进价不能为负数) conn sqlite3.connect(DB_PATH) try: with conn: cur conn.cursor() cur.execute( INSERT INTO purchase_items (product_id, quantity, purchase_price, total_amount, supplier) VALUES (?, ?, ?, ?, ?), (product_id, quantity, purchase_price, round(purchase_price * quantity, 2), supplier.strip()) ) cur.execute( UPDATE products SET stock_quantity stock_quantity ?, purchase_price ?, updated_at datetime(now, localtime) WHERE id ?, (quantity, purchase_price, product_id) ) finally: conn.close()注意这里把 purchase_price 同步更新到了商品表这就是「最近进价」的语义下次改售价或算毛利时拿它做基准。如果你想要更精确的成本核算需要引入加权平均进价在 UPDATE 里用(原库存 * 原进价 本次数量 * 本次进价) / 新库存去算。第一版不建议上先把流水留好后面要升级数据都还在。进货功能还缺一个常见场景退货或作废一笔错误进货。正确做法不是直接 DELETE而是加一个 status 字段标记为作废或者用负数数量写一笔冲销单。直接删流水会让库存轨迹断掉月底查账时说不清那笔货去哪了。4.2 会员价和促销价价格可以改但成交单价必须在流水里留快照会员价已经体现在商品表的 member_price 字段收银时 checkout 函数会优先取它。这里再补促销价的处理方式。最省事的做法是临时改 sell_price促销结束后改回来def set_sell_price(product_id, new_price): if new_price 0: raise ValueError(售价必须大于 0) conn sqlite3.connect(DB_PATH) try: with conn: conn.execute( UPDATE products SET sell_price ?, updated_at datetime(now, localtime) WHERE id ?, (new_price, product_id) ) finally: conn.close()这个方法简单但有一个必须接受的代价促销期间的销售流水unit_price 快照是促销价历史记录是对的促销结束后恢复原价不影响任何历史数据。真正的坑不在促销功能本身而是有人促销结束后忘了改回去第二天原价商品按促销价卖了一整天。我的习惯是改价函数里加一行打印把「原价 → 新价 → 操作时间」输出到操作日志靠日志提醒自己别忘了恢复。如果你不想靠人工恢复可以在商品表加 promo_end_at 字段收银时判断当前时间是否在促销窗口内自动决定取哪个价。这是第二版的事第一版保持简单。会员积分同理第一版只需要在收银事务里加一张 points 流水表不要急着做积分兑换先把销售额统计准。4.3 盘点账面库存和实物对不上用调整单兜底而不是直接改库存每个月店里都会盘点结果基本一定是账面数和实物数对不上。损耗、串货、收银漏扫全都会造成差额。盘点功能的正确做法是把差额记成一张调整单而不是在商品表里悄悄改数字def stock_take(product_id, actual_quantity, note盘点): conn sqlite3.connect(DB_PATH) try: with conn: cur conn.cursor() cur.execute( SELECT stock_quantity FROM products WHERE id ?, (product_id,) ) row cur.fetchone() if row is None: raise ValueError(商品不存在) book_quantity row[0] diff round(actual_quantity - book_quantity, 2) if diff 0: print(f商品 {product_id} 账面与实物一致无需调整) return cur.execute( INSERT INTO stock_adjustments (product_id, book_quantity, actual_quantity, diff, note) VALUES (?, ?, ?, ?, ?), (product_id, book_quantity, actual_quantity, diff, note) ) cur.execute( UPDATE products SET stock_quantity ? WHERE id ?, (actual_quantity, product_id) ) print(f盘点完成商品 {product_id} 调整 {diff:.2f}) finally: conn.close()diff 的正负号很有用正数表示盘盈负数表示损耗。月底汇总 stock_adjustments 里 diff 的和就能得出整店损耗金额这是老板判断「货有没有被合理损耗掉」的核心数据。直接改商品表的库存而不留调整单等于把这个数据扔进了无底洞以后什么也说不清。盘点还有一个注意点盘点期间最好停售或者至少保证盘点时没有正在写入的收银事务。SQLite 单写者在并发写入时报 database is locked小店操作频率低一般不会遇到但如果收银和盘点同时进行就会看到。我把盘点功能设计成关门后的动作界面上明确提示「盘点期间请勿收银」。5. 小型商场商品管理系统常见问题与排查五类翻车现场的处理记录这部分把我在类似小系统上实际排查过的五类问题整理出来。每一类按现象、原因、解决三段对照和你的系统逐条比对命中率很高。前四个是代码和数据模型层面的坑第五个是浮点精度问题属于平时看不见、月底必冒头的类型。排查问题的基本方法就一条先看流水再看商品表最后看操作日志。库存数字不对永远先查产生变动的流水而不是猜商品表被改坏了。5.1 现象库存扣成了负数现象收银完成后商品库存显示负数甚至当天就变负。原因扣库存的 UPDATE 语句没有带 stock_quantity ? 条件库存不足时数据库仍然执行了扣减或者系统里有两段代码都在改库存收银走了一段带条件的另一段后台手工调整没走事务直接把数字改了。解决所有库存扣减统一使用带条件的 UPDATE并检查 cur.rowcount为 0 就抛异常终止。整个系统只保留 checkout 一个收银入口其他任何位置不得直接改 stock_quantity。商品表的库存字段是结果不是被随意改的草稿。5.2 现象同一件商品建了两条记录现象商品表里出现两条「可乐」一条库存 20一条库存 5收银时不知道选哪条报表里同一个商品算了两行。原因建表时 barcode 字段没有加 UNIQUE 约束散货没有条码每次录入时图省事直接新建库里积累了多条同名的「未分类」记录。解决barcode 加 UNIQUE散货允许 NULL但录入前先按名称和规格做一次模糊查询。已经出现重复数据的先写一个合并脚本把库存累加到保留的那一条上再删除冗余记录顺序不能反否则库存会丢。5.3 现象月底销售金额和现金对不上现象月底把流水金额加起来和实际收到的现金差几十块怎么查都查不出是哪天的哪笔。原因历史销售流水没有存成交单价快照月底算账时拿商品表的当前售价去反推金额或者系统里某个功能对 sale_items 做了 UPDATE把历史单价跟着商品改价一起改了。解决sale_items 只允许 INSERT不允许 UPDATE。unit_price 必须是收银那一刻写入的快照。对账一律查 sale_items永远不要把商品表的 sell_price 拿出来乘数量充当销售额这两个数在改价之后就不是同一个概念了。5.4 现象数据库文件打不开或变成 0 字节现象某天打开程序提示数据库损坏shop.db 大小为 0 字节或明显偏小所有商品资料都在里面损失惨重。原因直接用文件复制的方式备份正在写入的数据库复制出来的是损坏副本或者把数据库文件放在网盘实时同步目录、U 盘里同步冲突把文件覆盖成了空文件。解决备份用 SQLite 的 backup 接口不要用文件复制的土办法。用 Python 写一个定时备份函数import sqlite3, datetime def backup_db(): ts datetime.datetime.now().strftime(%Y%m%d_%H%M%S) src sqlite3.connect(shop.db) dst sqlite3.connect(fbackup/shop_{ts}.db) with dst: src.backup(dst) dst.close() src.close() print(f备份完成backup/shop_{ts}.db)每天关门后跑一次保留最近 7 份。SQLite 的 backup 接口支持在线备份不需要把程序停下来这也是它在单机场景比 Access 文件稳的地方。数据库文件本身只放在本地硬盘不要放在任何实时同步目录里。5.5 现象盘点后库存的小数位越调越乱现象散货按斤称重卖库存数量出现 0.6183 这种小数每次盘点把实物重量填进去数字越调越乱损耗金额也算不准。原因REAL 类型浮点累加产生尾差称重复原进库存时没有最小计量单位约束金额和数量用了两套精度规则有的地方 round 有的地方不 round。解决所有金额统一 round 到两位小数库存数量按最小计量单位取整比如散货统一记到 0.001 位超过这个精度就四舍五入。每月盘点时把累计尾差一次性消化在调整单里不要在平时频繁微调库存。这是浮点数的天性不是系统坏了是精度策略没定清楚。6. 交付前最后一周和 Excel 并行跑三天再决定要不要切换新系统做完别急着把老账本扔掉。我的习惯是并行验证三天白天店里正常走 Excel 原流程收银机上用新系统再录一遍晚上关门后对账。这一步不是为了测 bug而是为了让老板亲眼看到两个口径的数字能对上。每天关门后跑三句 SQL第一句看当日销售总额和笔数第二句看各分类销售额第三句看低于预警值的商品SELECT date(created_at) AS 销售日期, sum(total_amount) AS 销售额, count(*) AS 笔数 FROM sale_items WHERE date(created_at) date(now, localtime) GROUP BY date(created_at); SELECT p.category AS 商品分类, sum(s.total_amount) AS 销售额 FROM sale_items s JOIN products p ON s.product_id p.id WHERE date(s.created_at) date(now, localtime) GROUP BY p.category; SELECT name AS 商品名称, stock_quantity AS 当前库存 FROM products WHERE stock_quantity min_stock;三类数字同时和 Excel、现金、货架实物比对连续三天全部对上再让店里停用 Excel。并行验证的价值在于把切换风险压到最低老板信任了这个数据系统才算真正交付。验证通过后功能按这个顺序加第一优先是扫码枪它本质上是键盘模拟扫码就是向条码输入框快速输入一串字符现有 add_product 和 checkout 的 barcode 参数可以直接接住不需要改表结构第二是会员积分新增 members 表和积分流水表在收银事务里同步写积分事务保证积分不会漏记第三才是多门店和远程报表那个阶段把 SQLite 换成 MySQL只改数据访问层业务函数结构不用动。这套系统真正的边界在规模和复杂度单店、单机、几千个 SKU 是舒适区一旦店开多了、销售并发上来SQLite 就不够用了。那是另一个项目的开始不是这个系统的失败。我自己的教训是这类小系统最容易死在两个地方一是过度设计一上来就做权限、做多角色结果进货、收银、盘点这个核心闭环没跑稳二是不留流水所有库存问题都靠直接改数字糊弄过去最后数据变成了黑匣子。把闭环跑稳让库存和金额每一天都能对上比什么花哨功能都重要。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑