开篇:为什么改造开源 WMS 而不是从零自研

我是 sunrise314,一名后端开发。这个系列记录我把一套开源仓储管理系统(GreaterWMS)改造成生产级系统的完整过程:从技术选型、领域模型、出入库全链路,到并发治理、金额精度、批次效期、外部集成、设备对接,最后是报表看板与部署复盘。全部内容基于真实代码,每一章都有可以直接对照的源码。这一章先回答一个最根本的问题:为什么是改造,而不是自研。
一、接到的需求:一个多货主仓库的数字化
事情的开端是一个很典型的需求:客户有一个多货主的仓储场景——同一个仓库里放着好几个货主的货,进出库都要按货主隔离,还要对接他们的 ERP 出入库单据。仓库里有 PDA 扫码枪,未来可能上 AGV 搬运车。预算不支持买一套商用 WMS 的 license,也不想为了"万一以后要改"把命运交到别人的闭源代码手里。
摆在面前的是三条路:
| 路线 | 优点 | 缺点 |
|---|---|---|
| 从零自研 | 完全可控、技术栈随意 | 仓储业务模型极其庞杂,从库存账到波次拣货全是细节坑,周期以年计 |
| 商业 WMS | 开箱即用、有实施团队 | 价格高,深度定制受制于厂商,二开黑盒 |
| 开源改造 | 业务底座现成、代码全透明 | 上游代码质量参差,需要一次"生产化体检" |
仓储业务的复杂度经常被低估。表面上就是"进货、出货、记库存",但真正做起来会发现:收货有预装载、预分拣、分拣、上架四个动作;发货有订单下发、波次、拣货、调度、回单;库存不是一个数字,而是在手、在途、待检、冻结、待拣、已拣、缺货等十几个状态并存的账户体系;再加多货主隔离、库位管理、批次效期、盘点流水——这些东西没有实际踩过坑的人写不出来。
自研最大的风险不是写不出代码,而是业务模型的缺口。你可以在三个月内搭出一个能跑的 CRUD 系统,但要花一年以上才能补齐一个成熟 WMS 在十年里沉淀的状态机和多维库存设计。所以我的结论很早就倾向了第三条路:找一个业务模型完整的开源 WMS 做底座,把精力花在生产化改造上。
二、GreaterWMS 是什么:福特亚太售后物流的基因
我选的是 GreaterWMS V2.1.54,Apache 2.0 协议,GitHub 上星标最高的开源 WMS 之一。选它有四个理由。
第一,业务基因正统。 它的业务模型源自福特亚太的售后物流仓储供应链流程,覆盖多仓、波次发货、合并拣货、Milk-Run 等模型。售后物流是供应链里最琐碎的场景之一——零件种类极多、单量碎片化、时效要求高——在这种场景里打磨出来的状态机和流程设计,比"演示级"开源项目扎实得多。
第二,架构干净。 前后端彻底分离:后端 Django 只出 RESTful API,前端是独立的 Quasar(Vue)工程,编译产物由 Django 根路由托管。按业务领域拆成 32 个 Django app,每个 app 自带 models / views / serializers / urls,职责单一。我数了一下,全项目 34 个数据模型、158 个 API 端点(path),规模适中——大到业务模型完整,又没有膨胀到读不完。
第三,多租户是原生设计。 几乎所有业务表都带 openid 字段,作为租户/货主隔离键,配合自定义认证实现多货主数据隔离。这正是我那个多货主仓库需求的核心,省掉了自己设计租户模型的一大块工作。
第四,多端统一(OneAPP)。 同一套后端 API 服务 Web、PDA、手机 App:前端基于 Quasar 可以编译成 Web、Android 甚至小程序。仓库现场 PDA 扫码作业是刚需,这一点直接命中。
技术栈盘点如下:
| 层级 | 技术 | 版本 |
|---|---|---|
| 后端框架 | Django | 4.1.2(二开目标 4.2 LTS) |
| REST 框架 | Django REST Framework | 3.14 |
| ASGI 服务器 | daphne / Twisted | 4.0 |
| API 文档 | drf-spectacular(Swagger/Redoc) | 0.26 |
| 认证 | PyJWT(自定义 JWT) | 2.8 |
| 数据处理 | pandas / openpyxl / numpy | — |
| 数据库 | SQLite(默认)/ MySQL / PostgreSQL | — |
| 前端 | Quasar 1.7+ / Vue 2.6+ | — |
我对 Django 不算狂热粉,但必须承认它在这个场景里是合理的:自带 ORM 和 admin,DRF 的序列化器和 ViewSet 让"一个主数据的 CRUD"几乎零成本,32 个 app 的组织方式也让模块边界非常清晰。对 Java 团队来说可以这样类比:Django app ≈ Spring 的 module,ViewSet ≈ Controller + Service 合体,DRF Serializer ≈ DTO + Converter,ORM 迁移 ≈ Flyway(只是生成方向相反——它从模型类生成迁移文件)。
三、先跑起来:Python 3.13 的适配实录
评估一个开源项目,我的习惯是先在本机把它完整跑起来,再谈架构。这一步就遇到了第一个坑:官方 requirements.txt 面向 Python 3.8,而我的环境是 Python 3.13——老项目的依赖锁在三年前,直接装会有一片兼容性报错。
我做了一版依赖适配(requirements-py313.txt),核心升级:
| 依赖 | 原版 | 适配后 |
|---|---|---|
| Django | 4.1.2 | 4.2.19(LTS) |
| djangorestframework | 3.14 | 3.15.2 |
| numpy | 1.23 | 2.1.3 |
| pandas | 1.5 | 2.2.3 |
| asgiref 等 | — | 补齐 `asgihandler` 等缺失项 |
然后按以下步骤初始化:
# 1) 创建虚拟环境并安装适配版依赖
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements-py313.txt
# 2) 生成业务表迁移(源码不含迁移文件,必须执行)
.\.venv\Scripts\python.exe manage.py makemigrations
# 3) 应用迁移建库
.\.venv\Scripts\python.exe manage.py migrate
# 4) 用 daphne(ASGI 服务器)启动
.\.venv\Scripts\python.exe -m daphne -b 0.0.0.0 -p 8008 greaterwms.asgi:application
有一个对老 Django 项目很关键的细节:源码仓库里不含 migrations 目录,作者预期你用他发布的某个数据库快照。这意味着如果你自己建库,必须先 makemigrations 生成全部 29 个 app 的 0001_initial,再 migrate。跳过第一步直接 migrate 会报"no migrations"。
跑通之后的验证结果:
| 检查项 | 结果 |
|---|---|
| `manage.py check` | ✅ 0 issues |
| makemigrations | ✅ 29 个 app 的 0001_initial |
| migrate | ✅ 26 个业务表 + Django 内置表全部建立 |
| daphne 启动 | ✅ 监听 8008 |
| `GET /api/` | ✅ HTTP 200,OpenAPI Schema 365KB |
| `GET /api/docs/`(ReDoc) | ✅ HTTP 200 |
| `POST /login/` | ✅ HTTP 200,错误场景返回 `{"code":"1011","msg":"User Name Or Password Error"}` |
POST /login/ 故意用了错误密码来验证——返回的是业务层的错误码 JSON 而不是异常堆栈,说明认证、序列化、异常处理三条管线都活着。这个"先破坏再确认"的冒烟习惯,比单纯看到一个 200 可信得多。
另外 drf-spectacular 自动生成的 OpenAPI 文档开箱可用:/api/debug/ 是 Swagger UI 交互调试,/api/docs/ 是 ReDoc 阅读视图,/api/ 直接吐 365KB 的 Schema JSON。158 个端点全部有文档,这对二开来说是巨大利好——你不用通读代码就能摸清 API 全貌。
四、开源代码的真实成色:优点与生产风险清单
跑起来只是及格线。真正决定"能不能改造成生产系统"的是代码成色。我把 32 个 app 通读了一遍,结论是:业务模型优秀,工程化欠账不少。
先说优点。asn(收货)和 dn(发货)两个核心 app 的状态机设计相当成熟:主单(List)+ 明细(Detail)结构,通过 *_status 字段驱动流转,配合独立的动作端点实现仓储作业流程。入库是 asnlist → asndetail → preload(预装载)→ presort(预分拣)→ sorted(分拣)→ movetobin(上架),出库是 dnlist → dndetail → neworder → orderrelease(波次下发)→ pickinglist(拣货单)→ picked → dispatch(车辆调度)→ pod(回单)。这条主链路完整覆盖了真实仓库的作业动作,是整个项目最值钱的部分。
库存设计也很见功力:stocklist 一张表维护 15 个维度的库存数量(在手、可订购、已订购、待检、冻结、破损、在途收、在途发、待装载、待分拣、已分拣、待拣、已拣、缺货……),所有出入库动作本质上是这些维度之间的转移。这套"库存账户"模型在第二章我会展开讲。
再说欠账。生产化体检之后,我列出了这张风险清单:
| 项 | 现状 | 风险 |
|---|---|---|
| SECRET_KEY | `get_random_secret_key()` 每次启动随机生成 | 重启后所有 JWT 失效,用户全被登出 |
| DEBUG | `True` | 生产环境泄露敏感信息 |
| ALLOWED_HOSTS | `['*']` | Host 头攻击风险 |
| CSRF 中间件 | 被注释 | 纯 API 场景可接受,但需评估 |
| CORS | `CORS_ORIGIN_ALLOW_ALL = True` | 生产应收紧为白名单 |
| JWT 有效期 | 20 年 | 会话过长,等同没有过期 |
| 数据一致性 | 库存增减依赖应用层多次 save | 并发下超卖、部分更新脏数据 |
| 金额字段 | `FloatField` | 二进制浮点误差,财务不可接受 |
| 限流实现 | 模块级全局变量存时间戳 | 多请求并发串扰 |
这份清单基本就是我的二开路线图。逐条应对的方案会在后面章节展开,这里先给结论:
- SECRET_KEY / DEBUG / ALLOWED_HOSTS / CORS / JWT 属于配置层加固,一次性解决(第十章部署复盘统一收口);
- 数据一致性是硬骨头:入库 9 处、出库 5 处写操作要事务化,再加 12 处
select_for_update()行锁(第三、四、五章); - 金额精度要做 Float → Decimal 的存量迁移,顺带修两个隐藏 bug(第六章);
- 业务增强按客户场景补批次效期(第七章)、外部系统对接(第八章)、IoT 设备(第九章)、报表看板(第十章)。
五、为什么这个改造过程值得写成系列
市面上讲 Django 的教程很多,讲 WMS 的文章也不少,但"把一个真实开源项目从 demo 改造成生产系统"的完整记录很少。这两者之间的差距恰恰是最有信息量的部分:
- 你会遇到别人的设计决策并被迫理解它——为什么库存要拆 15 个维度,为什么上架和分拣是两个动作;
- 你会遇到真实的技术债——全局变量存限流时间戳、异常处理器弄丢 HTTP 状态码、SQLite 下
select_for_update静默失效; - 你要做存量数据迁移——Float 转 Decimal 不只是改字段类型,要考虑已有数据怎么换算;
- 你要做边界设计——外部系统重复推送同一张入库单怎么办,设备事件乱序到达怎么办。
这些都不是教程会教你的,但每一件都会在生产环境咬你一口。
本系列的章节安排如下,全部基于改造中的真实代码:
| 章节 | 主题 | 核心内容 |
|---|---|---|
| 02 | 领域模型 | 34 张表、openid 多租户、15 维库存账户、ASN/DN 状态机 |
| 03 | 入库全链路 | preload → presort → sorted → movetobin,9 处事务化 |
| 04 | 出库全链路 | 波次下发、库位分配、拣货、回单、缺货挂单 |
| 05 | 并发治理 | `select_for_update` 行锁、数据库方言的坑 |
| 06 | 金额精度 | Float → Decimal 存量迁移,顺带修两个 bug |
| 07 | 批次效期 | 医药冷链场景的到期预警设计 |
| 08 | 集成层 | X-API-KEY 认证、幂等建单、ERP/OMS/TMS 对接 |
| 09 | IoT 设备 | 适配器模式接入 AGV / 称重 / 立体库 |
| 10 | 报表与部署 | 四类经营报表、ECharts 仪表盘、安全加固清单 |
如果你也面对"要不要用开源 WMS 做底座"的决策,或者正在维护一个 Django 老项目,这个系列应该能给你一张相对完整的地图。下一章,我们从整个系统最核心的资产开始:那 34 张表到底怎么读。
