危险化学品仓储单看这个题目可能觉得就是普通进销存换了个壳。但真正深入去做的时候才会发现普通仓储系统关注的是数量对不对、出入库记不记得清危化品仓储要解决的是“安全存量”“双人双锁”“合规台账”“应急联动”这一连串硬需求。而高校实验室场景又比工厂、医院更特殊——实验室的药品种类杂、单人领用频繁、师生流动性大、安全员很难全覆盖盯防。所以这个题目本质上做的不是一个普通进销存系统而是一套面向实验室场景的合规管理工具。我个人做了多年开发也接触过不少类似的管理系统项目有几个项目也正好是高校的安全管理场景。这类系统最值钱的地方从来不在增删改查而在你对业务规则的理解深度。这篇文章我就从需求拆解、技术选型、数据建模、核心代码、权限安全、排错方法六个维度把整套系统从头到尾说清楚。内容基于我这几年的实践经验也会补充一些常见的坑和毕设答辩时容易被追问的点。适合正在做类似选题的同学参考也适合想进入软件开发领域但还没有接触过完整项目开发的初学者借鉴。1. 项目整体设计与技术选型1.1 危化品仓储系统到底要解决哪些问题很多人在做毕设的时候一看到“仓库管理系统”就觉得是简单的CRUD。真正深入需求调研之后会发现危化品仓储和普通商品库存有几个本质区别首先是安全存量问题。危化品不像日用品囤得越多越划算。很多品种有最大储存量限制比如某类易燃液体在同一个储存柜里不能超过多少升这就需要在库存管理的时候做“存量上限校验”一旦超出不仅要提醒还得在出库逻辑上优先执行出库入库逻辑进行拦截。其次是双人双锁和审批流程。正规的危化品管理不是一个人能随便领走的要经过“申请—审批—双人复核—领用登记”这套流程。学生做实验需要领用盐酸得先提交申请实验指导老师审批库房管理员复核出库整个过程全部留痕。系统里如果没有这块流程就只是玩具系统面试官或者答辩老师一问就会露怯。第三是台账追溯。实验室安全检查的时候危化品的购入、领用、结存、报废记录必须能对得上做到来源可查、去向可追、账物相符。这部分往往被很多毕设忽视但恰恰是危化品仓储系统区别于普通库存系统最重要的功能点。综上核心功能至少包括基础信息管理试剂库、供应商、试剂字典、库存管理入库、出库、盘点、调拨、审批工作流领用申请、审批、复核、安全预警存量上限、有效期、临期、过期、低位、台账查询与统计报表、系统权限管理。1.2 为什么选择Python技术栈确定了功能范围接下来是技术选型。Python在高校里属于普及度很高的语言很多学生日常都在用Python做数据分析或算法实验上手阻力最小。而且在Web开发领域Python有Django和Flask这两个特别成熟的框架适合处理这类管理系统。Django自带Admin后台、ORM、迁移机制、认证系统对毕设来说意味着开发效率极高。Flask更轻自由度更高但很多模块需要自己拼装比如登录认证要自己写Session管理、ORM要自己接SQLAlchemy。我个人在做这类系统的时候如果是一个人在团队里开发、要求快速出成果且必须保证代码质量通常优先选Django。它内置的Admin后台在调试阶段可以直接作为数据维护界面使用甚至答辩演示的时候都可以作为辅助界面展示数据结构。为什么不选Node.js或者Java没有绝对的好与坏核心是匹配度。Java做大型企业级系统确实稳健但开发周期更长代码量大毕设时间通常只有几个月Java容易把节奏拖垮。Node.js适合高并发I/O密集场景但高校危化品仓储这个场景并发量极低没有扬长避短的必要。Python配合Django在这类中小型信息管理系统上属于“降维打击”工具和场景高度匹配。技术栈具体如下后端框架Django 4.x数据库MySQL 8.0 或 SQLite演示环境前端Bootstrap 5 jQuery 或 Vue 3 分离模式身份认证Django内置认证 自定义扩展报表ECharts 图表库部署Linux服务器 Nginx Gunicorn 或直接开发板运行这里有一个经验如果毕设要求里没有强制前后端分离优先采用Django模板Bootstrap的方案。原因很简单页面数量多前后端分离意味着要同时维护两套工程工作量差不多翻倍。而模板方案视图直接渲染数据逻辑直观答辩讲解时也容易说清楚。2. 功能模块与数据模型设计2.1 功能模块拆解一套完整的危化品仓储系统按用户视角可以拆成四个角色端普通教师/学生领用人可浏览试剂库存、提交领用申请、查看自己的历史领用记录、登记归还或报废。库房管理员负责入库登记、出库操作、盘点、处理领用申请、管理库存上下限。学院安全员查看全库台账、审批特殊化学品领用、导出安全报表、查看预警信息。系统管理员管理用户账号、配置权限、维护试剂分类、维护储存位置。模块划分要遵循高内聚低耦合原则。最忌讳把所有逻辑塞进一个视图文件比如一个add函数里既处理入库又处理审批还管着导出后面维护会非常痛苦。我在实际项目里一般这样拆分模块users用户注册、登录、角色管理reagent试剂分类、试剂字典、危险等级管理warehouse仓位管理、库存查询、库存预警stock_in入库登记、入库审核stock_out领用申请、领用审批、出库登记stock_check盘点任务、盘点记录report台账报表、统计图表2.2 危化品数据模型设计要点数据模型是整个系统的地基。刚才说的CRUD只解决“有没有”的问题数据模型决定系统能不能跑通业务规则。这里我直接给出一份核心模型参考代码基于Django的models写法from django.db import models from django.contrib.auth.models import User class HazardLevel(models.TextChoices): LOW L, 低风险 MIDDLE M, 中风险 HIGH H, 高风险 EXTREME E, 剧毒 class ReagentInfo(models.Model): 试剂字典表记录危险化学品的基础档案 name models.CharField(试剂名称, max_length128) cas_no models.CharField(CAS号, max_length32, blankTrue) formula models.CharField(分子式, max_length64, blankTrue) hazard_level models.CharField(危险等级, max_length1, choicesHazardLevel.choices) hazard_class models.CharField(GHS分类, max_length128, blankTrue) max_stock models.FloatField(最大库存量, default100) min_stock models.FloatField(最小库存量, default0) unit models.CharField(单位, max_length16, default瓶) storage_condition models.CharField(储存条件, max_length255, blankTrue) msds_file models.FileField(MSDS文档, upload_tomsds/, blankTrue, nullTrue) def __str__(self): return f{self.name}({self.cas_no}) class StorageLocation(models.Model): 库位表楼层-房间-柜号-层板 building models.CharField(楼栋, max_length64) room models.CharField(房间号, max_length64) cabinet models.CharField(柜号, max_length64) shelf models.CharField(层板, max_length64) def __str__(self): return f{self.building}-{self.room}-{self.cabinet}-{self.shelf} class Stock(models.Model): 库存表某个批次试剂在某库位的存量 reagent models.ForeignKey(ReagentInfo, on_deletemodels.CASCADE, verbose_name试剂) location models.ForeignKey(StorageLocation, on_deletemodels.CASCADE, verbose_name库位) batch_no models.CharField(批号, max_length128) quantity models.FloatField(当前存量, default0) total_in models.FloatField(累计入库量, default0) total_out models.FloatField(累计出库量, default0) production_date models.DateField(生产日期, nullTrue, blankTrue) expiry_date models.DateField(有效截止日期, nullTrue, blankTrue) class Meta: unique_together (reagent, location, batch_no) class StockInRecord(models.Model): 入库记录 stock models.ForeignKey(Stock, on_deletemodels.CASCADE, verbose_name关联库存) quantity models.FloatField(入库数量) operator models.ForeignKey(User, on_deletemodels.PROTECT, verbose_name入库操作人) source models.CharField(来源, max_length255) # 采购单号/供应商 record_time models.DateTimeField(入库时间, auto_now_addTrue) class StockOutRecord(models.Model): 出库记录 stock models.ForeignKey(Stock, on_deletemodels.CASCADE, verbose_name关联库存) quantity models.FloatField(出库数量) applicant models.ForeignKey(User, on_deletemodels.PROTECT, related_nameapplicant, verbose_name领用人) auditor models.ForeignKey(User, on_deletemodels.PROTECT, related_nameauditor, verbose_name审批人) purpose models.CharField(实验用途, max_length255) status models.CharField(状态, max_length16, defaultPENDING) # PENDING 待审批APPROVED 已通过REJECTED 已驳回OUT 已出库 record_time models.DateTimeField(申请时间, auto_now_addTrue) out_time models.DateTimeField(出库时间, nullTrue, blankTrue)几个字段设计的细节值得说清楚CAS号必须要有。CAS号是化学物质的唯一识别号没有CAS号的试剂管理不严谨。而且CAS号是字符串不是数字不要用IntegerField去存因为CAS号带有连字符。批号和有效截止日期对上才有意义。同一瓶试剂不同批次有效日期完全不同所以库存表要按批号分行不能用试剂ID直接合并存量。我在早期做这类项目的时候踩过这个坑——当时直接按试剂名称汇总库存结果同一批试剂不同批次的保质期完全没法区分做巡检报表时过期和未过期混在一起非常麻烦。后来改成按批次拆行才算解决。审批流建议用状态字段而不是独立工作流引擎。如果系统复杂度不高用status字段切换状态就够了。引入独立工作流引擎反而增加复杂度很多状态流转还是要自己写逻辑得不偿失。2.3 为什么库存表单独建而不是直接写在入库记录里设计的时候要分清“流水”和“余额”两个概念。入库记录、出库记录是流水动账都在流水表里而库存表是余额它体现的是某个库位某个批次当前还剩多少。流水表和余额表分离的好处是出库的时候只需要判断余额表里的存量够不够然后写一条出库流水同时扣减余额。拆分清楚之后查询库存列表、做统计报表都更快更直接不用每次去流水表里SUM汇总。真实的生产系统也是这样做的比如金融系统的账户余额表与交易流水表就是分离的。3. 核心实现逻辑与代码示例3.1 入库流程的实现入库的核心逻辑是这样的根据试剂名称和CAS号查找试剂字典如果不存在就先创建字典项然后绑定库位、批号判断库里是否已有同试剂同批次同库位的记录有就累加数量没有就新建一条库存记录最后写一条入库流水。流程完整实现def stock_in(request, reagent_id): if request.method POST: form StockInForm(request.POST) if form.is_valid(): reagent ReagentInfo.objects.get(idreagent_id) quantity form.cleaned_data[quantity] batch_no form.cleaned_data[batch_no] location form.cleaned_data[location] production_date form.cleaned_data[production_date] expiry_date form.cleaned_data[expiry_date] # 1. 查找或创建库存余额记录 stock, created Stock.objects.get_or_create( reagentreagent, locationlocation, batch_nobatch_no, defaults{ quantity: 0, production_date: production_date, expiry_date: expiry_date, } ) stock.quantity quantity stock.total_in quantity stock.save() # 2. 写入入库流水 StockInRecord.objects.create( stockstock, quantityquantity, operatorrequest.user, sourceform.cleaned_data[source], ) # 3. 检查是否超出最大存量 if stock.quantity reagent.max_stock: messages.warning(request, f当前存量已超过该试剂最大安全存量{reagent.max_stock}{reagent.unit}请及时处理) return redirect(stock_detail, stock_idstock.id)这里用get_or_create是一个实用技巧比先查再判空新增要简洁得多而且天然避免并发情况下重复插入的问题。3.2 出库审批与安全校验出库是所有业务里最需要谨慎的环节因为危化品一旦出库就意味着要进入实验室或某个使用场景流程上不能只由学生申请、管理员直接发必须有审批逻辑。同时还要考虑存量够不够、有没有超过有效期、领用人是否具有该类别化学品的领用资格。核心代码参考def apply_stock_out(request, stock_id): stock get_object_or_404(Stock, idstock_id) if request.method POST: quantity float(request.POST.get(quantity)) purpose request.POST.get(purpose) # 1. 校验数量 if quantity 0: return JsonResponse({code: 400, msg: 出库数量必须大于0}) if quantity stock.quantity: return JsonResponse({code: 400, msg: f库存不足当前剩余 {stock.quantity}{stock.reagent.unit}}) # 2. 校验有效期 if stock.expiry_date and stock.expiry_date date.today(): return JsonResponse({code: 400, msg: f该批次已于 {stock.expiry_date} 过期禁止出库}) # 3. 创建申请记录 record StockOutRecord.objects.create( stockstock, quantityquantity, applicantrequest.user, purposepurpose, statusPENDING, ) return JsonResponse({code: 200, msg: 申请已提交等待审批, record_id: record.id})审批侧的逻辑login_required user_passes_test(lambda u: u.role in [admin, safety_officer]) def approve_stock_out(request, record_id): record StockOutRecord.objects.select_related(stock, stock__reagent).get(idrecord_id) action request.POST.get(action) # approve / reject if action approve: stock record.stock if record.quantity stock.quantity: return JsonResponse({code: 400, msg: 审批失败当前库存不足以满足该申请}) # 扣减库存 stock.quantity - record.quantity stock.total_out record.quantity stock.save() record.status OUT record.auditor request.user record.out_time timezone.now() record.save() # 如果低于最低库存触发补货预警 if stock.quantity stock.reagent.min_stock: add_alert(stock, alert_typeLOW_STOCK) return JsonResponse({code: 200, msg: 审批通过已出库}) elif action reject: record.status REJECTED record.auditor request.user record.save() return JsonResponse({code: 200, msg: 已驳回申请})出库业务的关键点在于把申请和出库拆成两个动作。申请的时候只校验数量、有效期等基础规则真正扣减库存的动作发生在审批通过的那一刻。这模拟了真实业务里的“审批通过后仓库才发货”流程逻辑上更严谨。另一个关键是低库存预警要写在出库之后。一开始我把预警放在申请阶段后来发现会出现审批还没通过就预警的情况造成误报。后来改成出库实际发生后判断库存是否低于最低限值才触发。3.3 库存预警机制预警机制是高校安全检查中经常被拿出来问的部分也是系统里比较能体现思考深度的模块。我做了几种预警方式存量超标预警库存量超过试剂字典中设定的max_stock时告警低存量预警库存量低于min_stock时提示准备采购补货即将过期预警有效期在90天内提醒处理或使用已过期预警超过有效期自动锁定禁止出库。这个预警模型是整个系统在“安全管理”属性上区别于普通进销存系统的关键。实现上也很简单Django的ORM一个查询就能完成例如today date.today() expiring_soon Stock.objects.filter( expiry_date__isnullFalse, expiry_date__ltetoday timedelta(days90), expiry_date__gtetoday, quantity__gt0 ) overdue Stock.objects.filter( expiry_date__isnullFalse, expiry_date__lttoday, quantity__gt0 )需要注意一个细节预警查询一定要加quantity__gt0这个条件。一个批次的库存已经用完清零了就不需要再预警了。我接手的某个项目里就发生过这种情况旧批次出库完清零后仍然出现在“即将过期”名单里安全员以为还有库存要处理实际上已经空了。预警列表页面建议同时显示“试剂名称、批号、库位、剩余量、生产日期、有效期、剩余天数、建议处理方式”并支持导出Excel。后续做学期安全检查的时候直接从系统导出一份预警表就能作为管理存档使用非常方便。3.4 RFID和二维码在仓储场景的接入虽然毕设不一定要求接入硬件但这是很容易在答辩中拉开区分度的拓展点。多数高校实验室不具备大规模RFID条件因为RFID读写器和电子标签都有成本尤其是化学品类目多贴标签粘贴损耗也高。相比之下二维码的成本几乎为零。我通常推荐的做法是库存管理采用“二维码批次标签”方案。每种试剂入库时生成一个唯一的QR码打印出来后粘贴在包装或货架上扫码即可查看批号、有效期、存放位置、安全类别、当前剩余量。对于出库和盘点扫码能快速定位到具体批次避免人工翻找单据。Django里生成二维码用的是qrcode库配合Pillow生成图片。参考实现import qrcode from io import BytesIO from django.core.files.base import ContentFile def generate_qr(request, stock_id): stock Stock.objects.get(idstock_id) qr qrcode.QRCode(version1, error_correctionqrcode.constants.ERROR_CORRECT_M) qr.add_data(fREAGENT:{stock.reagent.cas_no}:BATCH:{stock.batch_no}:LOC:{stock.location}) qr.make(fitTrue) img qr.make_image(fill_colorblack, back_colorwhite) buffer BytesIO() img.save(buffer, formatPNG) stock.qr_code.save(fstock_{stock.id}.png, ContentFile(buffer.getvalue()), saveTrue) return redirect(stock_detail, stock_idstock.id)如果将来实验室想接入RFID二维码方案也不会白做。二维码里的信息就是RFID标签EPC应该存储的属性主干将ID映射逻辑平移即可。4. 权限与安全的工程化实现4.1 为什么权限设计是危化品系统的一等公民普通仓储系统让普通员工只能查库存没有问题但危化品系统不一样每个动作都可能涉及安全合规所以身份必须分级操作必须留痕。我建议把角色权限做到按钮级而不是简单的页面级。比如库房管理员可以看到“入库登记”按钮但只有安全员才能看到“报废审批”按钮教师能提交领用申请但不能审批自己的申请学生账号默认只有浏览试剂字典的权限领用申请必须绑定指导教师。实现上我会把用户的角色字段追加到Django自带的User模型上from django.contrib.auth.models import AbstractUser class User(AbstractUser): role models.CharField(角色, max_length16, choices[ (student, 学生), (teacher, 教师), (admin, 库房管理员), (safety, 安全员), ])在视图中用自定义装饰器校验角色def role_required(*roles): def decorator(view_func): wraps(view_func) def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: return redirect(login) if request.user.role not in roles: raise PermissionDenied(当前角色无权执行该操作) return view_func(request, *args, **kwargs) return wrapper return decorator视图用法就是一行装饰器role_required(admin, safety) def delete_invalid_stock(request, stock_id): ...4.2 审批逻辑里防止“自己审自己”在真实的管理系统里审批人和申请人如果可以是同一个人等于审批流形同虚设。虽然高校场景里一般不会出现自己申请自己审批的事但作为系统设计应该把这条规则硬编码进去。if record.applicant request.user: return JsonResponse({code: 400, msg: 申请人不能审批自己的领用申请})答辩的时候老师如果问“为什么不能自己审批自己”可以从利益冲突和工作分离原则去回答这样一个常见问题反而成了加分项。4.3 操作日志与审计危化品仓储系统的操作日志必须以“谁在什么时间对哪个批次做了什么操作”为最小记录粒度。我用了一个独立的UserLog模型记录所有关键动作class OperationLog(models.Model): operator models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue) action models.CharField(动作, max_length32) detail models.TextField(详情) ip_address models.GenericIPAddressField(IP地址, nullTrue, blankTrue) created_at models.DateTimeField(操作时间, auto_now_addTrue)在视图函数中通过装饰器自动记录def log_operation(action): def decorator(view_func): wraps(view_func) def wrapper(request, *args, **kwargs): response view_func(request, *args, **kwargs) OperationLog.objects.create( operatorrequest.user, actionaction, detailf{request.method} {request.path}, ip_addressget_client_ip(request), ) return response return wrapper return decorator日志记录放在业务动作完成后这样不会因业务逻辑抛异常而产生虚假日志。这一点我在以前的项目里吃过亏一开始把日志放在视图开头记录后来发现接口报错也记了一条“成功日志”导致审计数据不可信。4.4 常见Web安全的防护措施毕设系统也要做好基本安全防护不能只是功能逻辑对就完事。这里我只强调几个最关键的CSRF保护Django默认开启CsrfViewMiddleware模板里所有POST表单都需要带{% csrf_token %}。如果做前后端分离需要在DRF里搭配JWT或Token认证并在前端请求头带X-CSRFToken。SQL注入防护使用Django ORM天然参数化查询禁止直接拼接SQL字符串。这块在原生SQL使用较多时需要额外注意。文件上传MSDS文档上传时要校验文件后缀只允许pdf, doc, docx限制大小建议5MB内并用uuid重命名文件。密码安全用Django自带认证系统默认使用PBKDF2加盐哈希不要自己存明文密码。这个在很多毕设里是硬伤。实际操作中还有一处很常见但容易忽略的点不要把DEBUGTrue的模式部署到演示环境。DEBUG模式下Django会打印完整堆栈信息包括数据库配置等敏感信息安全上属于大忌。演示机上最好用Gunicorn Nginx部署同时把DEBUG关闭还可以通过ALLOWED_HOSTS限制访问域名或IP。5. 实际开发中的常见问题与排查技巧5.1 并发扣库存导致数据不一致我实际遇到过这样的问题两个人同时看到库存还剩3瓶同时提交出库申请理论上有一个人应该失败但最终两条出库记录都成功了库存变成了负数。根本原因是两个请求同时读到旧库存值扣减后写回覆盖。Django的ORM里直接读取再进行赋值保存就是典型的“读改写”竞态。解决方案是条件更新from django.db.models import F updated Stock.objects.filter( idstock.id, quantity__gtequantity ).update( quantityF(quantity) - quantity, total_outF(total_out) quantity ) if updated 0: return JsonResponse({code: 400, msg: 库存不足请刷新后重试})用F对象加上filter条件在数据库层面保证只有满足quantity 出库量时才会更新成功。这样即使并发请求来也只有一条记录的更新影响行数为1另一条自然失败。5.2 ORM查询导致性能问题Django ORM懒加载的特点是好用但也容易产生“N1查询”问题。比如在库存列表页面显示每一种试剂的分类名称、库位名称如果没有用select_related预加载外键列表页每显示一行就会多查一次数据库。30条库存数据可能产生31条SQL数据量大了页面明显变慢。解决方案很简单stocks Stock.objects.select_related(reagent, location).all()select_related适合ForeignKey和OneToOneField的情况ManyToManyField则用prefetch_related。这两个方法在查询性能优化上属于必会的入门级操作。演示的时候故意把几百条库存数据显示出来感受一下加载速度差你就明白为什么不能省这一步了。5.3 日期判断与时间的坑危化品有效期涉及日期比较很容易栽在数据类型上。比如前端传过来的是“2025-01-01”字符串后端如果直接用字符串比较和datetime对象比较就会出错。我的经验是所有日期字段在模型层统一用DateField表单接收后立刻转成date对象再比较不要用字符串倒来倒去。还有一个典型问题时区设置。使用Django时如果不设置TIME_ZONE默认是UTC。如果用户在中国创建的记录时间会显示成UTC时间相差8小时日志审计对不上。配置项里设置TIME_ZONE Asia/Shanghai USE_TZ True但注意USE_TZ True情况下Django存储到数据库的时间仍然是UTC只是模板渲染时转成本地时间。如果你用原生SQL查询直接取datetime字段就会拿到UTC值。处理办法是在查询时用datetime.datetime.now()转成当前时区或者直接用django.utils.timezone.now()——它会自动处理时区转换。5.4 Excel导入导出常见坑危化品系统一般都要有Excel导出功能用于安全台账存档。导出可以用openpyxl或pandas但要注意编码问题。导出的Excel如果包含中文建议专门加上样式设置表头。openpyxl里处理中文比较简单只要不涉及到旧版xls格式基本没有编码问题。Excel导入更麻烦常见的问题是用户上传的Excel里有合并单元格解析后部分字段为空日期列在Excel里是文本格式显示“2024/7/1”解析成datetime失败数字列被Excel识别成文本比如CAS号“7732-18-5”变成了字符串“7732185”。建议导入的时候做一个统一的清洗函数def parse_excel_value(val): if val is None: return if isinstance(val, float) and val.is_integer(): return str(int(val)) if isinstance(val, datetime.datetime): return val.strftime(%Y-%m-%d) return str(val).strip()导入前还要加一个“预览”环节先上传文件、解析前10行供用户查看确认无误后再真正写入数据库。这个设计能很大程度避免错误数据直接污染主库。5.5 部署环境里出现的静态文件404Django在开发模式下能自动处理静态文件但部署到Nginx之后如果配置不对CSS和JS都会404页面全部裸奔。原因是Django本身不负责服务静态文件生产环境的静态文件必须交给Nginx处理。关键步骤如下在项目settings.py里配置STATIC_ROOT并设置STATIC_URL执行python manage.py collectstatic将所有静态文件收集到STATIC_ROOT目录Nginx配置里增加一个location指向STATIC_ROOT目录。如果用了Django Admin执行collectstatic时Admin的静态文件也会一并收集不用担心漏掉。6. 演示与答辩的经验总结6.1 演示数据和场景要提前准备答辩演示的时候不要拿着空数据库现场操作。提前往系统里导入一批贴近真实的模拟数据效果会好非常多。数据不需要很多但要有层次准备10-15种常见试剂覆盖低、中、高、剧毒四个等级库存里有2-3个“即将过期”批次演示预警的时候直接点击查看2个“已过期”批次演示锁定不可出库1个“存量超标”的条目演示预警和入库拦截20条左右的历史出入库记录演示台账统计报表时数据不至于太空。我见过不少同学答辩时现场创建数据一边打字一边讲解观众等待时间一长节奏就断了。提前把带有丰富状态的演示数据准备好演示起来行云流水回答提问也有底气。6.2 安全管理功能多展示一些答辩提问环节老师大概率会问“你的系统跟普通的进销存系统有什么区别”这时候安全管理模块是你整个系统的护城河。重点去讲这几个点双人双锁的双人复核逻辑怎么设计的剧毒品是否单独分类、双重审批有效期预警和过期锁定怎么自动处理台账如何支持溯源查询最大库存量超限时系统做了什么。只要把这些功能讲清楚老师就会明白这个系统不是套壳项目而是真的理解了危化品管理的核心需求。6.3 系统扩展方向提前准备还有一个容易被追问的问题“系统还能怎么改进”我建议提前准备两三个扩展点但不要编造明显做不出来的方向。比较务实的有对接物联网称重传感器智能柜实时监测重量变化与账面库存自动核对对接短信/企业微信提醒预警消息推送给安全员而不是只能在网页里查看实验废液回收管理危化品出库之后使用完形成的废液也需要登记和危化品全生命周期管理串起来引入物品编码标准实现校内多实验室统一管理。这几条都是基于“全生命周期管理”的自然延伸不是空话也基本符合现实需求。写在最后做危化品仓储系统让我最大的收获不是Python或Django的语法技巧而是理解了软件系统永远是在解决业务问题业务规则才是灵魂。库存个数的加减是任何程序员都能完成的但能够理解“为什么审批和出库要分开”“为什么旧批次必须先出库”这些业务逻辑开发出来的系统才真正可用。给正在做这个题目或者其他管理系统题目的同学一个建议动手写代码之前至少花一周时间去把业务流程梳理清楚。你可以画流程图可以把所有需要记录的字段列成清单可以去找实验室的老师问一次实际的管理流程。前期需求调研做充分了后面开发的时间反而会大幅缩短因为少走了很多弯路。如果这篇能帮助你把系统做得更扎实我分享的这些经验就有价值了。最后再分享一个小技巧把系统里的安全预警规则单独放到一个配置文件里阈值不要写死在代码里。这样以后响应新政策要求调整预警标准时只需要改配置不需要动逻辑代码。我用了很久才意识到这个设计对长期维护有多省力。