Python果蔬库存预警与补货工具:从命令行到网页的实战拆解

📅 发布时间:2026/10/10 14:14:10
Python果蔬库存预警与补货工具:从命令行到网页的实战拆解
前阵子接了个练手需求一个果蔬销售小系统要管理每款水果的名称、进价、售价和库存按库存预警值判断缺不缺货默认库存低于10斤就输出到“需要补货的水果”列表里。听起来就是个课堂作业级别的小工具但真动手的时候我发现里面藏着一堆值得写清楚的东西数据怎么存、阈值该不该写死、等于10斤算不算低于、脏输入怎么拦、中文编码怎么处理、怎么从“能跑”变成“老板能用”。这篇就按我自己的实操过程来拆从一个空文件开始写到最后扩展成能存数据、能排优先级、能上网页的小工具。每一步都会解释为什么这么写顺便把踩过的坑都摆出来。1. 先别急着写代码把“库存低于10斤”拆成可执行规则1.1 预警值不是写死的先定判断口径需求里最核心的一句是“库存低于10斤输出需补货的水果”。第一反应就是把判断写成if stock 10这没错但这是把阈值写死在代码里了。水果店的实际情况不是这样的香蕉保质期短安全库存要拉高苹果柚子能放预警线可以压低。同一个店里不同水果的安全库存线根本不该共用同一个10斤。所以正确做法是把阈值做成参数。函数里写def find_low_stock(fruits, threshold10)调用时默认用10店主想改就改。至少不要让“10”这个数字散落在代码各处否则后面想调成“8斤”或“按水果品类分别设置”的时候你会满文件找数字。还有一个比“写不写死”更隐蔽的坑判断口径到底是 10还是 10。“低于10”在日常用语里通常指严格小于库存正好10斤时不算缺货。但从备货角度如果你下单后隔天才送到库存刚好等于安全线时其实就该预警了不然明天一早可能就断货。我的建议是把判断函数也做成可配置项但第一版默认用 threshold。为什么因为需求原文是“低于10斤”严格按需求实现不会错。等业务方反馈“等于10也要提醒”你改一行或加个参数include_equalTrue就行。边界情况一定要单独测试这个我后面第4章会专门讲。1.2 数据怎么存列表套字典比四个数组聪明在哪先把数据模型想清楚。一个水果需要记录四样信息名称苹果进价3.5元/斤售价5.8元/斤库存7.2斤新手最容易写成四个平行列表names、purchase_prices、sale_prices、stocks。我一开始也这么干过因为每个列表对应一列看着整齐。但问题马上就来你想遍历每个水果的信息时得靠下标去四个列表里跳names[i]配purchase_prices[i]配sale_prices[i]配stocks[i]。这就像把一个人的姓名、电话、地址分别记在四张纸上每次查人都要四张纸一起翻。万一删了一个水果四个列表的下标全部要同步很容易错位。更好的方案是单个水果用一个字典{ name: 苹果, purchase_price: 3.5, sale_price: 5.8, stock: 7.2 }所有水果再放进一个列表fruits [ {name: 苹果, purchase_price: 3.5, sale_price: 5.8, stock: 7.2}, {name: 香蕉, purchase_price: 2.2, sale_price: 4.0, stock: 12.0}, {name: 草莓, purchase_price: 12.0, sale_price: 19.9, stock: 3.5}, ]遍历的时候for f in fruits取库存就是f[stock]不需要记住任何下标。想加字段比如“日均销量”“保质期天数”直接在字典里加一个 key 就行不需要动其他水果的数据结构。列表套字典已经够用不需要一上来就上类Class。有人可能觉得面向对象更“正确”但在这个需求里字典就是最轻量的方案。等后面你发现多个函数都要操作“水果”这个复合概念而且行为越来越复杂时再考虑封装成Fruit类不迟。项目初期能少一层抽象就少一层。1.3 把功能拆成输入、判断、输出三个模块我看过很多同学把整个程序写在两个for循环里输入和判断混在一起。当时能跑但想改任何一个逻辑就头疼。这个需求天然可以拆成三段输入采集水果数据转成统一格式业务逻辑判断哪些水果库存低于预警值输出把要补货的水果整理成清单对应到代码就是三个函数def get_fruits_input(): pass def find_low_stock(fruits, threshold10): pass def print_replenish_list(low_items): pass def main(): fruits get_fruits_input() low_items find_low_stock(fruits, threshold10) print_replenish_list(low_items) if __name__ __main__: main()拆开的好处很直接判断逻辑不依赖从键盘输入还是从文件读以后换成网页表单、CSV导入find_low_stock完全不用改。print_replenish_list也可以换成“写入Excel”“发到企业微信”只要传入同一个low_items列表就行。这三级结构是后面所有扩展的地基。我后面讲的那个JSON存储扩展和网页版都是在不动判断函数的前提下把输入和输出换了壳而已。2. 从零写一个能跑的水果补货工具2.1 连续录入水果信息顺便把脏数据挡在门外第一版输入我用的是最简单的方式一行输入一个水果四个字段用空格分隔像这样苹果 3.5 5.8 7.2 香蕉 2.2 4.0 12.0输入q结束录入。为什么用“一行四个值”而不是分四次提问因为分四次提问太累赘录一个水果要按四次回车录10个水果手指都麻了。一行一个批量录入的效率高很多也方便以后从表格里直接复制粘贴。但这样对输入的容忍度就低了。用户可能输入“苹果 3.5元 5.8元 7.2斤”也可能把库存写成“七”。我的做法是先拆分再转换转换失败就提示重录不直接崩溃。def get_fruits_input(): fruits [] print(请输入水果信息格式名称 进价 售价 库存单位元/斤斤) print(输入 q 结束录入) while True: line input( ).strip() if line.lower() q: break parts line.split() if len(parts) ! 4: print(格式不对需要4个字段名称 进价 售价 库存) continue name, purchase_str, sale_str, stock_str parts try: purchase float(purchase_str) sale float(sale_str) stock float(stock_str) except ValueError: print(价格和库存必须是数字请重新输入) continue if purchase 0 or sale 0 or stock 0: print(价格和库存不能为负数请重新输入) continue fruits.append({ name: name, purchase_price: purchase, sale_price: sale, stock: stock, }) return fruits这里try/except是必须的因为float(七)会直接抛异常不拦下来整个程序就崩了。负数也要拦因为库存为负就说明录入数据本身有问题让它混进候选列表会导致预警判断失真。还有一个细节字段个数校验。有人会输入“苹果 3.5 5.8”少一个库存有人会多一个备注字段。split()后数量不等于4就提示比让程序硬解析然后报“下标越界”要友好得多。2.2 库存预警判断的3行核心逻辑和浮点数陷阱判断函数本身很简单def find_low_stock(fruits, threshold10): low_items [] for f in fruits: if f[stock] threshold: low_items.append(f) return low_items就一个for循环加一个if。但它有一个容易被忽略的坑浮点数比较。价格和库存都有小数float在计算机里不是精确的。比如0.1 0.2 0.3 # False题目里的库存临界是10斤用户实际录的可能是 9.999999 或者因为计算累计变成了 10.0000001。如果你用 10去判断一个本应刚好多10斤的库存可能因为浮点误差被判成“需要补货”也可能刚好反过来。库存单价场景我通常这么处理录入后直接round(stock, 2)把比较基准统一到“分”或“两”这个精度单位或者干脆把库存按“斤”保留一位小数再比较。严格场景用Decimal也行但这里库存预警没那么高的精度要求round就够用。另外判断逻辑里我不推荐在循环里直接打印。把符合条件的都收集到一个low_items列表里再统一输出是因为后面还要做排序、算建议补货量、写文件这些功能都需要这份数据而不是几行打印文本。2.3 输出“需要补货的水果”清单输出的第一版长这样def print_replenish_list(low_items, threshold10): if not low_items: print(所有水果库存充足暂时不需要补货。) return print(需要补货的水果) for i, item in enumerate(low_items, 1): shortage threshold - item[stock] print(f{i}. {item[name]}当前库存 {item[stock]} 斤 f建议补货 {shortage:.1f} 斤)这里“建议补货量”我暂时用threshold - stock表示。这个公式很粗糙因为真实场景还要考虑下单到货时间和日均销量第5章我会给出更完整的算法。但第一版能帮老板快速知道“哪个水果差多少斤到预警线”。实际输出效果需要补货的水果 1. 苹果当前库存 7.2 斤建议补货 2.8 斤 2. 草莓当前库存 3.5 斤建议补货 6.5 斤如果你想把输出做成表格推荐用字符串格式化不要用print硬拼print(f{水果名称:10}{当前库存:12}{建议补货:12}) print(- * 36) for item in low_items: shortage threshold - item[stock] print(f{item[name]:12}{item[stock]:14.2f}{shortage:12.1f})10表示左对齐并占10个字符位数字/中文名称宽度不一致实际对不齐很正常但比光秃秃输出好看得多。“够用”就行别在这上面花太多时间后面要上网页/Excel 的话这个打印函数会整个被替换掉。第一版的完整代码我已经在前面拼好了get_fruits_input采集find_low_stock判断print_replenish_list输出main串起来。这个骨架已经能完整满足题目要求下面开始说怎么把它从“作业”变成“工具”。3. 三个扩展让工具从“能跑”变成“能用”3.1 让数据存下来接一个 JSON 文件第一版的问题是所有数据都在内存里程序一关录的10个水果全没了。下次再开要重新录非常劝退。给工具加持久化的最省事方案就是 JSON 文件。思路很简单录入时把水果数据存进一个字典键是水果名称值是这个水果的完整信息。退出前用json.dump写入fruit_data.json启动时用json.load读回来。import json DATA_FILE fruit_data.json def load_data(): try: with open(DATA_FILE, r, encodingutf-8) as f: return json.load(f) except (FileNotFoundError, json.JSONDecodeError): return {} def save_data(data): with open(DATA_FILE, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)几个细节写文件时ensure_asciiFalse这样JSON里中文能直接看到而不是变成一堆\uXXXX转义字符。indent2让文件格式可读方便你手动改。load_data里同时捕获“文件不存在”和“JSON格式损坏”两种异常。前者是首次运行后者是文件写一半被中断导致都返回空字典保证程序还能启动。录入的时候遇到已存在的水果名不要直接覆盖要提示“已有库存是否累加”。否则老板想补录结果把之前的库存顶掉了很危险。有了这个存储层main就变成了启动加载data循环录入/修改/删除save_data保存。输入输出只是换了数据源核心判断函数还是那一行for循环这就是拆分模块的价值。3.2 结合进价售价算补货优先级同样的库存先补谁光知道“谁缺货”还不够。水果店库存有限进货资金也有限不可能所有缺货的水果一次性全补齐。这时候应该排优先级。我用一个很朴素的思路库存低于预警线的水果毛利越高、库存越低的越应该优先补货。计算一个“紧急分”def prioritize(low_items): for item in low_items: profit item[sale_price] - item[purchase_price] item[profit] profit item[priority_score] profit / max(item[stock], 0.1) low_items.sort(keylambda x: x[priority_score], reverseTrue) return low_items为什么是profit / stock因为水果是生鲜库存越低离断货越近单斤毛利越高越值得把有限的进货资金押上去。分母加个max(stock, 0.1)是为了防止库存为0时除零报错。真实场景里还可以加入“保质期天数”“日均销量”做更精细的加权分但工程上更重要的是先把优先级逻辑和核心流程解耦find_low_stock负责筛选prioritize负责排序输出层按排好序的列表渲染。这样哪个环节想调整都不影响其他地方。3.3 从命令行到网页给水果店老板一个能点的界面命令行工具最大的问题是门槛老板看到黑底白字的控制台可能直接懵。想让他真正用起来最好做成网页。但我不推荐用 Flask/Django 这种重型框架来写一个库存预警工具用pywebio或streamlit就够。以pywebio为例它可以把原来的三个函数直接复用输入部分用input组件收集表格用put_table渲染。from pywebio import input as pinput from pywebio import output as poutput def app(): fruits [] while True: data pinput.input_group(录入水果, [ pinput.input(名称, namename), pinput.input(进价, namepurchase_price, typepinput.NUMBER), pinput.input(售价, namesale_price, typepinput.NUMBER), pinput.input(库存, namestock, typepinput.NUMBER), ]) fruits.append(data) if not pinput.actions(继续录入吗, [继续, 结束]): break low_items find_low_stock(fruits) poutput.put_table([[名称, 库存, 建议补货]] [ [x[name], x[stock], 10 - x[stock]] for x in low_items ])核心判断函数一行没改只是把input()换成了网页组件、把print()换成了表格组件。这就是上面拆模块的红利。如果一开始输入判断输出全糊在一起换界面等于重写。4. 调试踩坑实录脏输入、边界值和中文编码都不省心4.1 用户不会按格式输入怎么办我在测试时故意输了一些乱七八糟的格式苹果 3.5元 5.8元 7.2斤 香蕉 2.2 4.0 q 草莓 十块 19.9 3.5第一条会因为float(3.5元)直接抛异常。最稳的办法是用正则从字符串里提取数字而不是要求用户必须按规范输入。只要用户没有太离谱都能救回来。import re def extract_number(text): match re.search(r\d\.?\d*, text) if match: return float(match.group()) return None提取规则很简单把“3.5元”里的3.5抠出来“5斤”里的5抠出来。如果用户输入“七斤”extract_number返回None再提示重试。这样比直接要求用户改格式体验好太多。要注意正则里的\d\.?\d*匹配不了3.5.2这种畸形数字但至少能匹配出第一个合法的数字。该报错的地方还是让它报错不要过度宽容否则后面算出来的补货量是错的比报错更麻烦。4.2 库存刚好等于10斤到底补不补前面讲过stock 10和stock 10的取舍。这里放一组边界测试用例直接给结论库存数值 10判断 10判断直观解释9.99需要补货需要补货差一点点到线10.00不需要需要补货刚好在线上10.00浮点误差可能变9.999999不确定大概率需要浮点比较坑真实项目里我建议把“安全库存”做成每个水果可配的字段。比如{name: 香蕉, safety_stock: 15, stock: 12}香蕉保质期短安全库存设15斤苹果设8斤。这样判断逻辑统一写成if f[stock] f.get(safety_stock, DEFAULT_THRESHOLD)既支持默认10斤又允许特殊水果覆盖。这既解决了“等于10算不算”的哲学问题也解决了不同水果不同预警线的业务问题。4.3 中文乱码、同名水果覆盖、斤和公斤的混乱这三个坑几乎每次写这类小工具都会碰到。中文乱码多数出现在 Windows 控制台和文件读写两个地方。控制台乱码是编码设置问题可以先chcp 65001切到UTF-8或者在代码里用sys.stdout.reconfigure(encodingutf-8)。文件乱码最恼人因为老板可能会用Excel打开JSON文件如果没写ensure_asciiFalseExcel里看到的全是\uXXXX。导出CSV时我推荐用UTF-8 BOM也就是open(file, w, encodingutf-8-sig)这样双击打开不会乱码。同名水果覆盖问题在数据层解决。用字典data[苹果] {...}时如果已经有苹果了应该累计库存而不是直接覆盖。我给一个简单版本if name in data: data[name][stock] stock else: data[name] {purchase_price: purchase, sale_price: sale, stock: stock}累计后价格怎么办理想是按批次加权平均(旧库存*旧进价 新库存*新进价) / 总库存。如果两批货进价不同直接覆盖会把成本算错后面利润统计就全错了。斤和公斤的单位混乱更隐蔽。有人录5公斤香蕉等于10斤但程序当成5斤预警判断就失真了。统一单位的最好办法是露出一层单位换算函数录入时可选“斤/公斤/箱”内部全部转斤。def to_jin(value, unit): if unit 公斤: return value * 2 if unit 箱: return value * 18 # 假设一箱18斤具体按实际调整 return value别小看单位统一真实生鲜场景里不统一单位算出的补货量直接害人。4.4 一组直接能用的测试用例不管代码多简单建议还是用单元测试把核心函数锁住。下面这套用例覆盖了正常、边界、异常三类情况用例输入预期结果正常缺货苹果 stock7.2, 阈值10苹果进入补货列表正常不缺香蕉 stock12.0, 阈值10香蕉不进入列表严格低于草莓 stock10.0, 阈值10用草莓不进入列表浮点误差库存round(9.99999,2)等于9.99进入列表用unittest或者简单的assert都可以重点是把find_low_stock的判断口径固定下来以后谁改了逻辑跑一下测试立刻露馅。def test_find_low_stock(): fruits [ {name: 苹果, stock: 7.2}, {name: 香蕉, stock: 12.0}, {name: 草莓, stock: 10.0}, ] low find_low_stock(fruits, threshold10) assert len(low) 1 assert low[0][name] 苹果这种测试代码不花哨但能保证你重构的时候不把一个原本正确的小工具改坏。5. 从练习项目到真实水果店认清需求和现实之间的差距5.1 真实生鲜场景里的安全库存不是固定值题目里的“低于10斤就预警”在作业里能得满分但真实水果店没这么简单。一个摆在货架上的水果影响它是否缺货的不是单纯“剩余斤数”而是“还能卖多久”。两个因素最关键日均销量。苹果一天卖2斤剩7斤还能扛3天草莓一天卖8斤剩7斤当天就断货。采购提前期。今天下单明天到货和明天下单、后天到货需要留的底仓完全不同。所以更合适的公式是建议补货量 安全库存 - 当前库存 日均销量 × 采购提前期举个例子某水果安全库存15斤当前库存4斤日均销量5斤采购提前期2天那建议补货量 15 - 4 5 × 2 21斤。意思是现在补21斤等两天后到货时库存大概降到安全线附近不会断货也不会堆积。如果你手头连日均销量都没有就老老实实用threshold - stock但心里要清楚这只是“补到安全线”的意思不是“够卖到下次进货”。5.2 店主真正想看的“补货单”长什么样把“名称、当前库存、建议补货”三列直接给老板他大概率还要拿计算器按一下“能卖几天”。与其让他自己算不如把该补的信息一次列全水果名称当前库存(斤)日均销量(斤)预计可售天数建议补货量(斤)预计毛利(元)草莓3.560.581594.5香蕉1243.00843.2“预计可售天数 当前库存 / 日均销量”列出来后老板一眼就能看出草莓今天不进货就断货。建议补货量用上面那个公式算预计毛利用(售价-进价) × 建议补货量算方便老板估这单货能赚多少。这个表其实就是把原来print_replenish_list函数稍微扩一扩。别小看这种输出方式真实用户不关心你的代码结构他们只关心“明天该进什么、进多少、能赚多少”。输出层多花点心思工具的接受度会高很多。5.3 往后还能接多少事扫码、销量预测、自动提醒如果这个工具真的用起来了后续可扩展的方向非常多扫码枪进货时扫一下条码自动更新库存不用手敲。销量记录每天关门把“当日销售额/销量”录入自动算日均销量安全库存就能慢慢“活”起来。自动提醒库存低于预警线时自动生成补货单发到企业微信群或邮件老板不用天天打开系统看。趋势预测用最近两周的销量预测下周需求节假日自动调高安全线。这些方向听着大但每一步都是在原来的核心判断函数外面加一层东西数据源变了、输出目标变了判断逻辑始终是那个stock safety_stock。我个人在这个小工具上踩过最大的坑就是第一版把“10斤”写死在了两个地方后来老板说香蕉要调高警戒线苹果要降低我只能满文件找数字。改成逐水果配置安全库存之后这类需求就变成改一行配置的事。还有一个体会这种小工具别在第一次写的时候就想着上框架、上微服务、上设计模式。先让老板用上、解决“明天进什么货”这个问题后面发现不行再重构。代码烂不烂是次要的能不能真实解决一个具体问题才是核心。这个水果补货工具就是一个最好的起点。