
这个项目我前后帮几个学弟学妹调过代码自己也完整做过一版算是比较有发言权。Python医药管理系统听起来像是个标准的课程设计题目但真正动手做的时候涉及的坑比想象中多得多——从数据库表结构怎么设计才能兼顾批次和效期到库存预警的触发逻辑怎么写才不误报再到演示录像里怎么把亮点功能串成一条完整的故事线每一步都有讲究。这篇就把我从需求分析、表结构设计、核心代码实现到答辩演示的完整思路和踩坑记录都写出来给正在做这个项目或者准备拿它当毕设的同学一个参考。1. 项目整体设计与技术选型思路1.1 医药管理系统到底在管理什么很多人一听到“医药管理系统”第一反应就是“增删改查的进销存”这没错但远远不够。普通超市的进销存管的是“数量”医药系统管的是“批次 效期 数量”三个维度的组合。同样一盒阿莫西林生产批号不同、有效期不同就不能简单地合并成一个库存数字。医生开药的时候要能追溯到具体是哪个批次的药效期临近的药品要提前预警过期药品必须锁死不允许销售——这些才是医药管理系统的核心灵魂。所以在做需求分析的时候我给自己列了几个必须覆盖的场景药品基础信息的统一维护包括通用名、商品名、规格、生产厂家、批准文号、剂型、单位、零售价、进货价。采购入库环节要记录每一批药品的批号、生产日期、有效期、入库数量和供应商。销售出库的时候系统要自动扣减对应批次的库存并且优先消耗效期更近的批次先近效期先出。库存不足、库存积压、药品近效期、药品过期都需要自动提醒。不同角色管理员、采购员、销售员、系统管理员的操作权限要分开防止越权操作。这个定位直接决定了后面的表结构设计和代码逻辑比你上来就写代码再回头补需求要高效得多。我见过很多同学毕设做得“看起来很全”但逻辑上漏洞百出根本原因就是没想清楚“管理”这两个字背后到底有什么业务约束。1.2 为什么选择Python这套技术栈选Python做这个项目多半是因为上手快、生态好、答辩的时候也好讲。但“用Python”和“用Python做出来一个像样的系统”是两回事。我在这一步的核心选型是这样的Web框架Flask。对比DjangoFlask更轻量路由和ORM都是自己说了算代码结构清晰特别适合毕设级别的项目展示。Django自带Admin后台确实省事但也正因为太省事答辩时老师问“你讲讲ORM怎么用的”你反而不好展开。Flask需要你自己定义模型、自己写视图函数、自己管理session整个链路都过一遍手提问的时候心里有底。数据库MySQL 8.0。这个没什么争议主流、稳定、面试笔试也常问。如果本机装MySQL觉得麻烦也可以用SQLite先跑通逻辑最后再切MySQL。但建议直接用MySQL避免切换过程中的SQL语法差异坑。ORMSQLAlchemy。它既保留了写SQL的思维习惯又能利用模型关系简化查询。特别是药品、批次、入库、销售这几张表之间的关联查询用ORM的外键关系写起来会清爽很多。前端Bootstrap jQuery AdminLTE模板。医药管理系统是后台管理系统前端不需要多花哨突出一个整洁、信息密度高、操作直观。AdminLTE这种现成的后台模板拿来改一改就能用比自己从零写CSS效率高太多了。图表ECharts。销售统计、库存分布这些页面用ECharts画几个柱状图、饼图、折线图视觉效果和专业感直接拉满。毕设演示的时候图表页是加分项。这套组合最大的好处是每一层你都能说出“为什么这么选”而不是“大家都用所以我也用”。答辩老师追问技术细节的时候你能从路由、ORM、模板渲染一路讲到MySQL事务这就是脱胎于“背题”和“真懂”的差别。1.3 免费领源码这件事关键在“看懂”而不只是“跑通”我知道很多同学看到“免费领源码演示录像”第一反应是太好了下载下来改个名字交上去。这个想法我劝你趁早收起来。源码可以领录像可以看但如果你连自己项目里的核心表结构都说不清楚答辩的时候三句话就能被问穿。正确使用源码的方式应该是先把它跑起来然后跟着演示录像把每个页面点一遍再打开数据库看每个表存了什么数据最后找到几个核心代码文件比如登录验证、入库逻辑、库存预警逐行读。读不懂的地方先自己想再搜资料实在不行去问提供源码的人。这个过程走下来这个项目在你脑子里才是“立得住”的。后面我写的所有代码和表结构都会标注“这是基于常见实践的方案你拿到任何一份源码后都能对照着理解”核心目的是帮你看懂而不是叫你照抄交差。2. 系统功能模块拆解与数据库设计2.1 功能模块怎么划分才清晰我把整个系统分成六个功能模块模块之间边界清楚代码也好组织模块核心功能涉及角色用户认证模块登录、注销、密码修改、角色权限所有角色药品信息模块药品CRUD、分类管理、厂商管理管理员、采购员库存管理模块入库登记、出库登记、库存查询、批次管理、效期预警采购员、销售员销售管理模块创建销售单、订单明细、销售记录查询销售员统计报表模块销售趋势、药品销售排行、库存分布、利润统计管理员系统管理模块用户管理、角色管理、操作日志系统管理员你在演示录像里的讲解路径就按这个顺序来登录 → 维护药品 → 采购入库 → 销售出库 → 查看统计 → 管理用户。这条线下来功能上没有任何遗漏逻辑也通顺。2.2 核心表结构设计我不建议少于这八张表表结构是整个项目的地基地基歪了后面写多少代码都是白搭。我在设计时按照医药业务的特点定义了八张核心表users用户表id、username、password_hash、real_name、role、phone、status、created_at。密码字段一定要存哈希值不要存明文Flask里用werkzeug.security的generate_password_hash就够了。categories药品分类表id、name、description、created_at。分类用一张独立的表方便扩展和维护不要在药品表里直接写死一个“分类”字符串。suppliers供应商表id、name、contact_person、phone、address、remark。medicines药品信息表id、name、generic_name、specification、category_id外键、manufacturer、approval_number、unit、purchase_price、sale_price、stock_quantity、min_stock、created_at、updated_at。这里的stock_quantity是“当前总库存”用于列表展示和快速判断真正的批次库存明细放在medicines_batches里。medicines_batches药品批次表id、medicine_id外键、batch_number、生产日期produced_date、有效期expiry_date、quantity、remaining_quantity、supplier_id外键、inbound_date、status正常/预警/过期/售罄。这张表是判断效期预警的核心。stock_records出入库记录表id、medicine_id、batch_id、type入库/出库/报损、quantity、operation_user_id、operation_time、remark。这张表相当于操作流水账出现数据对不上的时候顺着它追溯。sales_orders销售订单主表id、order_no、customer_name、total_amount、sale_user_id、sale_time、remark。sales_order_items销售订单明细表id、order_id外键、medicine_id、batch_id、quantity、price、subtotal。这八张表各司其职共同支撑起“批次可追溯、效期可预警、流水可审查”的医药管理逻辑。特别是medicines_batches这张表很多人做医药系统的时候容易忽略只在medicines表里加一个total_stock字段那就把“库存”和“批次”混为一谈了。药品入库一箱和入库十箱如果批次不同在效期管理上的意义完全不同。2.3 为什么要单独拆出批次表我拿一个实际例子说明。假设药店里有一批布洛芬缓释胶囊批号A的有效期是2026年3月批号B的有效期是2025年12月两批货都剩50盒。如果只在medicines表里存一个“总库存100盒”那销售出库的时候你到底先卖哪批系统不知道。现实中必须“先进先出、近效期先出”先把2025年12月那批卖掉否则近效期的药会砸在手里。有了medicines_batches表之后销售出库的SQL逻辑就变成了按expiry_date升序找到该药品剩余数量大于0的批次优先扣减最早到期的批次。这个逻辑在答辩的时候讲出来老师一听就知道你确实理解医药业务而不是单纯做了一个通用的进销存。库存预警页面的查询也依赖批次表SELECT * FROM medicines_batches WHERE status正常 AND expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 60 DAY)这个查出来的就是未来60天内要过期的批次列表直观又准确。2.4 权限设计不能每个用户都能删药品医药系统涉及敏感操作权限必须分清楚。我用了最经典的“角色字段”方案字段不多但够用admin管理员拥有药品管理、供应商管理、统计报表、用户管理的全部权限。purchaser采购员可以维护药品信息、做入库操作、查看库存但不能删除药品、不能查看销售统计。seller销售员可以开销售单、查库存但不能修改药品价格。在Flask视图函数里我写了一个简单的装饰器来做权限校验from functools import wraps from flask import session, redirect, url_for, flash def role_required(*roles): def decorator(f): wraps(f) def wrapper(*args, **kwargs): if user_id not in session: flash(请先登录, warning) return redirect(url_for(auth.login)) if session.get(role) not in roles: flash(没有权限访问该页面, danger) return redirect(url_for(main.index)) return f(*args, **kwargs) return wrapper return decorator # 用法示例只有管理员能访问用户管理页面 app.route(/admin/users) role_required(admin) def admin_users(): users User.query.all() return render_template(admin/users.html, usersusers)权限控制做得好不好是判断一个毕设项目专业度的重要分水岭。很多“全功能版”系统所有页面都裸奔谁登录了都能删数据这在实际场景里是不可接受的。3. 核心功能实现从登录到库存预警的完整链路3.1 Flask项目目录怎么组织一个清晰的目录结构能帮你省掉后面大量的调试时间。我常用的组织方式是这样pharmacy_system/ ├── app.py # 应用入口 ├── config.py # 配置文件数据库连接、密钥 ├── models.py # SQLAlchemy模型定义 ├── forms.py # 表单类定义 ├── requirements.txt # 依赖列表 ├── utils/ │ ├── decorators.py # 权限装饰器 │ ├── stock_service.py # 库存核心业务逻辑 │ └── alarm_service.py # 预警逻辑 ├── views/ │ ├── auth.py # 登录/注销路由 │ ├── medicine.py # 药品管理路由 │ ├── stock.py # 出入库路由 │ ├── sale.py # 销售路由 │ └── report.py # 统计报表路由 ├── templates/ # Jinja2模板 │ ├── base.html │ ├── auth/ │ ├── medicine/ │ ├── stock/ │ ├── sale/ │ └── report/ └── static/ # CSS、JS、图片 ├── css/ └── js/路由单独拆到views目录下每个模块一个文件别把几十个路由全堆在app.py里。否则项目一膨胀改一个功能要翻几百行代码体验极差。3.2 登录认证session、密码哈希和登录状态保持登录功能是系统的入口也是答辩时大概率会被问到的部分。我实现的逻辑大致是用户提交用户名密码后先按用户名查用户然后对比密码哈希值验证通过后将用户id和角色写入session再重定向到首页。from werkzeug.security import check_password_hash from flask import request, session, flash, redirect, url_for, render_template from models import User, db app.route(/login, methods[GET, POST]) def login(): if request.method POST: username request.form.get(username, ).strip() password request.form.get(password, ) user User.query.filter_by(usernameusername).first() if user and check_password_hash(user.password_hash, password): session[user_id] user.id session[username] user.username session[role] user.role flash(登录成功, success) return redirect(url_for(main.index)) flash(用户名或密码错误, danger) return render_template(auth/login.html)这里有几个细节值得注意用户名要strip()去空格防止手滑多打一个空格导致登录失败这种问题排查起来非常隐蔽。密码错误和用户不存在尽量用同一个提示“用户名或密码错误”不要明确告诉用户“用户不存在”这在安全意识上是基本要求。SQLAlchemy的filter_by查询参数不需要百分号包裹这是ORM的习惯和写原生SQL不一样。登出功能就更简单了session.clear()然后重定向到登录页。Flask的session默认是客户端cookie加密存储的SECRET_KEY一定要在config里配置不配置的话每次重启服务用户登录状态就会失效。3.3 药品管理的CRUD与表单验证药品信息新增/编辑是整个系统出镜率最高的页面。我建议用Flask-WTF来做表单和CSRF防护它能自动生成表单字段还能在服务端做数据验证比手动从request.form里取数据要安全得多。from flask_wtf import FlaskForm from wtforms import StringField, FloatField, IntegerField, SelectField, SubmitField from wtforms.validators import DataRequired, NumberRange class MedicineForm(FlaskForm): name StringField(药品名称, validators[DataRequired(message药品名称不能为空)]) specification StringField(规格, validators[DataRequired()]) category SelectField(分类, coerceint, validators[DataRequired()]) manufacturer StringField(生产厂家) approval_number StringField(批准文号) unit StringField(单位, validators[DataRequired()]) purchase_price FloatField(进货价, validators[DataRequired(), NumberRange(min0)]) sale_price FloatField(零售价, validators[DataRequired(), NumberRange(min0)]) min_stock IntegerField(库存下限, default10) submit SubmitField(保存)这里再给一个非常实用的建议新建和编辑共用同一个表单类只是视图中根据是否有id来区分是插入还是更新。这样代码量能减少接近一半。app.route(/medicine/add, methods[GET, POST]) role_required(admin, purchaser) def medicine_add(): form MedicineForm() form.category.choices [(c.id, c.name) for c in Category.query.all()] if form.validate_on_submit(): medicine Medicine( nameform.name.data, specificationform.specification.data, category_idform.category.data, ... ) db.session.add(medicine) db.session.commit() flash(药品添加成功, success) return redirect(url_for(medicine.medicine_list)) return render_template(medicine/form.html, formform, title新增药品)一个小坑SelectField的choices必须在实例化之后、渲染之前赋值否则下拉菜单是空的。尤其是编辑场景还要把当前药品的分类设为默认选中项不然每次编辑都会变成一个空选择。3.4 入库业务的完整代码串联入库是整个系统里逻辑最重的操作之一。它要同时做两件事第一新增或更新medicines_batches表中的批次数据第二更新medicines表中的总库存stock_quantity。我把这部分逻辑单独抽成一个服务函数而不是直接写在路由里这样测试和维护都方便def create_stock_in(medicine_id, supplier_id, batch_number, produced_date, expiry_date, quantity, operator_id, remark): medicine Medicine.query.get(medicine_id) if not medicine: raise ValueError(药品不存在) # 检查同一个药品是否已有同批号的批次记录 batch MedicineBatch.query.filter_by( medicine_idmedicine_id, batch_numberbatch_number ).first() if batch: # 同批号继续入库累加剩余数量 batch.remaining_quantity quantity if batch.expiry_date expiry_date: batch.expiry_date expiry_date else: batch MedicineBatch( medicine_idmedicine_id, supplier_idsupplier_id, batch_numberbatch_number, produced_dateproduced_date, expiry_dateexpiry_date, quantityquantity, remaining_quantityquantity, status正常 ) db.session.add(batch) medicine.stock_quantity quantity record StockRecord( medicine_idmedicine_id, batch_idbatch.id if batch.id else None, typeinbound, quantityquantity, operation_user_idoperator_id, remarkremark ) db.session.add(record) db.session.commit() return batch.id为什么入库的时候要先查一次“同药品同批号”的记录因为现实中同一个批号的药可能是分两批送到仓库的。如果每次入库都新建一条批次记录同批号的数据就被拆成了多条剩余记录销售出库时扣减顺序会变得混乱。合并到一条记录里逻辑就清爽多了。入库表单里日期字段的处理也容易踩坑。Jinja2模板里如果用了input typedate提交到后端的格式是“YYYY-MM-DD”字符串直接用datetime.strptime转一下再存数据库就行from datetime import datetime produced_date datetime.strptime(form.produced_date.data, %Y-%m-%d).date()3.5 销售出库事务、库存扣减和金额计算销售开单的步骤是前端选择药品 → 输入数量 → 后端检查库存 → 扣减批次余量 → 生成销售主表和明细表 → 提交事务。关键是“检查库存”和“扣减库存”之间不能出现时间差否则并发下会超卖必须用数据库事务把整个操作包起来。from sqlalchemy.exc import IntegrityError def create_sale_order(items, customer_name, operator_id): items: 列表元素是 (medicine_id, quantity) order SalesOrder( order_nogenerate_order_no(), customer_namecustomer_name, sale_user_idoperator_id, sale_timedatetime.now() ) total_amount 0.0 try: db.session.add(order) db.session.flush() # 先拿到order.id for medicine_id, quantity in items: medicine Medicine.query.with_for_update().get(medicine_id) if not medicine or medicine.stock_quantity quantity: raise ValueError(f药品[{medicine.name}]库存不足) # 找出该药品所有的有效批次按有效期升序 batches MedicineBatch.query.filter_by( medicine_idmedicine_id, status正常 ).filter(MedicineBatch.remaining_quantity 0) \ .order_by(MedicineBatch.expiry_date.asc()).all() remaining_to_deduct quantity for batch in batches: if remaining_to_deduct 0: break deduct min(batch.remaining_quantity, remaining_to_deduct) batch.remaining_quantity - deduct remaining_to_deduct - deduct item SalesOrderItem( order_idorder.id, medicine_idmedicine.id, batch_idbatch.id, quantitydeduct, pricemedicine.sale_price, subtotalround(deduct * medicine.sale_price, 2) ) db.session.add(item) total_amount item.subtotal if remaining_to_deduct 0: raise ValueError(f药品[{medicine.name}]批次库存不足) medicine.stock_quantity - quantity order.total_amount round(total_amount, 2) db.session.commit() return order except Exception as e: db.session.rollback() raise e这里有两个非常关键的细节用with_for_update()给药品行加锁是为了防止两个销售员同时下单导致库存扣成负数这在数据库层面解决了并发问题。答辩的时候主动把“我用了行级锁来保证库存一致性”讲出来是一个不小的加分项。批次扣减顺序用order_by(MedicineBatch.expiry_date.asc())实现的就是“近效期先出”的业务规则。3.6 库存预警给近效期药品亮红灯库存预警页面是我在这个项目里最满意的一部分。它把预警分成三档用不同的颜色标识绿色正常、黄色临近有效期、红色已过期。这样一眼就能看到哪些药需要处理。def get_stock_alarm_list(): today datetime.now().date() warning_date today timedelta(days60) batches MedicineBatch.query.filter( MedicineBatch.remaining_quantity 0 ).all() result [] for batch in batches: if batch.expiry_date today: status expired elif batch.expiry_date warning_date: status warning else: status normal result.append({ medicine: batch.medicine.name, batch_number: batch.batch_number, expiry_date: batch.expiry_date, remaining: batch.remaining_quantity, status: status }) return result60天的预警窗口是有讲究的。太短比如30天给采购员的处理时间不够太长比如180天页面里过半药品都在报警失去了预警的意义。60天是药品流通行业里比较常用的近效期提醒周期这个参数你可以直接在config里做成常量方便调整。除了效期预警我还加了一个低库存预警当药品总库存小于min_stock时在首页的仪表盘上提示“库存不足”建议补货。低库存和近效期是两种完全不同的业务信号前者代表“该进货了”后者代表“这批药快过期了赶紧卖/退”一定要分开展示不要混在同一个列表里。4. 前端页面与数据可视化演示时撑起整个场面的部分4.1 基于AdminLTE的后台布局后台管理系统的前端最重要的是信息架构清楚。我用了AdminLTE的后台布局左侧是导航菜单顶部是用户信息栏主体区域是内容区。菜单结构如下仪表盘、药品管理药品列表/新增药品/分类管理、库存管理入库登记/出库登记/库存查询/效期预警、销售管理创建销售单/销售记录、统计报表销售趋势/药品排行、系统管理用户管理/操作日志。AdminLTE的好处在于它自带了一整套CSS组件和js插件表格、表单、分页、弹窗都不用自己调样式。你只需要把它官方文档里的示例代码改成自己的页面就行。4.2 用ECharts做销售统计和库存分布统计报表页面是很多同学容易做得很单薄的地方但这一块恰恰是答辩时的加分项。我用ECharts做了三个图第一个是“近30天销售趋势”折线图数据来源是sales_orders里按日期group by的sum(total_amount)。Jinja2模板里先把统计数据序列化成JSON再传给前端图表。// 图表初始化示例销售趋势折线图 var salesChart echarts.init(document.getElementById(salesChart)); $.getJSON(/api/report/sales_trend, function(data) { salesChart.setOption({ title: { text: 近30天销售趋势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.dates }, yAxis: { type: value, name: 销售额(元) }, series: [{ type: line, data: data.amounts, smooth: true }] }); });第二个是“药品销售排行TOP10”柱状图按销售数量和销售额两个维度展示。第三个是“库存分类占比”饼图展示各类药品的库存数量占比帮助管理者直观看出哪类药备货最多。这里要提醒一个重要细节图表数据接口用AJAX异步加载但Flask的模板渲染是服务端的两者容易混。正确做法是模板渲染负责页面骨架图表数据单独提供JSON接口两者分工明确。不要硬把JSON塞进HTML里也不要让前端页面完全没有初始数据导致白屏。4.3 演示录像里的操作路径设计演示录像不是把每个页面都点一遍就完事而是要设计一条有故事的演示路径。我的演示录像核心路径是这样的用管理员账号登录系统页面跳转到仪表盘。仪表盘页面上展示总药品数、总库存、近效期预警数、低库存预警数、今日销售额、近30天销售趋势图。停留几秒钟把“一眼掌握全局”的感受讲出来。点击“药品管理”→“新增药品”现场录入一条新药品信息演示表单验证比如价格填负数时报错。进入“库存管理”→“入库登记”选择刚才新增的药品录入批号和有效期入库100盒。回到药品列表展示库存数量从0变成了100。进入“效期预警”展示刚才入库药品的状态因为录入有效期离今天还有90天会显示黄色预警。进入“销售管理”→“创建销售单”卖出20盒系统自动扣减库存。查看“销售记录”展示刚才的订单金额和明细。查看“统计报表”展示销售趋势图和药品排行。演示权限控制用一个销售员账号登录试图访问用户管理页面被拒绝并提示“没有权限”。最后演示操作日志每一步操作在日志里都能查到操作人、操作时间、操作内容。这条路径讲下来12分钟到15分钟既完整又有逻辑层次。录像时我建议用OBS录屏分辨率1920x1080帧率30就够了录之前先把浏览器窗口分辨率设置好别录出来上下左右都是黑边。5. 实操中会遇到的问题与排查实录5.1 数据库连接报错Access denied和驱动安装MySQL连接最常见的报错就是Access denied for user rootlocalhost。先是确认密码对不对再确认用户有没有远程访问权限。毕设项目一般本机跑直接root加密码就行不要在这上面浪费时间折腾授权。另一个常见报错是ModuleNotFoundError: No module named MySQLdbSQLAlchemy连接MySQL需要额外装驱动。我的config里用的是PyMySQLSQLALCHEMY_DATABASE_URI mysqlpymysql://root:123456localhost:3306/pharmacy_system?charsetutf8mb4连接串的charsetutf8mb4一定要带上否则中文数据容易出编码问题。数据库本身也要用utf8mb4字符集创建CREATE DATABASE pharmacy_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;5.2 中文乱码问题从数据库到页面层层把关中文乱码的根源通常是三层中有一层编码不一致数据库字符集、连接字符集、页面字符集。数据库表统一用utf8mb4。连接串带charsetutf8mb4。HTML模板的head里写meta charsetutf-8。Flask的响应用app.config[JSON_AS_ASCII] False确保接口返回的JSON里中文不变成\uXXXX。我遇到过一次很诡异的情况数据库里存的中文正常页面列表也正常但导出CSV文件之后用Excel打开全是乱码。排查到最后发现是CSV编码问题解决办法是在文件开头加BOMoutput.write(\ufeff)Excel就能正确识别UTF-8了。这种小坑你不实际踩过一遍光看文档是想不到的。5.3 外键约束导致删除药品失败删除一个已经被入库记录引用的药品时数据库会报外键约束错误页面直接500。这是个设计问题不是bug。合理的做法是前端提示“该药品存在入库或销售记录无法删除建议改为停用状态”而不是硬删。我给medicines表加了一个status字段默认为enabled停用时改为disabled。药品列表默认只显示启用状态的药但管理员可以勾选“查看已停用药品”。这样既保留了历史数据的完整性又不需要真的删记录。这个方案在很多真实业务系统里也是这么做的答辩的时候讲出来能体现你对数据一致性的理解。5.4 时间字段datetime和date要分清药品的有效期只精确到“天”所以用DateField存expiry_date。而销售时间需要精确到时分秒用DateTimeField。write操作的时候避免直接用datetime.now()去赋值DateField字段那会存进去一个带时分秒的时间第二天你按日期查就查不到了。正确的做法是用date.today()取date类型或者datetime.now().date()截断时间。查询“今天所有销售记录”的时候用today_start datetime.combine(date.today(), datetime.min.time()) today_end datetime.combine(date.today(), datetime.max.time()) orders SalesOrder.query.filter(SalesOrder.sale_time.between(today_start, today_end)).all()这样可以精确锁定当天的数据不会把昨天的记录带进来。5.5 手工测试还不够建议写几个自动化接口测试调试期一遍遍在浏览器里点来点去效率确实低。我后来给核心业务流程写了几个简单的pytest测试专门验证“入库后库存增加、出库后库存减少、超库存销售被拒绝”这三条核心逻辑。测试代码不长但每次改了库存相关代码后跑一遍就能快速知道有没有改坏原来的功能。import pytest from app import create_app, db from models import Medicine, MedicineBatch def test_stock_in_and_out(): app create_app(testing) with app.app_context(): db.create_all() medicine Medicine(name测试药品, stock_quantity0, ...) db.session.add(medicine) db.session.commit() # 入库 100 create_stock_in(medicine.id, ...) assert medicine.stock_quantity 100 # 出库 30 create_sale_order([(medicine.id, 30)], ...) assert medicine.stock_quantity 70这个测试的价值在后期改代码时体现得尤其明显。没有测试保护很多重构都是一次“拆东墙补西墙”的冒险。6. 答辩前准备与项目交付规范6.1 演示录像要看什么、怎么用“免费领源码演示录像”里的演示录像不只是让你“看个大概”它是一个完整的样例演示。我建议按下面的方法用第一遍正常速度看完目的是建立整体印象。第二遍跟着操作录像播到哪一步你在本地项目里就做到哪一步重点观察页面参数和数据变化。第三遍只看关键业务入库、出库、预警对照数据库里的记录变化反推代码逻辑。第三遍尤其重要因为答辩时老师最喜欢的提问方式就是“你说说刚才那笔销售单后台数据库里涉及哪几张表的哪些字段”你要是只看过录像没操作过数据库这个问题直接卡住。6.2 答辩高频问题清单与回答思路根据我带过的学生反馈答辩老师针对这类系统最常问的问题集中在下面几个方向“你的系统有哪些角色权限是如何控制的”回答思路从users表的role字段、装饰器role_required、以及页面菜单的动态渲染三层去讲。“药品近效期预警是怎么实现的”回答思路强调批次表和expiry_date字段解释60天预警窗口的设定逻辑顺带提一下“近效期先出”的扣减策略。“库存扣减如何防止超卖”回答思路结合with_for_update()行级锁和事务回滚机制讲清楚并发场景下的数据一致性。“药品批次和药品是什么关系”回答思路一个药品可以对应多个批次批次独立记录数量和效期这是医药行业的基本要求。“你的系统有什么不足”回答思路不要说自己没做X要说“当前版本做了Y但实际应用中还可以扩展Z”比如我可以说“目前的报表是定期统计后续可以改成实时流式统计或者引入采购预测算法”。这个问题考察的是你对系统边界和后续演进方向的认知答好了反而加分。6.3 学术诚信与自主开发边界说点掏心窝子的建议。拿源码做参考、学习、甚至在此基础上二次开发都是正常的。但“拿源码改个标题就交”这种事风险极大。现在很多学校的论文查重系统和代码查重系统都在升级代码相似度过高是会被判学术不端的。正确姿势是把免费源码当成一个“可运行的参考实现”在理解它之后用自己的方式重写核心模块。比如它用了Bootstrap你可以换个AdminLTE皮肤它用了Flask的蓝图你可以尝试把视图拆成模块它没做权限控制你就自己加一个它的预警逻辑是30天你改成60天并增加分档。这些痕迹叠加起来你的项目就会自然地和原版区分开。6.4 项目交付清单一个完整的毕设交付通常需要这几样东西项目源码可运行、演示录像、毕业设计论文、答辩PPT、数据库初始化脚本。源码里记得放一份requirements.txt和READMEREADME里写清楚Python版本、依赖安装命令、数据库初始化命令、启动命令。我建议的依赖版本是Python 3.10 Flask 2.3 SQLAlchemy 2.0 PyMySQL 1.1 Bootstrap 4。这几个版本组合我实测过兼容性比较稳不要盲目追新版本尤其是Python 3.13出来之后部分库的C扩展还没跟上可能装上就报错。写在最后做这个项目的过程我最大的体会是医药管理系统真正难的不是“增删改查”而是让数据关系符合医药业务的真实逻辑。批次、效期、库存、流水四者的联动关系理清楚了这个系统就立住了理不清楚页面再多也只是花架子。你拿到任何一份源码第一件事不要急着跑起来先打开数据库看表结构再点开演示录像看操作最后回到代码里找对应逻辑——把这三步走完这套系统基本上就是你自己的了。再分享一个实用的小技巧做演示之前先在数据库里准备一批“看起来真实”的数据比如每个药品的入库批号和有效期都有三四个批次销售记录分散在过去30天里统计报表图表才好看。别用空数据库演示那几个光秃秃的柱状图让人一看就露怯。祝你这个项目顺利过关。