ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python Flask+Vue物流仓储管理系统开发实战:从架构到部署

Python Flask+Vue物流仓储管理系统开发实战:从架构到部署 做Web开发这几年我接手过的物流仓储类系统真不少但大多数是拿现成的框架改改要么太重要么和业务对不上。这次从零开始用Python Flask Vue组合做了一个快递公司内部的物流仓储管理信息系统整个项目从架构设计到前后端联调再到部署上线全程自己动手。这篇文章就把这次开发的完整思路、关键代码、踩坑记录都整理出来给想自己搭一套类似系统的人做个参考。这个系统解决的核心问题很简单快递公司的仓储环节涉及到入库、出库、库存盘点、物流跟踪、客户信息管理等多个流程靠Excel和纸质单据根本撑不住。系统需要把这些流程线上化让仓库管理员、调度员、财务都能在一个平台里协作同时给管理者提供实时的库存和物流数据。技术选型上我坚持用Flask而不是Django前端用Vue而不是传统的JQuery模板渲染后面会详细说为什么。如果你是个正在学Python的Web开发者想做一个能写到简历里的完整项目或者你在物流公司做信息化需要一个轻量好维护的内部系统又或者你准备做毕业设计需要一套前后端分离的仓储管理参考方案——这篇文章都适用。1. 项目整体设计与技术选型思路1.1 后端选Flask的核心理由现在Python做Web后端有两个主流选择Django和Flask。我这次坚定选Flask不是因为Django不好而是因为这个项目的定位是轻量、灵活、快速交付。Django自带Admin后台、ORM、认证系统、模板引擎功能齐全但对一个快递公司内部仓储系统来说一大半是多余功能。而且Django的重型特性比如全功能ORM、中间件链、模板语言会带来一定的学习和定制成本。Flask则像一把手术刀核心只负责路由和WSGI请求处理其他一切按需引入。我可以只装Flask-SQLAlchemy做数据库操作Flask-CORS解决跨域Flask-JWT-Extended做登录认证想用什么装什么不用的东西一点不冗余。另外仓储系统往往要对接外部设备或第三方物流API比如扫码枪、电子秤、快递单号查询接口。Flask的路由和视图函数写起来非常直观新增一个接口只需要几行代码这种轻量特性在后期的二次开发中优势很明显。顺带一提很多公司里现有的Python脚本和数据分析工具都是用Flask包装成网页服务的这说明Flask在企业内网工具中已经有很广泛的实践基础选它做仓储系统的后端风险很低。1.2 前端Vue带来的开发效率提升前端选Vue的核心理由是组件化开发和响应式数据绑定。仓储管理系统有大量重复的界面模式搜索表单、数据表格、弹出编辑框、状态标签这些用Vue组件化之后复用率极高。我这次用的是Vue 3配合Vite构建工具而不是Vue 2。Vue 3的组合式APIComposition API让逻辑复用变得特别舒服。举个例子货物入库表单和出库表单有大量类似的字段校验逻辑和提交逻辑用组合式函数类似hook抽出来两边各引用一下代码量直接减少三分之一。再说说为什么不用JQuery加服务端模板的老方案。传统的JQuery方式里每一页都要手写DOM操作数据变了要手动更新页面节点而Vue是数据驱动数据一变页面自动响应。再加上Vue内置了组件过渡动画、表单绑定、路由系统做这种管理系统确实是碾压级的效率提升。前端组件库我选的是Element Plus。这套组件的表格、表单、弹窗、日期选择器非常成熟做后台管理界面几乎是开箱即用省去了写大量CSS样式的时间。实际开发下来70%的页面UI靠拖组件拼装就能完成。1.3 核心功能模块拆解快递公司的仓储物流管理业务链路大概是这样的客户发货 → 快递网点揽收 → 转运中心入库 → 分拣出库 → 运输 → 到达网点 → 派送签收。在这个链路里仓储系统要管住的是转运中心这个环节。系统功能模块我拆成了六大块用户与权限管理区分管理员、仓库操作员、调度员、财务等角色不同角色看到不同的菜单和数据范围。基础信息管理维护客户资料、承运线路、仓库库位、货物类型等基础数据这是所有业务单据的字典。入库管理到件登记、入库单创建、库位分配、入库审核。这是所有库存数据的源头。出库管理出库单创建、拣货分配、出库复核、交接确认。这是库存减少的唯一途径。库存管理实时库存查询、库存预警比如某类货物低于阈值提醒补货、库存盘点、库位货物关联查询。物流跟踪与统计报表按运单号查货物当前位置和状态流转记录以及出入库趋势报表、库存周转率统计。模块划分上我遵循一个原则每个模块只干一件事模块之间通过数据库表和接口解耦。比如入库模块只管把货放进系统库存变化通过数据库事务自动关联出库模块只管减库存不关心库存为什么变。这样后期任何模块出问题排查范围都是清晰的。1.4 数据库表结构设计数据库我选的是MySQL 8.0原因无他企业里用MySQL最普遍运维经验多出问题好找人。核心表包括users用户表用户名、密码哈希、角色、状态、创建时间。customers客户表客户名称、联系人、电话、所属区域。warehouses仓库表仓库名称、所在城市、负责人、是否启用。storage_locations库位表所属仓库、库位编码、区域类型、最大容量。goods货物表货物名称、类型、规格、单位、预警库存阈值。inbound_orders入库单表入库单号、来源方式、供应商/客户、操作人、状态、入库时间。inbound_items入库明细表入库单关联、货物ID、数量、库位ID、批次号。outbound_orders出库单表出库单号、出库类型、收货方、操作人、状态、出库时间。outbound_items出库明细表出库单关联、货物ID、数量、库位ID。tracking_records物流跟踪表运单号、节点名称、节点时间、操作人、备注。表设计有个关键点值得说入库单和入库明细一定是分开的。单据头存的是公共信息单号、操作人、时间明细存的是每一笔货物的数量、库位。这样同一个入库单可以关联多笔不同货物符合真实业务场景——一个转运中心的一次到件扫描就是一整车几十票件。2. 环境准备与项目初始化2.1 后端环境配置开发环境我用的是Windows部署环境是CentOS 7服务器。开发阶段最重要的一个建议是不要用全局Python环境一定用虚拟环境隔离项目依赖。创建后端项目目录后执行mkdir warehouse-system cd warehouse-system python -m venv venvWindows下激活虚拟环境venv\Scripts\activateLinux/macOS下激活source venv/bin/activate激活后命令行前缀会出现(venv)这时候再安装依赖所有包都会装进项目自己的环境里不会污染系统Python也不会和别的项目冲突。核心依赖清单pip install flask pip install flask-sqlalchemy pip install flask-cors pip install flask-jwt-extended pip install pymysql pip install python-dotenv把这些写入requirements.txt方便部署的时候一键安装pip freeze requirements.txt我用的是Flask 2.x版本配合SQLAlchemy 2.x。这里有个细节PyMySQL是用来让SQLAlchemy连接MySQL数据库的驱动光装Flask-SQLAlchemy还不够必须装这个驱动才能跑起来。2.2 前端环境配置前端需要Node.js环境我用的版本是Node 16 LTS。装完Node之后npm是随附的包管理器直接用。我用Vite初始化Vue 3项目npm create vuelatest这个命令会交互式问你项目名、是否用TypeScript、是否需要Vue Router、是否需要Pinia等选项。这个系统我们用JavaScript选No/No来跳过TypeScript来减少心智负担但Vue Router和Pinia都选上后面会用。初始化完成之后再安装Element Plus和Axiosnpm install element-plus npm install axios前端开发环境的配置有一个非常容易踩坑的环节——Volar插件的版本兼容性。在VS Code里开发Vue文件一定装VolarVue语言支持并禁用Vue 2时代的Vetur插件。否则Vue 3的组合式API语法提示只会有残缺。2.3 项目目录结构规划好的目录结构能决定这个项目后期好不好维护。我的习惯是后端按业务模块建蓝图Blueprint前端按业务模块建视图目录。后端目录结构backend/ ├── app.py # 入口文件创建Flask应用 ├── config.py # 配置文件数据库连接、密钥等 ├── models/ │ ├── __init__.py │ ├── user.py # 用户模型 │ ├── goods.py # 货物模型 │ ├── order.py # 入库/出库单模型 │ └── tracking.py # 物流跟踪模型 ├── api/ │ ├── __init__.py │ ├── auth.py # 登录/认证接口 │ ├── inbound.py # 入库接口 │ ├── outbound.py # 出库接口 │ ├── inventory.py # 库存接口 │ └── stats.py # 统计报表接口 └── utils/ ├── __init__.py ├── response.py # 统一响应格式封装 └── auth_decorator.py # 权限校验装饰器前端目录结构frontend/ ├── src/ │ ├── api/ # axios请求封装 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia状态管理 │ ├── views/ │ │ ├── login/ # 登录页 │ │ ├── dashboard/ # 首页看板 │ │ ├── inbound/ # 入库管理 │ │ ├── outbound/ # 出库管理 │ │ ├── inventory/ # 库存管理 │ │ └── system/ # 系统管理 │ └── components/ # 公共组件 └── vite.config.js这个结构的好处是前后端各管各的接口路径清晰新同学接手的时候找代码位置只需要按模块名找不用到处翻。2.4 前后端联调的代理与跨域设置开发模式下前端跑在localhost:5173Vite默认端口后端跑在localhost:5000Flask默认端口两者端口不同必然有跨域问题。前端解决跨域最优雅的方式是配置Vite开发服务器代理。在vite.config.js里添加export default defineConfig({ server: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } })这样前端发请求到/api/loginVite会把它转发到http://localhost:5000/api/login浏览器看到的请求是同源的跨域问题直接消失。后端也要配合开启CORS以防万一有非浏览器客户端直接向后端发请求或者生产环境里前端和后端不在同一个域名下。后端这么配from flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})这里多说一句生产环境不要把origins设置成*应该限制为具体的域名比如https://warehouse.example.com。不然任何网站都能向你的后端发跨域请求这是个安全隐患。3. 后端核心接口与业务逻辑实现3.1 用蓝图Blueprint拆分业务模块Flask的蓝图Blueprint机制是这个系统后端架构的基石。它可以让你把不同业务封装成独立的模块注册到主应用上代码既清晰又不影响主入口。创建蓝图的方法很简单。以入库模块为例# api/inbound.py from flask import Blueprint, request, jsonify inbound_bp Blueprint(inbound, __name__, url_prefix/api/inbound) inbound_bp.route(/create, methods[POST]) def create_inbound(): # 入库单创建逻辑 pass然后在入口文件里注册# app.py from api.inbound import inbound_bp app.register_blueprint(inbound_bp)这样所有接口的URL统一带上了/api/inbound前缀路由冲突的概率大大降低。你还可以给蓝图设置不同的url_prefix做到接口路径的语义化。实际开发中我还发现一个技巧蓝图的名称可以结合蓝图内全局中间件使用。比如给某个需要管理员权限的蓝图统一加一个权限校验装饰器这样就不用每个视图函数单独写一遍权限判断。3.2 数据库模型的ORM实现数据库模型我用Flask-SQLAlchemy定义。这里举两个代表性的例子。用户模型from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime db SQLAlchemy() class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(80), uniqueTrue, nullableFalse) password_hash db.Column(db.String(128), nullableFalse) role db.Column(db.String(20), defaultoperator) # admin/operator/dispatcher status db.Column(db.Integer, default1) # 1启用 0禁用 created_at db.Column(db.DateTime, defaultdatetime.now) def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password)密码一定不能存明文我用werkzeug.security的generate_password_hash来做密码哈希验证用check_password_hash。这是安全底线谁存明文密码谁就是在给公司开安全漏洞。入库单模型class InboundOrder(db.Model): __tablename__ inbound_orders id db.Column(db.Integer, primary_keyTrue) order_no db.Column(db.String(32), uniqueTrue, nullableFalse) source_type db.Column(db.String(20)) # 到件/退货/调拨 supplier_name db.Column(db.String(100)) operator_id db.Column(db.Integer, db.ForeignKey(users.id)) status db.Column(db.Integer, default0) # 0待审核 1已入库 2已取消 remark db.Column(db.String(255)) created_at db.Column(db.DateTime, defaultdatetime.now) items db.relationship(InboundItem, backreforder, lazydynamic)order_no生成规则我建议用当前日期 随机序号比如IB20240115001。保证唯一性的同时人眼一看就知道是什么时候的单。日期加上随机数的组合可以用uuid4().hex[:8]补足后几位避免并发冲突。数据库连接配置写在config.py里import os from dotenv import load_dotenv load_dotenv() class Config: SQLALCHEMY_DATABASE_URI fmysqlpymysql://{os.getenv(DB_USER)}:{os.getenv(DB_PASS)}{os.getenv(DB_HOST)}:3306/{os.getenv(DB_NAME)}?charsetutf8mb4 SQLALCHEMY_TRACK_MODIFICATIONS False JWT_SECRET_KEY os.getenv(JWT_SECRET_KEY, dev-secret-key)注意配置里的charsetutf8mb4这个字符集才能完整支持中文、Emoji以及生僻字。用utf8在存入一些特殊字符比如一些生僻姓氏、繁体字的时候会报编码错误。3.3 入库出库流程的关键接口逻辑入库流程从业务角度来说是登记到件并上架到库位。接口核心代码如下inbound_bp.route(/create, methods[POST]) jwt_required() def create_inbound(): data request.get_json() # 校验必填字段 if not data.get(items) or len(data[items]) 0: return jsonify({code: 400, msg: 入库明细不能为空}), 400 try: # 在事务里执行要么全成功要么全失败 order_no generate_order_no(IB) inbound_order InboundOrder( order_noorder_no, source_typedata.get(source_type), supplier_namedata.get(supplier_name), operator_idget_jwt_identity(), remarkdata.get(remark) ) db.session.add(inbound_order) db.session.flush() # 先拿到自增ID for item in data[items]: detail InboundItem( order_idinbound_order.id, goods_iditem[goods_id], quantityitem[quantity], location_iditem[location_id], batch_noitem.get(batch_no, ) ) db.session.add(detail) db.session.commit() return jsonify({code: 200, msg: 入库单创建成功, data: {order_no: order_no}}) except Exception as e: db.session.rollback() return jsonify({code: 500, msg: f创建失败: {str(e)}}), 500这里有一个细节特别值得讲数据库事务的原子性。入库单和入库明细是多张表的写入任何一步失败都会造成数据不一致。所以我把整个创建流程包在事务里任何异常都回滚不做半截子数据。这个习惯在写所有涉及多表更新的接口时必须坚持。出库接口的逻辑类似但多了库存扣减的校验outbound_bp.route(/create, methods[POST]) jwt_required() def create_outbound(): data request.get_json() for item in data[items]: goods_id item[goods_id] quantity item[quantity] # 查询当前库存 inventory Inventory.query.filter_by( goods_idgoods_id, location_iditem[location_id] ).first() if not inventory or inventory.quantity quantity: return jsonify({code: 400, msg: f货物ID {goods_id} 库存不足}), 400 # 校验通过后创建出库单并扣减库存 ...库存扣减务必做成原子操作。简单写inventory.quantity - quantity在并发场景下两个操作员同时出库同一商品可能产生超卖。正确做法是用SQLAlchemy的update语句配合行级锁from sqlalchemy import update stmt update(Inventory).where( Inventory.id inventory.id, Inventory.quantity quantity ).values(quantityInventory.quantity - quantity) result db.session.execute(stmt) if result.rowcount 0: return jsonify({code: 400, msg: 库存不足或商品已更新}), 400这种写法能保证在并发环境下不会把库存扣成负数rowcount为0说明条件不满足库存不足直接拒绝。3.4 在系统里加入关键词相似度匹配的智能检索这个系统的亮点之一我在设计货物与运单检索功能时引入了关键词相似度匹配的思路。传统的SQL模糊搜索LIKE %关键词%在面对拼写不规范、别名不同、打错字的情况时效果很差。举一个真实场景货物名称可能叫作手机壳、手机保护壳、Phone Case如果用户搜索手机外套传统模糊搜索什么也匹配不到。但如果引入相似度算法哪怕搜索词和货物名称不完全一致也能搜出最接近的结果。我用的方案是Jaccard相似度 字面重叠度的加权匹配。核心逻辑很简单def similarity_score(search_key, goods_name): # 将字符串转为字符集合 set_a set(search_key) set_b set(goods_name) # 计算Jaccard相似度 intersection len(set_a set_b) union len(set_a | set_b) jaccard intersection / union if union 0 else 0 # 计算字面覆盖度搜索词里有多少比例出现在目标中 cover sum(1 for ch in search_key if ch in goods_name) / len(search_key) # 加权得分 score 0.4 * jaccard 0.6 * cover return score实际检索时把某条货物的名称和历史搜索记录里的常用叫法存进一张goods_alias货物别名表里然后用户输入关键词时对货物主名称和所有别名都计算相似度取最高值作为匹配分超过60分以上就作为候选结果返回。为了更精准地匹配中文关键词我用jieba分词做预处理import jieba def tokenize_text(text): return set(jieba.cut(text))把分词后的集合作为相似度计算的粒度而非单个字效果会好很多。比如手机保护壳分词后是[手机, 保护壳]和手机外套的[手机, 外套]能计算出一个合理的相似度虽然不完全匹配但至少提示了手机这个核心字段。这个功能在货物检索和运单号联想匹配的应用场景里非常实用。当一个客户打电话说我那个手机配件到了没有操作员在搜索框输入手机配件系统就能把手机壳、手机膜、手机支架等相近货物全部列出来再按得分排序。同时这种算法也用来做无效信息过滤。搜索时如果用户输入的是一个不规范的杂字符串和库里所有货物名称的相似度都低于40分系统会直接提示未找到相关货物而不是返回一堆无关结果。这就把模糊搜索的召回率控制在了合理区间。当然这套简易算法和自然语言理解不是一个量级但在一个轻量化的内部系统里完全够用而且没有额外依赖部署时不需要装庞大的搜索引擎组件。如果你后期有更多预算或者更复杂的搜索需求再替换成ESElasticsearch也不晚。3.5 身份认证与权限控制登录认证我用的是JWTJSON Web Token。流程很简单用户提交用户名密码后端验证通过后签发一个带有效期的token前端保存token并在后续请求的Header中携带后端每次请求通过token解析出用户身份。Flask集成用的是flask-jwt-extendedfrom flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity app.route(/api/auth/login, methods[POST]) def login(): data request.get_json() user User.query.filter_by(usernamedata.get(username)).first() if not user or not user.check_password(data.get(password)): return jsonify({code: 400, msg: 用户名或密码错误}), 400 if user.status ! 1: return jsonify({code: 400, msg: 账号已被禁用}), 400 token create_access_token(identityuser.id, expires_deltatimedelta(hours12)) return jsonify({ code: 200, msg: 登录成功, data: { token: token, username: user.username, role: user.role } })权限控制上我用了一个最简单的方案封装一个角色装饰器。from functools import wraps from flask_jwt_extended import get_jwt_identity def admin_required(f): wraps(f) def decorated(*args, **kwargs): user_id get_jwt_identity() user User.query.get(user_id) if user.role ! admin: return jsonify({code: 403, msg: 需要管理员权限}), 403 return f(*args, **kwargs) return decorated然后用的时候直接加在视图函数上inbound_bp.route(/delete/int:order_id, methods[DELETE]) jwt_required() admin_required def delete_inbound(order_id): ...这样的权限模型虽然简单但对一个内部系统来说已经够用。角色就三种管理员、操作员、调度员对应的权限矩阵在系统里用一张表维护清楚就行。如果后续要扩展更细粒度的权限比如按仓库划分数据范围可以把token里encode更多的claim比如role和warehouse_id。4. Vue前端核心页面与交互细节4.1 路由与导航框架搭建前端路由我用Vue Router 4。路由的设计思路是登录页独立布局登录后的主界面采用侧边栏 顶栏的经典布局。主路由结构const routes [ { path: /login, component: () import(/views/login/Login.vue), meta: { public: true } }, { path: /, component: () import(/layouts/MainLayout.vue), redirect: /dashboard, children: [ { path: dashboard, component: () import(/views/dashboard/Dashboard.vue), meta: { title: 首页看板, icon: Odometer } }, { path: inbound, component: () import(/views/inbound/InboundList.vue), meta: { title: 入库管理, icon: Download } }, // ...其余模块路由 ] } ]这里有个实用的技巧在meta里配置标题和图标侧边栏菜单就通过遍历路由自动生成。这样加新页面的时候只需要在路由配置里加一条记录菜单就会自动出现不用去菜单组件里手动增删。路由导航守卫用来做登录保护router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.public) { next() } else if (!token) { next(/login) } else { next() } })这个守卫逻辑简单但非常关键它保证了未登录用户无法访问任何业务页面同时登录用户访问登录页会被自动转发到主界面。4.2 登录页与状态管理登录页的逻辑很简单但要注意几个细节输入框的校验规则、登录状态对提交按钮的禁用、密码的显示/隐藏切换。核心代码逻辑const formRef ref(null) const formData reactive({ username: , password: }) const rules { username: [{ required: true, message: 请输入用户名, trigger: blur }], password: [{ required: true, message: 请输入密码, trigger: blur }] } async function handleLogin() { const valid await formRef.value.validate() if (!valid) return loading.value true try { const res await loginApi(formData) localStorage.setItem(token, res.data.token) localStorage.setItem(username, res.data.username) localStorage.setItem(role, res.data.role) router.push(/dashboard) } finally { loading.value false } }我用Pinia管理全局状态。登录成功后的用户信息放进Pinia的store里而不是只存在localStorage。因为localStorage读取是同步且不响应式的当用户信息在页面A被修改页面B不会自动更新。Pinia store天然是响应式的一处修改处处生效。// stores/user.js export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , username: localStorage.getItem(username) || , role: localStorage.getItem(role) || }), actions: { logout() { this.token this.username this.role localStorage.removeItem(token) router.push(/login) } } })4.3 入库登记和库存管理的表单交互入库登记页是整个系统中功能逻辑最复杂的页面。里面包含一个主表单填写供应商、来源等一个明细表格动态添加货物、库位、数量以及一个提交审核按钮。明细表格用Element Plus的el-table配合动态数据绑定用户添加一行货物明细表格就出现一行输入时实时校验。el-table :dataitems border el-table-column label货物 min-width150 template #default{ row } el-select v-modelrow.goods_id filterable placeholder选择货物 el-option v-forg in goodsList :keyg.id :labelg.name :valueg.id / /el-select /template /el-table-column el-table-column label数量 width120 template #default{ row } el-input-number v-modelrow.quantity :min1 / /template /el-table-column el-table-column label库位 width120 template #default{ row } el-select v-modelrow.location_id placeholder选择库位 el-option v-forloc in locationList :keyloc.id :labelloc.code :valueloc.id / /el-select /template /el-table-column el-table-column label操作 width80 template #default{ $index } el-button typedanger link clickremoveItem($index)删除/el-button /template /el-table-column /el-table这里有个很关键的交互细节货物选择器必须加filterable属性。仓库里的货物种类很多如果用户要滚动一千条下拉选项找人体验非常差。加了filterable之后用户可以直接在输入框里打字过滤。再配合后端的关键词相似检索接口输入能联想出最相近的货物操作效率能提高好几倍。表单数据校验要在前端做一层防止无效数据直接打到后端。比如数量必须为正整数、库位不能为空、至少有一行明细。Element Plus的el-form规则校验机制可以把这些约束集中管理。4.4 数据列表的分页、搜索与状态标签仓储系统的列表页基本都是同一种模式顶部是搜索条件区中间是数据表格底部是分页组件。我封装了一个通用的搜索列表组合式函数把所有模块复用逻辑抽出来。// composables/useSearchList.js export function useSearchList(fetchApi) { const list ref([]) const total ref(0) const loading ref(false) const queryParams reactive({ page: 1, pageSize: 10, keyword: , status: , dateRange: [] }) async function fetchList() { loading.value true try { const res await fetchApi(queryParams) list.value res.data.list total.value res.data.total } finally { loading.value false } } function handleSearch() { queryParams.page 1 fetchList() } function handlePageChange(page) { queryParams.page page fetchList() } return { list, total, loading, queryParams, fetchList, handleSearch, handlePageChange } }这个组合式函数弥补了我之前用Vue 2 Options API时期最大的痛点每个搜索页面都要复制粘贴七八个方法改动一点逻辑就要动所有页面。现在用这个抽取的公共逻辑每个列表页只需要额外处理本页的特殊字段和特殊操作代码量十分精简。列表状态标签我用一个小组件来渲染比如入库状态template el-tag :typestatusType{{ statusText }}/el-tag /template script setup const props defineProps({ status: { type: Number, required: true } }) const statusMap { 0: { text: 待审核, type: warning }, 1: { text: 已入库, type: success }, 2: { text: 已取消, type: info } } const statusText computed(() statusMap[props.status]?.text || 未知) const statusType computed(() statusMap[props.status]?.type || info) /script后续加新状态只需要改statusMap不用再改调用的地方。这种小组件在多模块复用的场景下节省的时间可以累计成以天为单位的效率提升。5. 部署、常见问题与避坑手册5.1 Flask生产环境部署开发环境里Flask自带的服务器只是单线程只能用于调试千万别直接拿来当线上服务器。生产环境部署我用的是Gunicorn等待WSGI Nginx反向代理 Supervisor进程守护这套经典组合。在服务器上创建虚拟环境并拉取代码后cd /var/www/warehouse-system python3 -m venv venv source venv/bin/activate pip install -r requirements.txt gunicorn -w 4 -b 127.0.0.1:5000 app:app-w 4表示启动4个工作进程能同时并发处理请求。-b 127.0.0.1:5000表示只绑定本机端口不直接对外暴露由Nginx做一层反向代理。进程守护用Supervisor可以确保Gunicorn挂了之后自动重启[program:warehouse-api] command/var/www/warehouse-system/venv/bin/gunicorn -w 4 -b 127.0.0.1:5000 app:app directory/var/www/warehouse-system autostarttrue autorestarttrue userwww-data stderr_logfile/var/log/warehouse-api.err.log stdout_logfile/var/log/warehouse-api.out.log这个方案已经被无数生产项目验证过稳定性和易维护性都很好。5.2 Vue构建后的静态文件托管前端构建产物部署方式有两种独立域名部署和Flask托管。我这次选择了由Flask托管静态文件因为公司内部系统没有独立域名的资源把前端打包后的文件放到Flask的static目录里一个服务搞定前后端更省事。前端执行构建npm run build构建完成后dist目录下是纯静态文件。把它拷贝到后端目录下cp -r dist/* /var/www/warehouse-system/static/然后Flask那边做一个兜底路由app.route(/, defaults{path: }) app.route(/path:path) def serve_frontend(path): # 如果路径请求的是静态资源文件而且存在则直接返回 if path and os.path.exists(os.path.join(app.static_folder, path)): return send_from_directory(app.static_folder, path) # 否则返回首页index.html交给Vue Router处理前端路由 return send_from_directory(app.static_folder, index.html)这个兜底路由特别重要。因为Vue Router默认是history模式页面路径比如/inbound/list在服务器上不存在对应的物理文件如果不做兜底刷新页面就会404。这个index.html兜底就是解决前端路由刷新404问题的经典方案。5.3 典型问题速查表开发到部署的过程中我整理出来的高频问题表问题现象根本原因解决方法Vue页面刷新后404前端history路由在服务器上没有对应文件Nginx配置try_files $uri $uri/ /index.html;或者用Flask兜底路由附件/图片上传后无法打开上传目录不存在或权限不足创建上传目录并设置读写权限chmod -R 755 uploads后端接口返回中文乱码数据库连接URL没加charsetutf8mb4在SQLAlchemy连接串末尾追加?charsetutf8mb4跨域CORS报错前后端不在同一域名或端口开发用Vite代理生产用Nginx反向代理或配置CORS白名单SQLAlchemy操作MySQL报Lost connection数据库连接被空闲回收配置连接池参数SQLALCHEMY_ENGINE_OPTIONS{pool_recycle: 3600}上传的文件下载后是损坏文件写了文件名但是没写文件内容检查send_from_directory时路径拼接是否正确Token过期后前端还在发请求前端响应拦截逻辑缺失拦截401响应并自动跳转登录页批量扣库存出现负数并发更新问题使用带条件的UPDATE语句用rowcount判断是否成功表里的每个问题都是真实踩过的坑。举个例子来说明附件路径错误这个经典问题Windows开发环境和Linux生产环境的路径分隔符不同我一开始写的是file_path os.path.join(app.config[UPLOAD_FOLDER], filename)这个写法本身没毛病但我在前端上传文件时后端代码是save成功返回了然而最终生成的文件却下载不了。排查了很久发现问题不是文件写失败而是FTP/代码传输工具在部署时把中文文件名编码弄乱了。后来我的解决方案是上传时必须重命名文件用UUID作为文件名原始文件名存到数据库里。这样彻底规避了中文文件名编码问题。5.4 我的一点个人实操心得按我的经验这个项目如果从零开始正常节奏是四周左右能出一个可用版本其中前后端各占两周。如果你是一个人在做这个项目我强烈建议你先把后端所有接口按文档列出来前端照着接口文档开发不然前后端进度容易互相卡脖子。物流仓储系统看起来业务复杂其实核心就是入库加库存出库减库存查询看库存。先把这三点打通后面所有的功能都是在这条主线上做加法。系统跑起来之后再上线进销存报表、库存周转率分析这些功能开发压力会小非常多。前后端联调的时候最省力的工具是Apifox或者Postman。先把后端每个接口都调试通了再写前端页面对接。千万不要一边写前端一边调接口同时排查前端和后端的问题会非常崩溃。最后说个小技巧项目里所有时间字段建议统一用datetime类型存数据库接口层返回ISO格式字符串前端用dayjs解析格式化。这样避免了前后端时间格式不一致的坑。别问我为什么知道这个坑问就是拆过别人留下的烂摊子。
返回列表