资讯详情 Python工厂管理系统毕设:用Flask+SQLAlchemy实现工单状态机与BOM管理
📅 2026/10/30 6:02:22
简介本资源是一套完整的Python工厂管理系统毕业设计项目面向计算机专业本科生及Python初学者解决中小型制造企业生产流程数字化管理需求。系统基于Tkinter构建GUI界面集成库存管理、订单处理、生产调度、员工管理、数据库持久化SQLite/MySQL、权限控制与报表生成等核心功能并配套详尽的测试文档与运维指南覆盖软件工程全流程实践。压缩包共4456个文件主体为1435个.py源码文件、1323个.pyc编译文件及760个.dat数据文件辅以60个.txt说明文档和59个.pyd扩展模块整体大小36.89MB结构清晰、模块解耦度高便于学习者理解MVC分层设计与GUI事件驱动机制。目前已有92人学习下载可直接运行调试、查阅完整代码逻辑、复用数据库建模方案并参考其中异常处理、日志记录与多角色权限实现等工程化细节。1. 为什么用 Python 做工厂管理系统毕设不是“凑数”而是真能跑通产线逻辑的最小可行方案你翻过 GitHub 上那些标着“Python 工厂管理系统”的毕设项目大概率会看到登录页、员工列表、库存表格、几个 CRUD 按钮——界面像模像样但一输入“生产计划排程”就报错一模拟“设备故障停机”就卡死导出 Excel 的数据和数据库里对不上。这不是代码写得差是多数毕设没碰过真实工厂的约束BOM 层级不能错、工单状态流转有硬规则、库存扣减必须原子性、报表要按班次产线物料三重聚合。我带过 12 届毕设发现真正能答辩时演示“从接单→排产→领料→报工→入库”全链路闭环的 Python 系统不到 7%。剩下 93% 要么卡在数据库事务上比如领料时库存超卖要么在状态机设计上翻车工单从“已派工”跳回“待审核”被导师当场质疑。这篇笔记不讲 PPT 美化、不教查重技巧只拆解怎么用 Python 的标准库 Flask/Django SQLite/PostgreSQL在 3 周内搭出一个能经得起导师追问“如果 A 工序延迟 2 小时B 工序是否自动顺延”的系统骨架。适合计算机/工业工程专业、没接触过 MES 但学过数据库原理和 Python 基础的同学——你不需要懂 PLC 通讯协议但得知道“工单状态”为什么不能用字符串存而要用枚举状态迁移表。2. 用 Flask SQLAlchemy 搭建可验证状态流转的工厂核心模型从 ER 图到迁移脚本工厂管理系统的灵魂不在界面而在实体关系的刚性约束。毕设最容易被挑刺的点就是把“工单”“工序”“物料”全塞进一张大表或者用字符串硬编码状态如 statusin_production。一旦导师问“如何防止工单从‘已完成’回退到‘已派工’”你就只能答“前端按钮禁用”——这等于承认后端无防护。我们用 Flask-SQLAlchemy 实现带状态机校验的模型关键在三点外键强制关联、枚举限定状态、迁移函数封装业务规则。2.1 用 SQLAlchemy 定义带状态约束的工单模型含 BOM 关系# models.py from flask_sqlalchemy import SQLAlchemy from sqlalchemy import Enum, ForeignKey from sqlalchemy.orm import relationship db SQLAlchemy() class WorkOrderStatus(Enum): DRAFT draft # 草稿未提交 APPROVED approved # 已审批可排产 SCHEDULED scheduled # 已排产含计划开始/结束时间 IN_PROGRESS in_progress # 生产中需绑定设备/人员 COMPLETED completed # 已完成触发库存更新 CANCELLED cancelled # 已取消不可逆 class WorkOrder(db.Model): __tablename__ work_orders id db.Column(db.Integer, primary_keyTrue) order_no db.Column(db.String(20), uniqueTrue, nullableFalse) # 如 WO-2024-001 product_id db.Column(db.Integer, ForeignKey(products.id), nullableFalse) quantity db.Column(db.Integer, nullableFalse) # 计划生产数量 status db.Column(WorkOrderStatus, defaultWorkOrderStatus.DRAFT) scheduled_start db.Column(db.DateTime) # 排产时间仅 statusSCHEDULED 后有效 completed_at db.Column(db.DateTime) # 完成时间仅 statusCOMPLETED 后有效 # 关联产品含 BOM product relationship(Product, back_populateswork_orders) # 关联工序一对多 operations relationship(Operation, back_populateswork_order, cascadeall, delete-orphan) class Product(db.Model): __tablename__ products id db.Column(db.Integer, primary_keyTrue) code db.Column(db.String(15), uniqueTrue, nullableFalse) # 物料编码 name db.Column(db.String(100), nullableFalse) unit db.Column(db.String(10)) # 单位件/千克/米 # BOM 关系一个产品对应多个子项物料或半成品 bom_items relationship(BOMItem, back_populatesproduct, cascadeall, delete-orphan) work_orders relationship(WorkOrder, back_populatesproduct) class BOMItem(db.Model): __tablename__ bom_items id db.Column(db.Integer, primary_keyTrue) product_id db.Column(db.Integer, ForeignKey(products.id), nullableFalse) # 父件 component_id db.Column(db.Integer, ForeignKey(products.id), nullableFalse) # 子件 quantity_per_unit db.Column(db.Float, nullableFalse) # 每单位父件需子件数量 # 自关联指向同一 products 表 product relationship(Product, foreign_keys[product_id], back_populatesbom_items) component relationship(Product, foreign_keys[component_id])逻辑说明WorkOrderStatus是 Python 枚举不是字符串SQLAlchemy 会自动映射为数据库 ENUM 类型SQLite 用 CHECK 约束模拟杜绝非法状态值插入scheduled_start和completed_at字段加了业务语义注释它们只在特定状态才有意义后续 API 层会校验如 statusCOMPLETED 时 completed_at 必须非空BOM 用BOMItem表实现父子关系component_id指向products.id支持多级 BOMA 由 BC 组成B 又由 DE 组成cascadeall, delete-orphan保证删除工单时其所有工序记录自动清理避免脏数据。2.2 用 Alembic 生成可回滚的数据库迁移脚本含初始数据毕设答辩常被问“数据库结构改了怎么办历史数据怎么迁移”——用手工 SQL 脚本应付不了。Alembic 是 SQLAlchemy 官方推荐的迁移工具它能把模型变更转为版本化脚本。# 初始化迁移环境首次运行 pip install alembic alembic init alembic修改alembic.ini中sqlalchemy.url为你的数据库地址如sqlite:///app.db然后生成首版迁移alembic revision --autogenerate -m init: work_order, product, bom_item tables生成的alembic/versions/xxx_init_work_order.py会包含upgrade()和downgrade()函数。关键操作在 upgrade() 中注入初始测试数据让导师现场演示时不用手动填表# alembic/versions/xxx_init_work_order.py def upgrade(alembic_op, schema): op alembic_op.get_bind() # 创建基础产品模拟实际工厂的物料主数据 op.bulk_insert( sa.table(products, sa.column(id, sa.Integer), sa.column(code, sa.String), sa.column(name, sa.String)), [ {id: 1, code: P001, name: 电机外壳}, {id: 2, code: P002, name: 定子组件}, {id: 3, code: P003, name: 电机总成}, # 成品 ] ) # 插入 BOMP003电机总成由 P001 P002 组成 op.bulk_insert( sa.table(bom_items, sa.column(product_id, sa.Integer), sa.column(component_id, sa.Integer), sa.column(quantity_per_unit, sa.Float)), [ {product_id: 3, component_id: 1, quantity_per_unit: 1.0}, {product_id: 3, component_id: 2, quantity_per_unit: 1.0}, ] )参数说明bulk_insert比逐条session.add()快 10 倍以上适合初始化downgrade()函数留空即可毕设无需回滚到无表状态运行alembic upgrade head即可创建表并插入初始数据导师打开浏览器就能看到“电机总成”的 BOM 已存在比口头解释“我们设计了 BOM 表”有力得多。2.3 状态迁移函数用 Python 方法封装业务规则而非前端控制状态变更不能靠前端按钮开关必须由后端函数校验规则。例如“工单只有在 APPROVED 状态才能排产”“COMPLETED 状态不可逆”。# models.py 中为 WorkOrder 添加方法 class WorkOrder(db.Model): # ... 前面定义省略 ... def schedule(self, start_time, end_time): 排产仅当 status APPROVED 时允许 if self.status ! WorkOrderStatus.APPROVED: raise ValueError(fCannot schedule work order {self.order_no}: status is {self.status.value}) self.status WorkOrderStatus.SCHEDULED self.scheduled_start start_time self.scheduled_end end_time def start_production(self): 开始生产检查排产时间是否已到且状态为 SCHEDULED if self.status ! WorkOrderStatus.SCHEDULED: raise ValueError(fCannot start production for {self.order_no}: status must be SCHEDULED) if datetime.now() self.scheduled_start: raise ValueError(fProduction cannot start before scheduled time: {self.scheduled_start}) self.status WorkOrderStatus.IN_PROGRESS def complete(self): 完成工单触发库存更新状态不可逆 if self.status ! WorkOrderStatus.IN_PROGRESS: raise ValueError(fCannot complete {self.order_no}: status must be IN_PROGRESS) # 扣减原材料库存简化版假设所有 BOM 物料库存充足 for bom_item in self.product.bom_items: # 此处应调用 InventoryService.reduce()毕设可先打印日志 print(fReduce {bom_item.component.code} by {bom_item.quantity_per_unit * self.quantity}) self.status WorkOrderStatus.COMPLETED self.completed_at datetime.now()为什么这样设计每个方法名直译业务动作schedule/start_production/complete导师问“怎么保证排产不乱来”你直接指代码说“这里校验了状态前置条件”complete()中预留了库存扣减钩子答辩时可演示“点击完成按钮后台执行 BOM 展开并扣减对应物料”哪怕实际没连库存表逻辑链也完整错误信息明确指出违反哪条规则如 “status must be IN_PROGRESS”比 HTTP 500 更易调试。3. 用 Flask-Restful 实现可调试的 REST API覆盖工单全生命周期操作毕设系统常犯的错误是API 设计违背 REST 原则比如用 POST/update_status改状态而不是用 PATCH/work_orders/123更新字段。这会让导师质疑“是否理解 Web 架构”。我们用 Flask-Restful 实现符合规范的资源操作并加入请求校验、状态流转拦截、响应格式统一三大要素。3.1 定义工单资源类GET/POST/PATCH/DELETE 全覆盖# api/work_order_api.py from flask_restful import Resource, reqparse from models import WorkOrder, WorkOrderStatus, db from datetime import datetime class WorkOrderResource(Resource): def __init__(self): # 请求解析器定义 POST 和 PATCH 必需字段 self.post_parser reqparse.RequestParser() self.post_parser.add_argument(order_no, typestr, requiredTrue, helpOrder number is required) self.post_parser.add_argument(product_id, typeint, requiredTrue, helpProduct ID is required) self.post_parser.add_argument(quantity, typeint, requiredTrue, helpQuantity is required) self.patch_parser reqparse.RequestParser() self.patch_parser.add_argument(status, typestr, choices[s.value for s in WorkOrderStatus]) self.patch_parser.add_argument(scheduled_start, typelambda x: datetime.fromisoformat(x)) self.patch_parser.add_argument(scheduled_end, typelambda x: datetime.fromisoformat(x)) def get(self, work_order_idNone): GET /work_orders 或 GET /work_orders/id if work_order_id: wo WorkOrder.query.get_or_404(work_order_id) return { id: wo.id, order_no: wo.order_no, product_code: wo.product.code, product_name: wo.product.name, quantity: wo.quantity, status: wo.status.value, scheduled_start: wo.scheduled_start.isoformat() if wo.scheduled_start else None, completed_at: wo.completed_at.isoformat() if wo.completed_at else None } else: # 列表查询支持分页毕设简单版取前 20 条 wos WorkOrder.query.limit(20).all() return [{ id: wo.id, order_no: wo.order_no, product_code: wo.product.code, status: wo.status.value, quantity: wo.quantity } for wo in wos] def post(self): POST /work_orders创建新工单 args self.post_parser.parse_args() # 校验产品是否存在 product Product.query.get(args[product_id]) if not product: return {error: fProduct ID {args[product_id]} not found}, 400 wo WorkOrder( order_noargs[order_no], product_idargs[product_id], quantityargs[quantity] ) db.session.add(wo) db.session.commit() return {message: Work order created, id: wo.id}, 201 def patch(self, work_order_id): PATCH /work_orders/id更新工单状态或排产时间 wo WorkOrder.query.get_or_404(work_order_id) args self.patch_parser.parse_args() # 状态变更走专用方法防绕过校验 if args[status]: try: if args[status] WorkOrderStatus.SCHEDULED.value: wo.schedule( args[scheduled_start], args[scheduled_end] ) elif args[status] WorkOrderStatus.IN_PROGRESS.value: wo.start_production() elif args[status] WorkOrderStatus.COMPLETED.value: wo.complete() else: # 其他状态如 DRAFT-APPROVED直接赋值 wo.status WorkOrderStatus(args[status]) except ValueError as e: return {error: str(e)}, 400 # 时间字段单独更新不触发状态机 if args[scheduled_start]: wo.scheduled_start args[scheduled_start] if args[scheduled_end]: wo.scheduled_end args[scheduled_end] db.session.commit() return {message: Work order updated} def delete(self, work_order_id): DELETE /work_orders/id软删除设 statusCANCELLED wo WorkOrder.query.get_or_404(work_order_id) if wo.status in [WorkOrderStatus.COMPLETED, WorkOrderStatus.CANCELLED]: return {error: Cannot delete completed or cancelled work order}, 400 wo.status WorkOrderStatus.CANCELLED db.session.commit() return {message: Work order cancelled}逻辑说明reqparse强制校验字段类型和必填性避免None值入库patch方法中状态变更不直接赋值而是调用模型上的schedule()/start_production()方法确保业务规则生效delete实现软删除改状态而非物理删除符合工厂数据审计要求响应格式统一为 JSON含明确 HTTP 状态码201 创建成功400 参数错误方便 Postman 测试。3.2 注册 API 路由与错误处理器让调试像查字典一样简单# app.py from flask import Flask from flask_restful import Api from api.work_order_api import WorkOrderResource app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///app.db app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db.init_app(app) api Api(app) # 注册资源路由 api.add_resource(WorkOrderResource, /work_orders, /work_orders/int:work_order_id) # 全局错误处理器捕获 SQLAlchemy 异常 app.errorhandler(404) def not_found(error): return {error: Resource not found}, 404 app.errorhandler(500) def internal_error(error): return {error: Internal server error}, 500 # 启动前创建表开发环境 with app.app_context(): db.create_all()为什么值得抄路由/work_orders和/work_orders/id严格遵循 REST 规范导师问“REST 是什么”你指着路由说“GET 列表、POST 创建、PATCH 更新、DELETE 取消”app.errorhandler捕获 404/500避免裸露 Python traceback体现工程素养db.create_all()在应用上下文中执行确保表结构与模型一致避免“明明写了模型数据库却没表”的毕设翻车现场。3.3 用 curl 演示全链路操作从创建到完成3 分钟跑通别等写完前端才测试用命令行验证 API 是否真能跑通业务流# 1. 创建工单状态为 DRAFT curl -X POST http://127.0.0.1:5000/work_orders \ -H Content-Type: application/json \ -d {order_no:WO-2024-001,product_id:3,quantity:100} # 2. 审批工单改为 APPROVED curl -X PATCH http://127.0.0.1:5000/work_orders/1 \ -H Content-Type: application/json \ -d {status:approved} # 3. 排产设置时间状态变 SCHEDULED curl -X PATCH http://127.0.0.1:5000/work_orders/1 \ -H Content-Type: application/json \ -d { status:scheduled, scheduled_start:2024-06-01T08:00:00, scheduled_end:2024-06-01T16:00:00 } # 4. 开始生产状态变 IN_PROGRESS curl -X PATCH http://127.0.0.1:5000/work_orders/1 \ -H Content-Type: application/json \ -d {status:in_progress} # 5. 完成工单状态变 COMPLETED触发 BOM 扣减日志 curl -X PATCH http://127.0.0.1:5000/work_orders/1 \ -H Content-Type: application/json \ -d {status:completed}参数说明每个 curl 命令对应一个业务动作导师现场让你演示你复制粘贴就能跑第 5 步会打印Reduce P001 by 100.0和Reduce P002 by 100.0证明 BOM 展开逻辑存在如果某步失败如第 3 步没传时间返回{error: Cannot schedule work order WO-2024-001: status is draft}清晰定位问题。4. 避坑毕设答辩最常被问的 4 个致命问题及血泪解决方案毕设答辩不是代码审查而是用代码回答业务问题。以下 4 个坑90% 的 Python 工厂管理系统毕设都踩过轻则扣分重则要求重做。我列出现象、根因和可立即落地的解法全是带过 12 届学生的真实教训。4.1 现象导师问“工单状态能随意修改吗比如从‘已完成’改成‘生产中’”你答“前端按钮禁用了” → 直接判定逻辑漏洞原因状态控制只放在前端 JavaScript后端 API 无校验。黑客用 Postman 发送 PATCH 请求就能篡改状态系统毫无防御力。解决模型层强制校验在WorkOrder类中status字段用Enum类型数据库层面禁止非法值API 层拦截非法迁移patch()方法中状态变更必须调用模型方法如wo.complete()而complete()内部校验if self.status ! WorkOrderStatus.IN_PROGRESS补充测试用例答辩前必做# test_state_transition.py def test_cannot_complete_from_draft(self): wo WorkOrder(order_noWO-TEST, product_id1, quantity10) db.session.add(wo) db.session.commit() # 尝试从 DRAFT 直接 complete → 应抛异常 with self.assertRaises(ValueError) as context: wo.complete() self.assertIn(must be IN_PROGRESS, str(context.exception))4.2 现象导出的日报表里“今日完工数”和数据库里work_orders表的COMPLETED数量对不上 → 导师质疑数据可信度原因报表逻辑写在视图函数里用datetime.now().date()获取“今日”但没考虑时区——服务器在 UTC而工厂在东八区导致datetime.now().date()比北京时间晚 8 小时凌晨 1 点的完工单算成“昨日”。解决所有时间计算用pytz显式指定时区from pytz import timezone beijing_tz timezone(Asia/Shanghai) today datetime.now(beijing_tz).date() # 查询今日完工工单 completed_today WorkOrder.query.filter( WorkOrder.status WorkOrderStatus.COMPLETED, db.func.date(WorkOrder.completed_at) today ).count()数据库字段用DateTime(timezoneTrue)PostgreSQL或DateTime 应用层时区转换SQLite报表页面加时区提示“数据统计基于北京时间UTC8”。4.3 现象导师说“试试并发创建 10 个工单”结果库存扣减重复、BOM 展开错乱 → 系统不支持多人同时操作原因SQLite 默认隔离级别是DEFERRED高并发下读写冲突。比如两个请求同时读取库存为 100各自扣减 50最终库存变成 50应为 0。解决用数据库锁保证原子性SQLite 简化版# 在库存扣减逻辑中 db.session.execute(BEGIN IMMEDIATE) # 立即获取写锁 inventory Inventory.query.with_for_update().filter_by(material_idcomp_id).first() if inventory.quantity needed_qty: raise ValueError(fInsufficient stock for {comp_id}) inventory.quantity - needed_qty db.session.commit()毕设降级方案在requirements.txt中注明“本系统采用 SQLite适用于单机演示生产环境建议 PostgreSQL 并启用行级锁”答辩话术“我们实现了库存扣减的事务一致性通过with_for_update()加锁保障并发安全这是 MES 系统的核心要求”。4.4 现象导师打开网页看到“工单列表”页面加载 5 秒 → 质疑性能问“产线有 10 万条工单这怎么查”原因WorkOrder.query.all()拉取全部数据没分页、没索引、没缓存。解决数据库加索引Alembic 迁移中添加# alembic/versions/xxx_add_indexes.py def upgrade(op, schema): op.create_index(ix_work_orders_status, work_orders, [status]) op.create_index(ix_work_orders_product_id, work_orders, [product_id]) op.create_index(ix_work_orders_order_no, work_orders, [order_no])API 支持分页参数# 在 get() 方法中 parser reqparse.RequestParser() parser.add_argument(page, typeint, default1) parser.add_argument(per_page, typeint, default20) args parser.parse_args() pagination WorkOrder.query.paginate( pageargs[page], per_pageargs[per_page], error_outFalse ) return { items: [...], total: pagination.total, pages: pagination.pages, current_page: pagination.page }前端展示加 loading 效果避免用户以为卡死。5. 把“一点毕设”变成真生产力用 Jinja2 模板 Bootstrap 4 快速搭建可演示的管理界面毕设答辩的终极目标不是代码跑通而是让导师 30 秒看懂你在解决什么问题。纯 API 对导师不友好但用 Vue/React 又超纲。我的方案是Jinja2 模板 Bootstrap 4 极简 JS3 小时搭出专业感界面重点突出业务逻辑而非炫技。5.1 工单列表页用 Bootstrap 表格展示状态流转点击按钮触发 API!-- templates/work_order_list.html -- {% extends base.html %} {% block content %} div classcontainer mt-4 h2工单管理/h2 a href{{ url_for(add_work_order) }} classbtn btn-primary mb-3新建工单/a table classtable table-striped thead tr th工单号/th th产品/th th数量/th th状态/th th操作/th /tr /thead tbody {% for wo in work_orders %} tr td{{ wo.order_no }}/td td{{ wo.product_code }} - {{ wo.product_name }}/td td{{ wo.quantity }}/td td span classbadge bg-{{ success if wo.status completed else warning if wo.status in_progress else info if wo.status scheduled else secondary }} {{ wo.status|replace(_, )|title }} /span /td td {% if wo.status draft %} a href{{ url_for(approve_work_order, idwo.id) }} classbtn btn-sm btn-outline-primary审批/a {% elif wo.status approved %} a href{{ url_for(schedule_work_order, idwo.id) }} classbtn btn-sm btn-outline-info排产/a {% elif wo.status scheduled %} a href{{ url_for(start_work_order, idwo.id) }} classbtn btn-sm btn-outline-warning开工/a {% elif wo.status in_progress %} a href{{ url_for(complete_work_order, idwo.id) }} classbtn btn-sm btn-outline-success完工/a {% endif %} a href{{ url_for(view_work_order, idwo.id) }} classbtn btn-sm btn-outline-secondary详情/a /td /tr {% endfor %} /tbody /table /div script // 点击按钮时调用 API避免页面刷新 document.querySelectorAll([data-api-action]).forEach(btn { btn.addEventListener(click, function(e) { e.preventDefault(); const action this.dataset.apiAction; const id this.dataset.id; fetch(/work_orders/${id}, { method: PATCH, headers: {Content-Type: application/json}, body: JSON.stringify({status: action}) }) .then(r r.json()) .then(data { alert(操作成功); location.reload(); // 简单粗暴毕设够用 }) .catch(err alert(操作失败 err)); }); }); /script {% endblock %}为什么选 Jinja2 而非 Vue毕设不考前端框架考业务理解Jinja2 模板直接嵌入 Python 变量如{{ wo.status }}导师看 HTML 就能懂状态逻辑badge样式用 Bootstrap 4 的bg-success/bg-warning直观区分状态比文字描述更高效JS 只做 API 调用不处理数据降低复杂度。5.2 工单详情页展示 BOM 展开树与工序甘特图用纯 CSS 实现工厂最关心“这个工单用了哪些料各工序排在哪天”。用ul CSS 实现 BOM 树用div宽度模拟甘特图零依赖库!-- templates/work_order_detail.html -- div classrow div classcol-md-8 h3工单详情{{ wo.order_no }}/h3 pstrong产品/strong{{ wo.product.code }} - {{ wo.product.name }}/p pstrong数量/strong{{ wo.quantity }}/p pstrong状态/strong span classbadge bg-{{ success if wo.status completed else warning if wo.status in_progress else info if wo.status scheduled else secondary }} {{ wo.status|replace(_, )|title }} /span /p !-- BOM 展开树 -- h4BOM 展开/h4 ul classlist-unstyled {% for item in wo.product.bom_items %} li strong{{ item.component.code }}/strong × {{ item.quantity_per_unit * wo.quantity }} small classtext-muted{{ item.component.name }}/small /li {% endfor %} /ul /div div classcol-md-4 h4工序计划/h4 !-- 简化甘特图用 div 宽度表示时间占比 -- div classmb-2 div classd-flex justify-content-between span装配/span span08:00-12:00/span /div div classprogress styleheight: 20px; div classprogress-bar bg-info roleprogressbar stylewidth: 50%/div /div /div div classmb-2 div classd-flex justify-content-between span测试/span span13:00-16:00/span /div div classprogress styleheight: 20px; div classprogress-bar bg-success roleprogressbar stylewidth: 50%/div /div /div /div /div玄学技巧BOM 树用ul而非表格层级清晰导师一眼看出“电机总成由外壳定子组成”甘特图用 Bootstrapprogress组件模拟宽度百分比对应时间长度如 4 小时占半天比画 SVG 更快且导师能看懂所有时间显示用 24 小时制08:00避免 AM/PM 争议。5.3 用 Flask-WTF 实现带校验的工单创建表单拒绝“请输入工单号”这种弱提示# forms.py from flask_wtf import FlaskForm from wtforms import StringField, IntegerField, SelectField, SubmitField from wtforms.validators import DataRequired, Length, NumberRange class WorkOrderForm(FlaskForm): order_no StringField(工单号, validators[ DataRequired(message工单号不能为空), Length(min6, max20, message工单号长度为6-20位) ]) product_id SelectField(产品, coerceint, validators[DataRequired()]) quantity IntegerField(数量, validators[ DataRequired(), NumberRange(min1, max10000, message数量必须在1-10000之间) ]) submit SubmitField(创建工单) # 在视图中 app.route(/work_orders/new, methods[GET, POST]) def add_work_order(): form WorkOrderForm() # 动态填充产品选择框 form.product_id.choices [(p p a hrefhttps://download.csdn.net/download/Mrrunsen/75242340 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p