资讯动态

Python购物管理系统课程设计全解析:模块划分、JSON持久化与打包

发布时间:2026/9/8 17:09:01 来源:尧图企业网站定制
我很少写关于“课程设计型项目”的源码分析。原因很简单这类项目的难点通常不在技术而在于你不知道一份代码拿回来后该怎么讲、怎么改、怎么把自己的名字写进去。基于Python的购物管理系统恰恰是这类项目里最典型的一个——它不复杂但功能链路完整特别适合用来解释“面向对象设计、数据持久化、简单业务闭环、部署打包”这一整条计算机专业的基础素养链条。如果你手上正好有一份类似的源码包或者正在准备自己动手做一个这篇文章想帮你解决的不是“把代码跑起来”这么简单而是让你弄明白每个模块为什么长这样、每个步骤背后的判断依据是什么以及最终怎么把代码、文档、答辩串成一套完整作品。这篇我会从需求边界开始按数据层、业务层、界面层、部署交付的顺序逐层拆不会给你贴一整份上万行的源码而是给核心模块的设计思路和可直接运行的代码骨架。适合三类人看要交期末作业但想真正弄懂逻辑的同学、接手了别人的源码包但不知从何讲起的同学、以及想基于这种小系统练手但不想做成玩具的初学者。1. 先把“购物管理系统”拆成一个能实现也能讲清楚的功能清单1.1 它到底为谁服务要解决什么场景问题“购物管理系统”从名字上看是一个很宽泛的概念宽泛到你去搜代码能搜出八百个不同版本。但如果你把使用场景收敛一下会发现几乎所有课程设计类的购物系统本质上都在模拟一个极简的线下或线上超市收银后台。它假设有这样两类人一类是超市管理员需要维护商品信息。比如新增一款商品、修改价格、查看当前剩余库存、下架不再售卖的商品。一条完整的商品记录通常只有四个核心字段商品编号、名称、单价、库存数量。有些版本会加分类和销量但那些属于锦上添花。另一类顾客通过命令行界面浏览商品选择要购买的东西放进购物车最终完成结算并生成订单。整个过程可以不需要注册登录也可以做成简单的登录版本。课程设计里大部分要求是“单机可用”也就是说不需要考虑真正的多用户并发、不需要对接支付接口、不需要部署到云服务器。把这个场景理清楚后你才能确定系统边界。许多同学拿到题目就开始写代码结果做出了一个既不像管理系统、也不像购物前台的四不像。我认为正确顺序是先识别用户故事再反推功能。哪怕只有两个角色每个角色的功能也足够写成一个小的用户故事列表。管理员侧的故事大概是管理员可以新增商品录入名称、进价或售价、初始库存管理员可以修改商品的价格和库存管理员可以按编号或名称查找商品管理员可以删除停售商品系统能显示所有商品清单顾客侧的故事则更多落在流程上顾客可以查看全部在售商品顾客可以将某件商品按指定数量加入购物车顾客可以查看购物车内已有商品和总价顾客可以从购物车中移除某个商品或修改数量顾客确认结算后生成订单系统提示应支付金额订单完成后商品库存自动扣减这些故事并不过分。一个控制台程序完全可以承载。但注意这里已经隐含了两个重要业务规则加入购物车时要检查库存结算完成后要真正让库存减少。很多半成品源码的问题恰恰出在这两条规则上后面我会单独讲。1.2 “管理”到底体现在哪里模块边界怎么划和纯商城前台不同“管理系统”这四个字意味着你还需要一个能事后追踪的数据载体。也就是说商品信息不能写死在代码变量里订单信息不能被程序退出后清空。数据持久化是这个项目能不能被称为“系统”的核心分水岭。从模块划分角度一个简洁的购物管理系统可以拆成五块模块职责典型接口商品服务提供商品查询、新增、修改、删除、搜索ProductService购物车服务维护当前顾客要买的商品及数量ShoppingCart订单服务校验库存、生成订单、扣减库存OrderService数据访问层负责从文件或数据库读写数据DataManager界面与流程控制打印菜单、读取输入、调度上面各服务Main / MenuHelper这个边界划好之后你能得到的最大好处是每个模块可以单独测试。你不需要打开完整程序敲一堆菜单才能验证购物车价格算得对不对写一个测试脚本直接调用cart.get_total()就行。很多基础薄弱的学生在答辩时最怕老师问一句“能不能现场加点功能”如果模块边界清晰现场加功能只是新增一个菜单分支的问题如果业务代码和界面代码全揉在while True里面现场加功能几乎等于重写。2. 数据层怎么选我从“一坨列表存内存”改成“JSON 文件落地”的判断过程2.1 三种常见数据方案的横向对比网上流传的购物系统代码数据层通常有三种写法。第一种是纯内存商品用一个list[dict]在程序启动时初始化程序一关数据全没。第二种是用 CSV 或文本文件一行行存储读的时候逐行解析。第三种是使用 JSON整个数据文件是嵌套结构读写只需json.load和json.dump。还有进阶版用 SQLite属于真正的轻量级数据库但课程设计里想讲清楚它需要额外篇幅。我建议项目从 JSON 起步理由非常实在JSON 对中文兼容好记事本打开不会乱码数据结构可以嵌套比如一条订单里带商品明细订单本身就是数组字典JSON 天然能表达Python 标准库直接处理不需要额外安装第三方依赖和 XML 类似学生能直观看到数据内容CSV 最大的问题是没有类型信息所有数据读出来都是字符串。商品价格存入 CSV 后要自己转 float库存也要记得 int()一旦某行缺失列或包含逗号解析逻辑瞬间变复杂。SQLite 虽然更好但会给部署部分增加负担你需要交代系统里安装了什么数据库驱动、是否单独安装数据库服务等。而真正重要的是JSON的清晰数据结构。你最终要向别人解释“我的数据是怎么落的”一个嵌套JSON文件的解释成本远低于把SQL建表语句再讲一遍。下面是我常用的存储文件结构。data/ ├─ products.json # 商品主数据 └─ orders.json # 历史订单每条订单含快照明细 products.json 初始内容 [ { id: 1, name: 可口可乐, price: 3.5, stock: 200 }, ... ]每种商品或订单用一条字典表示收集到列表里。这样一个列表就能表达“商品表”字段名即表头和 CSV 相比省去了字符串处理。2.2 项目骨架与类设计参考如果你是自己动手写我建议的目录长得像下面这样。它把分层逻辑映射到了物理文件上评审老师一眼就能看出你不是所有代码堆在一个文件里的新手。shopping_system/ ├─ main.py # 入口菜单管理与流程控制 ├─ models.py # Product Order 等数据模型 ├─ datamanager.py # 负责所有 JSON 文件读写 ├─ services.py # ProductService/OrderService 等业务 ├─ cart.py # 购物车对象 ├─ requirements.txt # 依赖清单通常为空 ├─ docs/ │ ├─ 部署文档.md │ └─ 课程设计报告.md └─ data/ ├─ products.json └─ orders.jsondatamanager.py尽量只做文件读写不掺和业务判断。它提供的接口是read_product()、write_products(products)这类最小单元。业务层在修改库存前先读取修改后再写回订单数据就是追加新纪录后整体写回。这种“先全量读再全量写”的做法效率不算高但在这个数据量场景下完全够用而且逻辑不容易出错。在 models 层面我建议使用轻量类而不是直接操作裸字典。定义一个Product类有pid、name、price、stock四个属性再给它两个方法to_dict()用于写入 JSON类方法from_dict()用于从字典还原对象。这类轻量数据模型代码量不多但能让后续的cart.get_total()写起来顺手很多因为你访问的是product.price而不是product[price]。代码如下class Product: def __init__(self, pid, name, price, stock): self.pid pid self.name name self.price float(price) self.stock int(stock) def to_dict(self): return { id: self.pid, name: self.name, price: self.price, stock: self.stock, } staticmethod def from_dict(data): return Product( piddata[id], namedata[name], pricedata[price], stockdata[stock], )订单模型稍微复杂一点。建议订单对象里直接保存“商品快照”而不是只存商品ID。商品快照就是把下单那一刻的商品名称和成交单价复制一份放进订单里。原因很现实三天后商品涨价了你不能让历史订单的总价跟着变如果商品被删除历史订单也不能变成一堆无法解析的 ID。这个设计专业系统里叫“数据快照”这里提前用上是很好的加分项。class Order: def __init__(self, order_id, items, total_amount, created_at): self.order_id order_id self.items items # [{name: 可乐, price: 3.5, count: 2}] self.total_amount total_amount self.created_at created_at def to_dict(self): return { order_id: self.order_id, items: self.items, total_amount: self.total_amount, created_at: self.created_at, }2.3 读写封装里最容易忽略的兜底处理一个靠谱的 DataManager除了读写之外还要做一次初始化动作程序首次运行时如果发现data目录或 JSON 文件不存在就自动创建并写入默认数据。很多同学直接把初始商品硬编码在主函数里会导致两个问题一是你后续通过管理功能新增的商品无法保存二是程序一跑就重新生成一批默认商品覆盖掉之前的数据修改。我在datamanager.py里会这样兜底import os import json BASE_DIR os.path.dirname(os.path.abspath(__file__)) DATA_DIR os.path.join(BASE_DIR, data) PRODUCT_FILE os.path.join(DATA_DIR, products.json) ORDER_FILE os.path.join(DATA_DIR, orders.json) def ensure_data_file(): if not os.path.exists(DATA_DIR): os.makedirs(DATA_DIR) if not os.path.exists(PRODUCT_FILE): init_products [ {id: 1, name: 可口可乐, price: 3.5, stock: 200}, {id: 2, name: 农夫山泉, price: 2.0, stock: 300}, {id: 3, name: 乐事薯片, price: 7.5, stock: 80}, ] write_json(PRODUCT_FILE, init_products)这个处理的优点是幂等。无论你启动多少次程序只要文件已在就不会重复重置数据。当你修改过库存或新增过商品关掉程序再启动数据依然保持上一次的状态。3. 从浏览商品到订单落库核心业务逻辑的运行链路3.1 商品浏览与关键字搜索程序启动后顾客看到的第一个功能应该是商品列表。这个列表直接读products.json然后按固定格式打印出来即可。打印格式建议用print(f{p.pid:4} {p.name:12} {p.price:8} 元)控制台里左对齐右对齐会让输出显得专业很多。搜索功能值得留意的是匹配维度。新手通常会只写“编号精确匹配”但人在购物场景里更习惯输入关键词。所以更好的实现是名称中包含用户输入内容就算命中这样用户输入“可乐”能搜出“可口可乐”输入“0”也能搜出所有包含0的编号或商品。字段模糊搜索逻辑很直白def search_products(self, keyword: str): keyword keyword.strip() if not keyword: return self.list_products() result [] for p in self.list_products(): if keyword.lower() in p.name.lower() or str(p.pid) keyword: result.append(p) return result这里有个隐藏好处你把“校验搜索结果是否为空”从业务代码里挪走了。调用方只需判断返回列表长度。如果为空界面层打印“未找到匹配商品”就可以服务层不需要知道界面怎么提示。3.2 购物车核心操作加、改、删、自动算钱购物车在不同系统里有不同形态。有的版本用一张数据库表每条记录是“用户ID商品ID数量”我们这种单机命令行系统更合适的做法是维护一个ShoppingCart对象。初始化时它有一个items字典键是商品ID值是顾客想买的数量。之所以用字典是因为它天然保证同一个商品不会出现两条记录。顾客第一步加了 2 瓶可乐第二步再加 3 瓶我们希望结果是 5 瓶而不是两行可乐。如果使用列表存购物车每次添加前都得先遍历判断是否已存在——能做但代码很难看。字典能直接用items.get(product_id, 0)取出当前数量并累加。购物车类最重要的边界是它不直接持有商品完整信息只持有商品ID和数量。需要算总价时通过外部传入的商品查询函数去拿最新单价。这样写的好处是如果管理员修改了商品价格顾客加入购物车后再次查看总价时使用的是最新价格购物车里不存在“旧价格快照”这种状态不一致问题。当然结算确认之后必须在订单里保存快照价格这是另一层逻辑。下面是购物车类的参考实现class ShoppingCart: def __init__(self, product_getter): self.items {} # product_id - count self._product_getter product_getter def add(self, product_id, count1): if count 0: raise ValueError(加入数量必须大于 0) product self._product_getter(product_id) if product is None: raise ValueError(商品不存在) after_add self.items.get(product_id, 0) count if after_add product.stock: raise ValueError(f库存不足剩余 {product.stock} 件) self.items[product_id] after_add def remove(self, product_id): if product_id in self.items: del self.items[product_id] def update(self, product_id, new_count): if new_count 0: self.remove(product_id) return product self._product_getter(product_id) if product is None or new_count product.stock: raise ValueError(更新后数量超过库存) self.items[product_id] new_count def show_detail(self): rows [] total 0.0 for pid, count in self.items.items(): product self._product_getter(pid) if product is None: continue subtotal product.price * count rows.append((product.pid, product.name, product.price, count, round(subtotal, 2))) total subtotal return rows, round(total, 2)add方法里先计算after_add再和库存比较这能避免“上次加 5 件剩余库存不足但程序没察觉”的问题。update方法里把new_count 0当作删除操作处理符合大多数购物软件的用户预期你把数量改成 0它就从购物车里消失了。3.3 结算与库存扣减的前后顺序结算是一个系统里最容易“埋雷”的环节。正常流程应该是这样的遍历购物车根据商品ID拿商品最新数据检查每个商品数量是否都不超过库存计算总金额生成订单记录订单明细写入“下单时商品名称与单价快照”把商品文件中各商品库存逐一减去相应数量清空购物车这两个步骤的顺序不能反过来。如果先生成订单再检查库存就可能出现“库存不足但订单已生成”的脏数据如果先扣库存再生成订单而订单写入失败库存就白白丢了。具体实现时由于我们的库存是 JSON 全量读写的只要保证“校验所有商品库存充足”和“更新所有商品库存”之间没有中断单进程程序基本能保证正确。代码节选大致如下def checkout(self, cart: ShoppingCart): cart_items cart.copy_items() if not cart_items: raise ValueError(购物车为空无法结算) products self.product_service.list_products() product_map {p.pid: p for p in products} checkout_items [] total 0.0 # 第一遍全部校验 for pid, count in cart_items.items(): product product_map.get(pid) if product is None: raise ValueError(f商品 {pid} 已下架) if count product.stock: raise ValueError(f商品 {product.name} 库存不足) subtotal product.price * count total subtotal checkout_items.append({ product_id: pid, name: product.name, price: product.price, count: count, }) # 第二遍真正扣减库存 for pid, count in cart_items.items(): product product_map[pid] product.stock - count self.product_service.save_products(list(product_map.values()))订单文件里再追加一条新订单记录。订单编号可以使用时间戳加上序号比如2025011510300001省去自增主键维护。这套代码单独看不算难真正的价值在于它把“下单”定义成了可重复执行的过程。你需要想清楚的是购物车在界面上属于用户的临时操作订单是持久化数据两者生命周期完全不同绝不能在用户点退出时把购物车用json.dump存到和订单同一个文件里。购物车只存活在内存里就够了下次启动从空车开始更符合程序逻辑也给演示省了麻烦。4. 最容易翻车的三个细节我从跑偏版本里看到的重灾区4.1 金额计算陷阱浮点数不该直接做比较Python 的浮点数有一个极为常见的表现0.1 0.2 ≠ 0.3因为底层二进制无法精确表达部分十进制小数。单价 3.5、数量 2 这类数字碰巧都能被二进制精确表达所以没问题但一旦出现3.6 * 2这种后台算出来的可能是7.2附近的一个极其接近但不完全相等的数。程序在显示订单总额时如果不做处理某些边界值会出现7.2000000000000005这种让人看着想删代码的结果。处理方案分两个层级。最省事的方式是所有展示位置都用round(金额, 2)包裹打印使用:.2f格式符。稍微规范一点的做法是引入decimal.Decimal把金额作为字符串传入再从 Decimal 转成字符串做显示。课程设计项目用到 Decimal 算是一个“懂行”的细节但会增加代码量和解释成本。我的建议如果是四则运算购物车总价计算过程先别急于 round只在最终展示和写入订单时进行 round。因为中间过早的四舍五入可能带来微小偏差累积。为了演示稳定性最终数据统一保留两位小数这样界面上至少是干净的。4.2 类型与输入健壮性用户不按套路输入时怎么办命令行系统最常见的崩溃原因是输入类型转换。代码里写int(input(请输入数量))用户手滑输入了abc整个程序直接抛出ValueError闪退。这种问题在第一轮测试时几乎必现必须从一开始就做输入防御。我习惯把输入处理写成一个小工具函数def read_int(prompt, defaultNone): while True: raw input(prompt).strip() if raw and default is not None: return default if raw.isdigit(): return int(raw) print(输入无效请输入非负整数)这样用户怎么敲都不会让界面崩掉。注意isdigit()对-1会返回 False所以负数会被自然拦截掉。数量想允许更大范围也可以再扩展判断。商品ID也一样。用户输入0或空字符串时要给出友好提示而不是直接去查表返回 None。别小看这些拦截我回收过许多同学的代码有一个通病是演示前正常演示时一旦输入了错误格式整个程序当场崩。此类灾难完全能用一个小函数避免但它往往被忽略。4.3 订单数据和商品数据的状态一致性基于 JSON 的系统没有数据库事务所以“订单写入成功但库存扣减失败”这类不一致是潜在存在的。虽然单机小规模下概率极低但在答辩时老师如果问到“你如何保证数据一致性”这是必考题。你能做的方案有三种。第一种最简单操作顺序固定为“先全量更新商品库存文件再追加订单文件”并在代码注释里写明这个顺序约定。第二种是对两个文件都重写这一侧整体包一层文件锁不过单机控制台程序这样做会显得过度设计。第三种是每次启动时做一次一致性校验扫描所有订单中每个商品 ID如果发现某商品快照数量与当前实际库存差异可疑就打印警告。我建议至少做到第一种并准备一段口述由于系统是单用户操作我们在同一个程序进程内连续完成库存扣减和订单追加中间没有并发写入文件本身较小使用“先校验后提交”的方式保证不会出现负数库存订单。万一订单文件写入时程序异常终止下一次启动会在加载文件时捕获到 JSON 解析异常并提示数据可能损坏这时候可以把 data 目录备份后删除重建。这不是完美的企业级方案但诚实可靠比强行说自己有数据库事务要真实得多。5. 部署文档、源码包和项目讲解这“交付三件套”该怎么组装5.1 让代码能一键跑起来的环境准备说明源码包里如果只有.py文件而不写清运行环境几乎等于没有交付。部署文档的第一步不是讲系统设计而是用最啰嗦的口气告诉对方先装 Python再运行命令。很多同学默认对方已经装好了 Python实际上对方可能连环境变量都不懂。部署文档里应当包含 Python 的安装方式和版本建议。以 Windows 为例去官网下载 Python 3.10 或更高版本安装时务必勾选“Add Python to PATH”。这个勾选是新手最常见的问题源头如果不勾命令行里输入python会提示找不到命令。安装完成后在项目根目录打开命令行执行python --version能正常输出版本号再执行pip install -r requirements.txt我们这个项目没有第三方依赖requirements.txt 可以是空文件或者只写注释。但保留这个文件的好处是保持了项目结构的统一性扩展时加入pyinstaller等依赖时不用改文档结构。最后运行入口python main.py部署文档建议附一张运行成功后的截图包括菜单界面和第一条商品数据。文档里写得越细对方越能少打扰你。5.2 用 PyInstaller 打包成可执行文件为了演示方便许多人希望把自己写好的 Python 程序发给老师时双击就能运行。PyInstaller 是干这个活的标准工具。部署到一个没有 Python 的电脑上只需要在开发环境执行一行命令pip install pyinstaller pyinstaller -F main.py-F表示打包成单个 exe 文件。打包成功后在dist目录里找到生成的main.exe。双击它程序窗口就会出现。但这里有个隐蔽问题由于我们用相对路径访问data目录如果 PyInstaller 把程序打包成单文件运行时数据目录的位置需要特别处理。PyInstaller 的单文件模式在启动时会把临时解压目录放在系统临时文件夹里如果代码里用了os.path.dirname(os.path.abspath(__file__))解压后的路径就会变得不可控你存进去的数据可能在程序退出后一起被清理掉。更稳妥的做法是为数据目录设计一个可配置的根路径DATA_ROOT os.environ.get(SHOP_DATA_DIR) or os.path.join(os.path.dirname(os.path.abspath(__file__)), data)如果设置了SHOP_DATA_DIR环境变量就使用指定目录否则默认放在程序文件旁边。练习阶段保持默认即可不要为了打包把程序做得无法保存数据。真正答辩时我会建议你直接用命令行现场运行python main.py让对方看到编码过程比打包更可信。打包成 exe 可以作为附加交付内容但别作为唯一演示方式。5.3 课程设计报告LW和讲解材料怎么写才能显得有体系源码和部署文档之外绝大多数课程项目都要求交一份“报告”或“设计文档”。这个东西的通病是需求和设计写一大段废话核心代码模块反而没有展示。我想说一份优秀的报告评分逻辑其实很简单让老师能按图索骥地找到你设计中的亮点。报告的结构建议是这样的绪论不要长篇大论介绍购物行业背景一两句带过即可重点是“开发此系统的目标”。需求分析用表格列出你识别到的功能模块并注明优先级。这个表格能显著提升报告专业性。总体设计给出模块划分、数据文件字段说明、程序流程图。这里不需要 UML 大全但类图和关键用例的时序逻辑应该画清楚。详细设计与实现按“文件名称/类名 - 核心职责 - 关键方法 - 代码片段 - 设计理由”的格式组织。每个模块用一两段文字解释设计思路即可。测试过程列出主要功能点的测试用例包括输入、预期输出、实际输出。这也是体现“管理思维”的重要部分能证明你不只是把程序跑通而是做了验证。总结收获克制地写两三句重点是在开发过程中遇到的问题和解决办法。讲解材料则是答辩时用的。我建议你准备一张 A4 纸正面是系统菜单截图背面是模块依赖关系。讲解时先演示流程再讲一个你最得意的设计细节。比如我在设计购物车时没有让购物车直接持有商品对象而是持有商品ID、通过查询函数拿最新单价——这个细节能解释三个好处本身就是极好的答辩素材避免同时改多个对象、保证总价实时反映价格变化、方便做持久化。6. 从及格走向良好演示、测试与小步扩展的经验项目能跑起来只是及格线。如果你想在课程设计里拿个不错的分数或者这段代码以后放进简历里不心虚我给你三个投入产出比很高的改进方向。第一个方向是加自动测试。这个改动并不需要庞大的工具链只给 cart 的加减和结算写一个自测模块就够了。在最朴素的模式下你这样写# test_cart.py from cart import ShoppingCart def fake_get(product_id): return type(Product, (), {pid: product_id, name: 测试, price: 10.0, stock: 5})() cart ShoppingCart(fake_get) cart.add(1, 2) cart.add(1, 3) assert cart.items[1] 5, 购物车数量累计逻辑错误 rows, total cart.show_detail() assert total 50.0, f总价计算错误: {total} print(基础测试通过)以后任何一次重构或微调运行一下这个文件就知道购物车是否还被改坏。第二个方向是补充登录功能。管理员界面和顾客界面之间用简单的用户名密码做区分。如果不想引入数据库可以把账户信息存成 JSON 或直接在代码里做硬编码。增加登录后系统里的操作日志就能记录“谁在什么时候做了什么”这又能在报告里多写一节。用户名和密码别用明文至少用hashlib.sha256加盐哈希一下。只有两三个账户时这也只是一个小函数的事。第三个方向是让数据表更接近真实业务。你可以把商品增加分类字段、销量字段也可以给订单增加状态字段“已支付/已取消”取消订单时执行“恢复库存”的逻辑。在实际项目里订单取消时的库存回补是一个专门需求不是所有系统都会做。你把这条流程做对就能在答辩时主动提出来会比唱独角戏讲菜单操作加分得多。如果让我给站在答辩现场的同学一个最实用的建议我会说找朋友当一次“小白用户”全程不给他任何提示让他用一个陌生人的视角去操作你的系统。试完你会发现至少有三处输入不够友好、两处输出格式对不齐。把这些体验问题修完再去讲源码里那些设计巧思整段展示才算真正闭环。那个为了记录数据而努力设计字段、为了计算精确而留意浮点数、为了可扩展而划分模块的过程就是这个项目对你最大的回报。别只满足于从网上扒一份代码跑通然后改改颜色亲手重建一遍上面的每一步都值得。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价