
欢迎来到预见猿份,本站项目均为站长原创,学习中有问题可直接提交给站长老苗解决(微信:mrt_0607)。
苗润土老师,20余年一线项目经验,2014年加入黑马,星辰wms、云岚到家、学成在线项目作者,历任高级讲师、教学主管及课程研究员。 b站老苗
第一章 熟悉项目
学习目标
入职公司进入项目团队的第一件事就是熟悉项目,通过本章节的学习完成以下学习目标:
day01:
- 能够说出项目的业务模块
- 能够说出项目的业务流程
- 能够说出项目的技术架构
- 能够搭建项目的开发环境
- 能够开发货主管理模块
- 能够开发仓库管理子模块
day02:
- 能够开发储区管理子模块
- 能够开发储位管理子模块
- 能够说出sku与spu的概念
- 能够开发商品分类管理子模块
- 能够开发商品品牌管理子模块
- 能够开发商品信息管理子模块
1 行业为什么卷?
1)行业现状
- 技术迭代的自我加速
技术周期从过去的5-10年缩短到现在的1-2年
如:
- 前端框架从jQuery到React/Vue的演进
- Java语言(语言加速+框架战争+AI革命)
JDK更新从8年缩短到2年
Spring每年推出新模块(Boot→Cloud→Native→AI)
开发方式变革(云原生、低代码、AI)
对开发者的影响:
- 需要持续学习(每2年一次技术大换血)
- 不仅要懂Java,还要会云、懂AI
- 基础编码岗位可能被AI替代,必须往高阶发展
- 人才供给的结构性变化
- 全球计算机专业毕业生年超80万(中国占40%)
- 转行培训市场催生大量"速成开发者"(培训班年输出10万+学员)
- 工具革命带来的生产力悖论
- AI代码补全工具(如GitHub Copilot)提升30%效率的同时,变相提高了产出预期
- 低代码平台使简单开发门槛降低,倒逼开发者向复杂领域转移
2) 如何破局
(1)技术纵深发展
- 垂直领域专精
如:专注高并发/高性能领域,云原生与微服务领域,智能系统与AI工程领域、专注某垂直业务领域的研发与技术方案优化。
(2)开发模式升级
- AI协同开发:使用AI工具开发项目提高效率,培养Prompt工程能力
- 架构思维培养:系统设计能力在高级岗位面试中权重达60%
(3)职业路径创新
- 发展跨域能力:"技术+产品"跨域能力已成为开发者晋升的关键杠杆,如:张楠(字节跳动中国CEO)早期为PHP工程师,主导抖音火山版整合,提出"中视频"战略。

2 如何学项目?
首先要明确学习一个项目的目的是什么?
将你所学技术转化为生产力,解决具体的用户需求,这个过程就是项目实战,通过项目课程提高自己的项目实战能力,学会需求分析、设计、开发、优化的方法,最终根据用户的需求完成项目开发。
如何学项目?
- 学会需求分析
大到一个系统、模块,小到一个功能、接口,开发前一定搞清楚需求,会看需求文档、界面原型,根据需求文档梳理用户业务流程、数据模型、系统接口。
- 宏观层面:掌握系统/模块的业务流程
- 微观层面:深入理解功能点/接口需求
- 核心技能:
✓ 需求文档解读
✓ 原型界面分析
✓ 业务流程梳理
✓ 数据模型构建

- 系统设计
根据需求分析的结果,设计系统架构、数据库表,系统接口等。
首先的建表,数据库表刚开始不会建没关系,缺少字段再加上,多字段确定不要了就删除,最终要自己按需求建表。
系统架构设计,系统架构设计需结合项目规模、功能模块、非功能性需求(性能、可扩展性、维护性等)进行综合权衡。首先明确采用单体架构还是微服务架构,在此基础上,进一步明确模块间的接口交互方式,包括同步接口(如 RESTful API、gRPC)与异步通信(如消息队列)的设计,并定义接口协议、鉴权方式、版本管理及依赖关系,数据库选型方面,需根据业务特性确定使用关系型数据库或非关系型数据库,中间件部分需结合实际场景选取。
前后端项目更多的就是接口开发,通常是基于SpringBoot实现HTTP接口,注意接口参数、返回结果、接口地址的定义。
对于技术复杂的技术场景要善于借鉴其它的项目的方案去设计新项目的技术方案,比如:缓存方案、多线程任务处理方案、消息通信方案等。
基础设计:
• 数据库表结构设计- 系统架构设计
- 系统接口设计
高阶设计:
• 技术方案选型(缓存/多线程/消息队列等)
• 技术难点解决方案
- 系统开发
前期照老师的代码写没关系,最终要按自己的思路去写代码,基于三层架构进行开发,用低代码和AI生成一部分代码,动手修改一部分代码。
写完代码一定要测试,修改Bug,修改完成回归测试,最终测试通过整个业务流程。
- 数据库开发:
迭代式建表 → 字段动态调整 → 最终符合需求 - 接口开发:
✓ SpringBoot框架应用
✓ 三层架构实现
✓ 参数/返回值规范
✓ 智能编码(AI/低代码+手动优化) - 测试:
写完代码一定测试一定测试一定测试!!!
一些重要的方法用单元测试,有些公司要求单元测试覆盖率达到一定要求(70-80%是基本要求,对于稳定性极高要求的项目要求90%以上)。
流程测试,测试整个业务流程,一般是黑盒测试,根据用户需求模拟用户进行操作。
Bug修复后一定要回归测试。
- 系统优化
所有的代码只要想优化一定有优化的空间,比如:一个任务处理功能,原来是单线程处理,当任务量上来后任务处理的速度就会很慢,改用多线程解决问题,如果不优化也能满足用户需求但是用户体验差,优化后提高了用户满意度。系统优化工作是提高工程师水平的有效途径,通过系统优化不仅可以提高系统的健壮性,也可以提高自己思考问题解决问题的能力。
✓ SQL优化(索引、慢查询分析)
✓ 代码性能优化(算法、缓存、异步处理)
✓ JVM调优(内存管理、GC优化)
✓ 并发优化(线程池、锁策略)
✓ 架构优化(微服务拆分、缓存策略)
- 培养模块和系统的独立开发能力
面对当前的行业现状,一定要使用自己独挡一面,而不仅仅是代码编写人员,给你一个模块和系统要有能力去分析、设计、开发。
- 目标:成为全流程负责人
- 要求:
✓ 模块级需求分析
✓ 系统级方案设计
✓ 完整开发能力
✓ 问题解决能力
- 培养自己的沟通表达能力
不论你是在面试过程,还是到企业与上级和同事的沟通,自己都需要有一定的表达能力,能够将需求说清楚,能够说清楚你的技术方案,遇到问题能够将问题说清楚。
在学习过程中问老师问题、和同学讨论的时候都是锻炼的好机会。
- 应用场景:
• 技术方案宣讲
• 问题精准描述
• 团队协作沟通
• 面试展示 - 提升方法:
多参与技术讨论
主动请教交流
定期总结复盘
3 课程介绍
1)整体学习目标
- 掌握系统需求分析的流程与方法
- 掌握系统设计的流程与方法
- 掌握使用低代码和AI进行单体应用的开发方法
- 掌握储位管理模块的设计与开发方法
- 掌握商品管理模块的设计与开发方法
- 掌握入库管理模块的设计与开发方法
- 掌握出库管理模块的设计与开发方法
- 掌握Spring AI 开发智能体应用的方法
- 掌握AI 在WMS系统中的应用方法
- 掌握用户认证授权模块的设计与开发方法
- 掌握SaaS多租户模块的设计与开发方法
- 掌握IOT 系统设备管理模块的设计与开发方法
- 掌握IOT 系统性能监视模块的设计与开发方法
- 掌握IOT 系统报警处理模块的设计与开发方法
- 掌握系统部署及线上故障处理的流程与常用方法
2)技术解决方案
- 缓存一致性解决方案
- 文件存储解决方案
- 低代码技术解决方案
- Web打印技术解决方案
- 条形码技术解决方案
- Excel导入导出解决方案
- 唯一序号生成解决方案
- 异构系统远程调用解决方案
- Spring AI开发智能体解决方案
- 最短路径规划解决方案
- 用户认证授权解决方案
- 多租户数据隔离解决方案
- 多线程任务处理解决方案
- 任务调度解决方案
- 分布式锁解决方案
- 大屏监控解决方案
- 基于MQTT物联网监控解决方案
- InfluxDB时序数据库解决方案
- MQ消息处理解决方案
4 创新WMS项目介绍
4.1 项目介绍
1) 项目介绍
创新WMS( Warehouse Management System)即仓库管理系统,它是一套专门用于管理仓库操作的软件系统,它通过自动化和系统化的方式,帮助企业优化库存管理、订单处理、货物接收与发货、仓库布局优化等关键环节,提高仓库运营效率和准确性。
老苗项目实战课堂推出本项目的目的是让大家学习完Java基础知识后能够快速并且完整的开发一个真正的企业级项目,项目由浅入深设计,门槛低,含金量高,非常适合初学者入行。
2)项目业务流程
入库流程
入库指货物从外部进入仓库,并完成登记、存储的全过程。主要包含以下业务场景和步骤:
业务场景
- 采购入库:供应商送货至仓库(原材料或成品)。
- 生产退料:生产车间将剩余原材料退回仓库。
- 调拨入库:从其他仓库调货至本仓库。
- 退货入库:客户退回商品(售后或质检不合格品)。
关键流程

- 创建入库单:ERP产生采购单(Purchase Order PO单)自动同步到WMS并创建入库单。
- 预约送货:供应商预约到货时间,供应商送货到仓库。
- 收货验收:核对PO单、检查货物数量、质量进行收货验收。
- 上架:工具使用PDA或PC完成货物上架,将货物存放到指定位置。
- 库存更新:货物信息(数量、批次、库位)录入系统,库存实时可查。
出库
出库指货物从仓库发出,交付给客户或下一环节的过程。主要分为以下类型:
业务场景
- 销售出库:根据客户订单发货(B2B/B2C)。
- 生产领料:车间从仓库领取原材料。
- 调拨出库:将货物调往其他仓库。
- 报废出库:损坏或过期货物移出仓库。
关键流程

- 创建出库单:WMS接收订单(如ERP同步的销售订单),创建出库单。
- 系统分配库存:系统分配库存(先进先出策略等)。
- 波次管理: 将订单分组划分波次,为批量拣货准备。
- 拣货:按波次从库位拣取货物。
- 打包:对拣货商品进行打包,核对订单与实物,打包并贴物流标签。
- 发货:交接给承运商,更新物流信息,库存扣减。
3) 功能模块

4) 技术架构
技术架构:
主流技术栈全覆盖,涵盖AI、自动化、无代码等前沿技术,无缝对接企业需求,采用Spring Boot、MySQL、Redis、RabbitMQ、MQTT等主流技术,讲解企业级解决方案,多租户SaaS架构设计 方案等。

前端技术栈
- Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
- 依赖管理:node、npm、pnpm
- 基石: Vue 3 + TypeScript + ES6+ 构成了应用的开发范式和质量基础。
- 构建工具: Vite 为开发和构建提供了极速体验。
- UI 与交互: Ant Design Vue 提供基础UI组件,ECharts 负责图表,vxe-table 处理复杂表格,共同构建用户界面。
- 样式方案: UnoCSS 用于快速编写和生成样式。
- 状态管理: Pinia 集中管理跨组件的复杂状态。
- 架构模式: qiankun 将整个应用或部分模块扩展为微前端架构,实现更大规模的工程解耦。
后端技术栈
Spring Boot 3、Spring AI、Spring Cache、MyBatis-Plus、MySQL8、Redis、RabbitMQ、InfluxDB、MQTT等。
后端基于JeecgBoot框架,使用它的低代码组件快速生成代码,还使用Spring AI 集成 AI大模型。
5) Q&A
Q:WMS 系统主要应用于哪些企业?
并不是只有“大企业”才配用WMS,其实只要你的仓库超过一定规模,有一定货品周转率,那就该上WMS了,以下几个行业特别适合:
- 电商行业:订单量大、SKU多、发货频繁,WMS绝对是救命稻草
- 制造业:原材料、半成品、成品分仓管理,需要精准控制
- 医药行业:对批次、有效期要求高,WMS能严控流程
- 食品冷链:需要温区管理、先进先出、保质期管理
- 第三方物流(3PL):多客户、多仓库,WMS能帮你做出专业服务
Q:WMS 系统主要由哪些模块构成?
A:WMS 系统一般包含入库管理模块,负责处理货物接收、质检、上架等流程;出库管理模块,用于安排订单拣选、包装、发货等操作;库存管理模块,实现库存数量监控、库存预警、库存调整等功能;此外还有盘点管理模块、报表管理模块等,各模块协同运作,保障仓库管理的流畅性。
Q:WMS 系统适用于哪些行业场景?
A:WMS 系统广泛应用于电商、物流、制造业、零售等多个行业。电商行业中,面对海量订单,WMS 能快速处理货物出入库;物流企业利用 WMS 优化仓储资源配置,提升转运效率;制造业可借助 WMS 管理原材料及成品库存,保障生产连续性;零售行业通过 WMS 实时掌握商品库存,优化补货策略。
Q:使用 WMS 系统能为企业带来哪些显著优势?
A:企业使用 WMS 系统可提高仓库作业效率,减少人工操作失误,降低人力成本;实现库存的精准管理,避免库存积压或缺货情况,提升库存周转率;同时,系统生成的各类数据报表能为企业决策提供有力支持,助力企业优化业务流程,增强市场竞争力。
Q:WMS 系统与其他企业管理系统(如 ERP)如何集成?
A:WMS 系统可与 ERP 系统通过接口实现数据交互。例如,ERP 系统将采购订单、销售订单等信息传递给 WMS 系统,WMS 系统则把库存状态、出入库结果反馈给 ERP 系统。两者集成后,能打破信息孤岛,实现企业整体业务流程的无缝衔接,提升企业运营管理的一体化水平。
Q: WMS(仓库管理系统)主要有以下几种部署模式:
- 本地部署(On-Premises)
- 系统安装在企业自有的服务器上
- 数据存储在本地
- 需要企业自行维护硬件和软件
- 前期投入成本较高
- 适合对数据安全性要求高的大型企业
- 云端部署(Cloud-Based)
- SaaS(软件即服务)模式
- 通过互联网访问
- 供应商负责维护和升级
- 按需付费,前期投入低
- 适合中小型企业或需要快速上线的场景
- 混合部署(Hybrid)
- 部分功能在云端,部分在本地
- 结合了本地部署的安全性和云部署的灵活性
- 适合有特殊合规要求或部分业务需要本地化的企业
Q: 用了WMS系统能给企业带来什么好处
1.效率大幅提升
以前3个人一天拣货100单,现在1个人能搞定。
2.出错率直线下降
错发漏发的几率几乎为0,提升客户满意度。
3.成本控制明显
人力节省、库位优化、库存预警都能帮你省钱。
4.管理更透明
库存、订单、效率数据一目了然,随时看。
5.业务抗压能力强
双11、年终促销,不怕爆单,系统调度应对得来。
6)小结
能够说出项目的业务模块
能够说出项目的业务流程
能够说出项目的技术架构
5 搭建环境
参考“系统安装与配置”搭建项目开发环境。
6 JeecgBoot入门
6.1 低代码技术方案
1) 什么是低代码
传统意义上的低代码平台通过可视化拖拽方式构建应用,以 减少手写代码 为核心,通常分为两类:
- 零代码(No-Code):完全不需要编码,业务人员可直接使用,如 阿里云宜搭、明道云。
- 低代码(Low-Code):可视化为主,必要时通过少量代码扩展复杂逻辑,如 用友 YonBuilder、金蝶云·苍穹(即你提到的“金蝶AI苍穹”)。
若希望在 完全控制代码 的前提下提高开发效率、降低成本,可选择 成熟的快速开发框架(常称为“代码生成器型低代码”)。这类框架提供权限、代码生成、通用模块等基础能力,开发者仍掌握全部源码,典型代表有:
- 若依(RuoYi):国内流行的 Java 快速开发框架
- JEECG Boot:基于代码生成器的低代码开发平台
两者核心区别:
- 低代码平台 减少编码,提升交付速度,但受平台约束
- 快速开发框架 保留完整代码控制权,兼顾效率与灵活性
本项目使用成熟的快速开发框架进行开发,对于一些通用的功能模块比如:用户、权限、菜单、数据字典等都已经具备。
下边介绍国内两款流行的快速开发框架。
2) 若依(RuoYi)
- 若依官网:http://ruoyi.vip
核心特点
定位:基于Spring Boot的权限管理系统脚手架,提供代码生成器。
技术栈:Spring Boot + MyBatis + Thymeleaf/Vue3(前后端分离版本)。
低代码能力:
- 通过代码生成器自动生成CRUD代码(Controller/Service/Entity等)。
- 提供基础模块(用户管理、角色权限、日志监控等),开发者需手动扩展业务逻辑。
适用场景:需要快速搭建后台管理系统,且希望完全控制代码的中小型项目。
与传统低代码的区别
- 无可视化开发:需直接修改生成的Java代码,无法通过拖拽配置业务逻辑。
- 代码所有权:生成的是标准Spring Boot代码,无平台锁定风险。
3) JEECG Boot
- JEECG官网:http://jeecg.com
核心特点
定位:基于代码生成器的企业级快速开发平台,强调“低代码”概念。
技术栈:Spring Boot + Ant Design Vue + MyBatis-Plus。
低代码能力:
- 在线表单设计器(通过配置生成前端页面和后台代码)。
- 支持流程审批(集成Activiti)、报表工具、大屏设计等。
- 提供“Online Coding”模式,可在线编辑业务逻辑代码。
适用场景:需要快速开发ERP、OA、CRM等企业应用,尤其适合国内政务/国企项目。
与传统低代码的区别
- 混合模式:既支持配置化生成代码,也允许开发者直接修改Java代码。
- 国产化适配:支持达梦、人大金仓等国产数据库,符合信创要求。
4) 技术选型
若依的可视化开发相比JeecgBoot较弱,JeecgBoot有在线表单设计器、在线报表设计器等,JeecgBoot采用混合模式,既可以能生成代码,也有可视化开发的功能,并且对于ERP、WMS、CRM等企业应用开发方面专门设计了Online表单设计器等产品,大大提高开发效率。
本项目选用JeecgBoot作为项目基础框架,并且还会使用Online表单设计器、在线报表、积木大屏等产品。
6.2 快速入门
6.2.1 Online表单
1) Online表单介绍
Online表单是JeecgBoot开发的代码生成器,通过在线建表,基于表生成前后端代码。Online表单支持对单表、一对多多表生成代码。
下边演示针对单表的代码生成,一对多表在入库模块学习。
你也可以通过官方文档和视频学习:https://www.jeecg.com/doc/quickstart
2) 生成代码
首先配置生成代码的包路径,找到配置文件:

打开jeecg_config.properties设置包路径,如下图:

根据货主表生成代码,在WMS系统中需要管理多个货主的商品,所以要管理商品信息需要先管理在系统中添加货主,货主表存储了货主信息。
注意:更改完上边的配置重启后端工程。
下边通过Online表单基于货主表生成前后端代码。
进入Online菜单界面

Online表单支持在线建表,点击新增即可在线建表,这里货主表已经建好,找到货主表:

选中货主表,点击“代码生成”

代码生成目录:选择一个单独的地方生成代码,注意不要选择项目代码目录,以免覆盖原文件。
点击“开始生成”

生成完成,在桌面自动生成一个src目录

基于三层架构的代码:controller、service、mapper
Entity: 实体类
vue3: PC端前端代码
uniapp: 移动端前端代码
3) 创建分支
在拷贝代码前先创建dev_01分支,此分支作为第一阶段的开发任务。

输入分支名称:dev_01,创建新分支并切换到新分支

创建完成如下图:

4) 拷贝代码
拷贝后端代码到后端工程中,如下图:

5) 修改包路径的配置
由于wms模块的包路径为与原包路径org.jeecg不一致,而启动类JeecgSystemApplication.java在org.jeecg下,
启动工程将扫描org.jeecg下的bean,无法扫描到com.miaoruntu下的bean。
下边需要添加扫描com.miaoruntu下的配置。
在启动类上添加包路径:

添加mapper文件路径:

在mybatis Plus配置文件中添加包路径配置:

修改切面中包路径配置


swagger文档包路径:

6) 运行后端工程
运行jeecg-system-start/src/main/java/org/jeecg/JeecgSystemApplication.java

如何验证wms模块已经运行起来呢?
通过查看swagger文档,是否存在货主管理相关的接口
访问:http://127.0.0.1:8080/jeecg-boot/doc.html
下图显示了货主管理相关的接口:

6.2.2 测试接口
1) 使用swagger
下边测试“分页列表查询”接口,找到“货主表-分页列表查询”接口,进入调试界面

上图需要输入token,如何获取用户身份的token呢?
使用swagger前需要通过登录接口获取token。
1.启动system项目,访问路径:http://localhost:8080/jeecg-boot
2.获取验证码,按图操作,输入参数key的值,此处输入123,点击发送,获取验证码图片


如果上图中无法正确显示验证码,可以复制验证码图片的base64编码,在浏览器中查看


3.登录,获取token
登录参数说明

使用如下账号和密码参数:
"username": "admin",
"password": "123456"
4.拿到token,按图操作,保存token信息到请求头
设备全局参数

设备完成刷新网页,再去进入测试货主查询页面。
测试分页查询货主接口正常,如下:

2) 使用apifox
参考 “使用Apifox”测试接口。
6.2.3 前端代码
1)创建分支
参考后端创建分支的方法,在前端工程创建dev_01分支并切换到dev_01分支。
2) 拷贝前端代码
jeecgBoot也帮我们生成了前端代码,查看vue3目录:

.sql:用于生成菜单
其它:前端代码
首先进入前端工程,在src/view下创建goods目录,然后将vue3上的全部内容(.sql除外)拷贝到goods下。

3) 创建菜单的方法
下边是创建菜单示例,由于我们导入的数据库已创建了菜单所以不用执行此sql,否则需要在mysql执行此sql。

上图内容是向sys_permission表插入菜单数据,我们只要执行此sql即可创建货主管理的菜单,我们也可以手工在菜单管理界面创建菜单。
注意:由于我们导入的课前提供的完整数据库,货主管理菜单已经存在不用重复导入。
我们进入“菜单管理”查看,在下图中找到“主档管理”

找到“货主”,如下图:

点击“货主”子菜单的编辑,如下图:

上图中前端组件:定位到vue3文件位置(无需输入.vue扩展名)

4) 运行前端工程
下边我们启动前端工程,访问货主管理菜单

6.2.4 货主模块完善
使用Online表单生成的代码是完全可控的,我们可以根据需求去修改完善生成的代码,所以我们需要去阅读生成的代码:

刚开始使用jeecgBoot或者刚接触一个新项目时,我们肯定是陌生的,不要试图全部搞清代码后再动手,一定要学会比葫芦画瓢,先会用再慢慢阅读代码。
除了参考一些现有代码外需要多阅读文档。
JeecgBoot官方提供的前端文档:http://help.jeecg.com/component/basicForm
因为JeecgBoot是基于Ant-Design-Vue ,对于Input组件的属性可以参考 Ant-Design-Vue (文档:https://2x.antdv.com/components/input-cn)
1)后端完善
自动生成的代码中货主编码需要人工输入,并且保证编码唯一,在使用时非常不方便。我们现在要实现货主编码自动生成并且保证唯一性。
生成方案:C+5位序号,序号使用redis自增序号实现
找到新增货主接口的代码位置:

定义service方法如下:
/**
* @Description: 货主表
* @Author: jeecg-boot
* @Date: 2025-04-13
* @Version: V1.0
*/
public interface IWmsCargoOwnersService extends IService<WmsCargoOwners> {
/**
* 货主添加
*/
public void add(WmsCargoOwners wmsCargoOwners);
...
}
@Service
public class WmsCargoOwnersServiceImpl extends ServiceImpl<WmsCargoOwnersMapper, WmsCargoOwners> implements IWmsCargoOwnersService {
@Autowired
private RedisUtil redisUtil;
@Transactional(rollbackFor = Exception.class)
@Override
public void add(WmsCargoOwners wmsCargoOwners) {
//生成货主编码
wmsCargoOwners.setOwnerCode(generateOwnerCode());
save(wmsCargoOwners);
}
/**
* 生成货主编码
*/
public String generateOwnerCode() {
//编码规则:C+5位序号,序号使用redis自增序号实现
//调用redis的incr函数
long incr = 0;
try {
incr = redisUtil.incr("wms:cargo-owner", 1);
} catch (Exception e) {
throw new JeecgBootException("生成货主编码出错");
}
String code = "C"+String.format("%05d", incr);
return code;
}
...
}修改controller方法:
@AutoLog(value = "货主表-添加")
@Operation(summary="货主表-添加")
@RequiresPermissions("goods:wms_cargo_owners:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody WmsCargoOwners wmsCargoOwners) {
wmsCargoOwnersService.add(wmsCargoOwners);
return Result.OK("添加成功!");
}2) 前端完善
既然货主编码自动生成,在添加货主时将货主编码输入框置灰。
找到表单定义处:

在货主编码字段添加:
//不允许输入
componentProps: {
disabled: true,
},3)测试生成编码
重启后端工程进行测试。
4)思考
如果生成的货主编码在数据库已存在怎么办?
这种情况可能是由于redis 自增序列起始数值小于数据库已有记录的编码,比如:数据库已存在一个C0005,而Redis自增序列的数值是4,当你新增时就会和C0005重复了。
6.2.5 数据字典
1) 什么是数据字典
数据字典表是一个核心的基础数据管理工具,它用于统一定义和管理系统中的各种配置项、类型和状态。
它主要包含以下内容:
- 仓库类型(如:普通仓、保税仓、冷链仓、跨境仓等)
- 仓库状态(如:启用、停用、盘点中等)
- 货架类型
- 库存状态(如:正常、锁定、残次品等)
- 作业类型(如:入库、出库、移库等)
- 单据状态
- 计量单位
- 包装类型
进入“系统管理-->数据字典”

找到“dict_item_status”字典,它用于货主状态

点击“字典配置”查看字典定义

注意:为了保证可读性,数据字典如果没有特殊要求不建议定义为0、1、2..数字编码。
2) 表单使用数据字典
数据字典的使用:
在添加/编辑页面 通过下拉框供用户选择,例如:

表单中使用下拉框显示数据字典需要使用JDictSelectTag组件,"JDictSelectTag"是jeecgBoot的前端组件,查看文档https://help.jeecg.com/ui/component/JDictSelectTag
功能:下拉显示数据字典选项
例子:下拉选择数据字典dict_item_status的选项
dictCode:指定数据字典code
表单下拉框中显示数据字典:
{
label: '状态',
field: 'status',
component: 'JDictSelectTag',
componentProps:{
dictCode:"dict_item_status"
},
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请输入状态'},
];
},
},component: 'JDictSelectTag': 指定使用JDictSelectTag组件
dict_item_status:数据字典的名称
注意:状态字段在数据库使用java.lang.String类型
找到货主添加/修改界面的表单配置

配置完成刷新页面查看效果。
3) 列表使用数据字典
在查询列表显示数据字典名称时需要前后端结合。
首先在后端实体类中使用@Dict(dicCode = "dict_item_status") 指定数据字典名称
/**状态*/
@Dict(dicCode = "dict_item_status")
private java.lang.String status;在查询货主表时jeecgBoot会关联查询数据字典表得到字典名称,字段名称为“status_dictText”,即“原字段名称_dictText”,在前端就可以指定显示此字段名称的值即可。
{
title: '状态',
align:"center",
dataIndex: 'status_dictText',
},
{
title: '结算币种',
align:"center",
dataIndex: 'settlementCurrency_dictText'
},6.2.6 探索
@Dict注解实现查询数据字典的底层是通过DictAspect切面类实现,在DictAspect中首先解析出模型类中@Dict中的配置的数据字典编码,然后根据数据字典编码查询数据字典,最终将数据字典名称存放在“原属性名_dictText”的属性中
有兴趣的同学有空了研究底层代码。

7 储位管理
7.1 如何进行需求分析?
1)讨论产品原型
在系统设计之前需要进行需求分析,需求分析就是根据用户所提的需求进行理解,它是软件开发周期中的关键环节,它直接影响到后续的设计、开发和测试阶段。需求分析阶段做好了能够减少后期修改的成本,提高系统的质量和用户的满意度。因此,在需求分析阶段投入足够的时间和资源是非常必要的。
产品经理通过产品原型进行需求分析,与客户沟通最好的方式就是通过界面原型进行沟通,因为界面原型客户看的懂,它是需求阶段最接近软件成品产物。
什么是产品原型:
将抽象的文字需求转化为可视化的界面和流程,让产品、设计、开发、运营及客户在同一页面上达成共识
可通过产品原型网(https://www.axureshop.com/)参考一些项目的原型。
在需求分析阶段产品经理会召集项目团队成员一起开会讨论确定产品原型,根据产品原型去讨论每个功能。
- 用户业务流程:对业务流程有清晰的理解,业务流程的每个步骤的产出物。
下图(泳道图)是一个业务流程的例子:

- 功能列表及优先级:根据功能列表划分模块,根据优先级制定工作计划。

- 对象属性分析:根据原型分析涉及到的对象有哪些及对象属性,为后续设计表作准备。


- 还有一些非功能需求:比如性能要求、安全性、可维护性等,非功能需求更多的为后期性能优化提供依据。
举例:
用户登录功能,为了保证安全性对密码加密传输、加密保存。
app首页为了提高动态数据的加载速度,保证首页3秒内加载完毕。
2)需求规格说明书
对于一些涉及多子系统交互或长周期开发时(如ERP、物联网平台),为了减少歧义产品经理还需要编写需求规格说明书。
比如:
- 高合规性行业(医疗、金融、航空等)需要严格的文档追溯,SRS是合规审计的必备材料。
- 大型外包项目(如政府或企业招标)通常要求交付详细的SRS作为合同附件。
需求规格说明书一般由产品经理编写,与开发人员共同讨论修改。
本项目需求文档:附件/创新WMS需求规格说明书
其它项目:课程资料/参考文档/XXX需求规则说明书

7.2 如何进行设计?
1)概要设计
需求分析阶段完成是系统设计阶段,系统设计分为概要设计和详细设计。
概要设计是对系统模块、架构、系统间的交互接口、外部接口等宏观层面进行设计。通常由架构师或高级开发工程师完成。




2) 详细设计
软件详细设计是在概要设计基础上,对每个模块内部的实现逻辑、数据结构、接口细节、异常处理等进行精确描述,目标是让开发人员可以直接依据文档编码。
重点包括:
数据库设计:
- 根据需求设计数据库表、及表结构。
- 考虑到数据的一致性和完整性,设计适当的约束条件。
- 性能优化:考虑索引、分区等技术以提高查询效率。
接口设计:
分为外部接口和内部接口
- 外部接口:是本项目与第三方系统之间的接口。
举例:在上传服务项图片时涉及外部接口,下图上传图片功能如果将图片保存到云平台则需要调用云平台的上传图片接口。
- 内部接口:是本项目的功能接口,以及项目内部模块之间的接口。
功能接口:根据需求用例分析功能接口,比如储位管理模块,后端为前端提供哪些接口。通常是设计RESTful 接口,定义接口的URL、HTTP方法、请求参数和响应格式。
项目内部模块之间的接口:模块之间的通信接口,比如:文件服务模块为其它模块提供文件上传、下载的接口。







2)接口设计
Java开发人员平时最多的是进行接口设计,有些公司要求编写接口设计文档(word),有些公司则使用swagger等工具。
本项目的接口文档地址:http://127.0.0.1:8080/jeecg-boot/doc.html
接口设计需要关注以下内容:
HTTP请求方法
根据RESTful规范:
查询方法一般用GET,请求参数比较多可用POST
新增方法用POST
修改方法用PUT
删除方法用DELETE
接口路径
定义为RESTful风格的路径。
请求参数类型
常用的有:
json格式:
application/json,传递json格式字符串,当传递的参数是属于一个对象的属性时可用此格式,比如:新增、修改时通常传递的数据是某个对象的信息就可以使用此格式。
表单格式:
application/x-www-form-urlencoded,传递key/value串,就是在url后通过?和&进行拼接的参数,一般在查询时使用。
请求参数内容
请求参数的内容根据需求文档和界面原型去识别。
对于查询类的接口常用的参数有:排序方式、排序字段、当前页码和每页显示记录数。
响应结果类型
常见的类型有:text/html、text/plain、application/json等。
本项目统一使用application/json
响应结果内容
分页查询通用的响应内容有:数据列表、总页数、总记录数
7.3 需求分析
1)认识仓库布局
首先认识仓库内部的布局,理解仓库、储位、储区的含义。
仓库中划分多个储区,储区内划分多个储位。
储区: 在仓库内部划分几个区域,通常包括:暂存区、存储区、发货区等。
WMS系统就是对储位上的商品进行管理,储位上放了什么商品,商品数量是多少。

储位通常在货架上:
重型货架

轻型货架

每个储位都会统一编码,如下图:

2)需求分析
阅读“创新WMS需求规格说明书”分析如下内容:
- 仓库、储区,储位信息包括哪些属性
- 仓库、储区,储位三者之间是什么关系
- 操作约束有哪些
7.4 系统设计
1) 设计数据库表
根据需求分析设计表:仓库表、储区表、储位表。

仓库表 职责:代表物理上独立的一个仓库或物流中心。
储区表 职责:根据功能、存储性质或作业流程对仓库进行的大区域划分。
储位表 职责:最底层的执行单元,代表货架上的某个具体位置(如第A排、第3列、第2层)
关系:
- 一个仓库 包含 多个储区
- 一个储区 包含 多个储位
- 一个储位 是仓库内最具体的存储单元
示例:

2) 生成代码
下边我们使用Online表单生成仓库表(wms_warehouses)、储区表(wms_storage_zones)、储位表(wms_storage_locations)的前后端代码。
包路径指定为:warehouse

生成完成将代码拷贝到后端工程:

重启项目,访问接口文档:http://127.0.0.1:8080/jeecg-boot/doc.html#/home,发现仓库、储位、储区相关接口都已生成,见下图:

7.5 仓库管理子模块
7.5.1 完善代码
1) 数据字典
后端
仓库状态使用数据字典“wms_status”,仓库属性使用数据字典“warehouse_attr”
修改实体类

前端
修改前端页面,指定表单、列表中使用该数据字典。
在前端工程找到文件 src\views\warehouse\WmsWarehouses.data.ts,代码如下:
{
label: '仓库属性',
field: 'warehouseAttr',
component: 'JDictSelectTag',
componentProps:{
dictCode:"warehouse_attr"
},
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请输入仓库属性!'},
];
},
},
{
label: '状态',
field: 'status',
component: 'JDictSelectTag',
componentProps:{
dictCode:"wms_status",
disabled: true,
},
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请输入状态!'},
];
},
},修改列表配置:
{
title: '仓库属性',
align:"center",
dataIndex: 'warehouseAttr_dictText'
},
{
title: '状态',
align:"center",
dataIndex: 'status_dictText'
},2)校验仓库编码的唯一性
定义添加仓库和修改仓库的service方法:
package org.jeecg.modules.wms.warehouse.service;
import org.jeecg.modules.wms.warehouse.entity.WmsWarehouses;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* @Description: 仓库表
* @Author: jeecg-boot
* @Date: 2025-04-10
* @Version: V1.0
*/
public interface IWmsWarehousesService extends IService<WmsWarehouses> {
/**
* 新增仓库
* @param wmsWarehouses
*/
void add(WmsWarehouses wmsWarehouses);
/**
* 修改仓库
* @param wmsWarehouses
*/
void edit(WmsWarehouses wmsWarehouses);
...
}方法实现如下:
@Override
public void add(WmsWarehouses wmsWarehouses) {
//添加时校验仓库代码是否已存在 sql select count(*) from wms_warehouses where warehouse_code = ?
LambdaQueryWrapper<WmsWarehouses> ne = new LambdaQueryWrapper<WmsWarehouses>()
.eq(WmsWarehouses::getWarehouseCode, wmsWarehouses.getWarehouseCode());
if (this.count(ne) > 0) {
throw new RuntimeException("仓库代码已存在");
}
//初始状态为“创建”
wmsWarehouses.setStatus(WarehouseDictEnum.STATUS_CREATED.getCode());
save(wmsWarehouses);
}
@Override
public void edit(WmsWarehouses wmsWarehouses) {
//修改时校验仓库代码是否已存在 sql select count(*) from wms_warehouses where warehouse_code = ? and id != ?
LambdaQueryWrapper<WmsWarehouses> ne = new LambdaQueryWrapper<WmsWarehouses>()
.eq(WmsWarehouses::getWarehouseCode, wmsWarehouses.getWarehouseCode())
.ne(WmsWarehouses::getId, wmsWarehouses.getId());
if (this.count(ne) > 0) {
throw new RuntimeException("仓库代码已存在");
}
updateById(wmsWarehouses);
}修改controller方法:
/**
* 添加
*
* @param wmsWarehouses
* @return
*/
@AutoLog(value = "仓库表-添加")
@Operation(summary="仓库表-添加")
@RequiresPermissions("warehouse:wms_warehouses:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody WmsWarehouses wmsWarehouses) {
wmsWarehousesService.add(wmsWarehouses);
return Result.OK("添加成功!");
}
/**
* 编辑
*
* @param wmsWarehouses
* @return
*/
@AutoLog(value = "仓库表-编辑")
@Operation(summary="仓库表-编辑")
@RequiresPermissions("warehouse:wms_warehouses:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<String> edit(@RequestBody WmsWarehouses wmsWarehouses) {
wmsWarehousesService.edit(wmsWarehouses);
return Result.OK("编辑成功!");
}测试
测试预期结果:
添加时输入已存在的仓库代码提示错误,输入未存在的仓库代码成功添加。
修改时输入已存在的仓库代码提示错误。
7.5.2 启用/禁用功能(后端)
1)查看需求
首先阅读需求文档,根据需求文档进行设计、开发。
2) 接口定义
新手可以参考类似的代码去编写新接口。
/**
* @Description: 仓库表
* @Author: jeecg-boot
* @Date: 2025-04-10
* @Version: V1.0
*/
@Tag(name="仓库表")
@RestController
@RequestMapping("/warehouse/wmsWarehouses")
@Slf4j
public class WmsWarehousesController extends JeecgController<WmsWarehouses, IWmsWarehousesService> {
/**
* 启用
*
* @param id
* @return
*/
@AutoLog(value = "仓库表-启用")
@Operation(summary="仓库表-启用")
@RequestMapping(value = "/enable",method = {RequestMethod.PUT,RequestMethod.POST})
public Result<String> enable(@RequestParam(name="id",required=true) String id) {
wmsWarehousesService.enable( id);
return Result.OK("启用成功!");
}
/**
* 禁用
*
* @param id
* @return
*/
@AutoLog(value = "仓库表-禁用")
@Operation(summary="仓库表-禁用")
@RequestMapping(value = "/disable",method = {RequestMethod.PUT,RequestMethod.POST})
public Result<String> disable(@RequestParam(name="id",required=true) String id) {
wmsWarehousesService.disable( id);
return Result.OK("禁用成功!");
}
...3)service定义
接口定义:
/**
* @Description: 仓库表
* @Author: jeecg-boot
* @Date: 2025-04-10
* @Version: V1.0
*/
public interface IWmsWarehousesService extends IService<WmsWarehouses> {
/**
* 启用仓库
* @param id
*/
void enable(String id);
/**
* 禁用仓库
* @param id
*/
void disable(String id);
....实现类:
/**
* @Description: 仓库表
* @Author: jeecg-boot
* @Date: 2025-04-10
* @Version: V1.0
*/
@Service
public class WmsWarehousesServiceImpl extends ServiceImpl<WmsWarehousesMapper, WmsWarehouses> implements IWmsWarehousesService {
/**
* 启用仓库
* @param id
*/
@Override
public void enable(String id) {
//校验参数是否为空
if (StringUtils.isEmpty( id)) {
throw new JeecgBootException("参数为空");
}
//校验仓库是否存在
WmsWarehouses wmsWarehouses = getById(id);
if (wmsWarehouses == null) {
throw new JeecgBootException("仓库不存在");
}
//仓库状态为“创建”或禁用时方可启用
if (!WarehouseDictEnum.STATUS_CREATED.getCode().equals(wmsWarehouses.getStatus()) && !WarehouseDictEnum.STATUS_INACTIVE.getCode().equals(wmsWarehouses.getStatus())) {
throw new JeecgBootException("仓库状态为“创建”或禁用时方可启用");
}
wmsWarehouses.setStatus(WarehouseDictEnum.STATUS_ACTIVE.getCode());
updateById(wmsWarehouses);
}
@Override
public void disable(String id) {
//校验参数是否为空
if (StringUtils.isEmpty( id)) {
throw new JeecgBootException("参数为空");
}
//校验仓库是否存在
WmsWarehouses wmsWarehouses = getById(id);
if (wmsWarehouses == null) {
throw new JeecgBootException("仓库不存在");
}
//仓库状态为“启用”时方可禁用
if (!WarehouseDictEnum.STATUS_ACTIVE.getCode().equals(wmsWarehouses.getStatus())) {
throw new JeecgBootException("仓库状态为“启用”时方可禁用");
}
wmsWarehouses.setStatus(WarehouseDictEnum.STATUS_INACTIVE.getCode());
updateById(wmsWarehouses);
}
...4)查看接口文档
重启后端,查看接口文档

7.5.3 启用/禁用功能(前端)
1)定义API
在WmsWarehouses.api.ts中定义启用和禁用的API
enum Api {
list = '/warehouse/wmsWarehouses/list',
save='/warehouse/wmsWarehouses/add',
edit='/warehouse/wmsWarehouses/edit',
deleteOne = '/warehouse/wmsWarehouses/delete',
deleteBatch = '/warehouse/wmsWarehouses/deleteBatch',
importExcel = '/warehouse/wmsWarehouses/importExcel',
exportXls = '/warehouse/wmsWarehouses/exportXls',
//启用
enable = '/warehouse/wmsWarehouses/enable',
//禁用
disable = '/warehouse/wmsWarehouses/disable',
}
/**
* 启用
*/
export const enable = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认启用',
content: '是否启用选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.put({url: Api.enable, data: params},{joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
})
}
/**
* 禁用
*/
export const disable = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认禁用',
content: '是否禁用选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.put({url: Api.disable, data: params},{joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
})
}2) 定义按钮
找到WmsWarehousesList.vue,定义启用/禁用 按钮
/**
* 操作栏
*/
function getTableAction(record){
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'warehouse:wms_warehouses:edit'
},
//启用
{
label: '启用',
onClick: handleEnable.bind(null, record),
},
//禁用
{
label: '禁用',
onClick: handleDisable.bind(null, record),
}
]
}定义handleEnable、handleDisable方法
import {list, deleteOne, batchDelete, getImportUrl,getExportUrl,enable,disable} from './WmsWarehouses.api';
/**
* 启用事件
*/
async function handleEnable(record) {
await enable({id: record.id}, handleSuccess);
}
/**
* 禁用事件
*/
async function handleDisable(record) {
await disable({id: record.id}, handleSuccess);
}3)前后端联调
重启前端、后端,测试启用、禁用功能。
7.6 储区管理子模块
7.6.1 配置数据字典
当前的页面效果:

修改前端代码,表单和列表配置正确的数据字典。
库区类型数据字典:zone_type
状态数据字典:wms_status
后端:

前端列表:
{
title: '库区类型',
align:"center",
dataIndex: 'zoneType_dictText',
},
{
title: '状态: ',
align:"center",
dataIndex: 'status_dictText',
},
{
title: '是否可售库存 ',
align:"center",
dataIndex: 'isSellable_dictText'
},前端表单:
{
label: '库区类型',
field: 'zoneType',
component: 'JDictSelectTag',
componentProps:{
dictCode:"zone_type"
},
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请输入库区类型!'},
];
},
},
{
label: '状态: ',
field: 'status',
component: 'JDictSelectTag',
componentProps:{
dictCode:"wms_status"
},
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请输入状态: 创建,禁用, 启用!'},
];
},
},
{
label: '是否可售库存',
field: 'isSellable',
component: 'JDictSelectTag',
componentProps:{
dictCode:"yn"
},
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请输入是否可售库存 0-否, 1-是!'},
];
},
},修改后效果:

表单:



7.6.2 关联查询所属仓库(方式一)
1)后端
储区表记录的仓库ID,所以在上图中显示了仓库的ID,我们需要根据仓库ID关联查询仓库名称进行显示 。
找到储区查询的接口,原始代码如下:
@ApiOperation(value="储区表-分页列表查询", notes="储区表-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<WmsStorageZones>> queryPageList(WmsStorageZones wmsStorageZones,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<WmsStorageZones> queryWrapper = QueryGenerator.initQueryWrapper(wmsStorageZones, req.getParameterMap());
Page<WmsStorageZones> page = new Page<WmsStorageZones>(pageNo, pageSize);
IPage<WmsStorageZones> pageList = wmsStorageZonesService.page(page, queryWrapper);
return Result.OK(pageList);
}下边需要关联查询仓库,拿到仓库名称:
首先我们修改储区实体类(org.jeecg.modules.wms.warehouse.entity.WmsStorageZones),添加仓库属性
@Data
@TableName("wms_storage_zones")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="wms_storage_zones对象", description="储区表")
public class WmsStorageZones implements Serializable {
....
//仓库对象用于显示
@TableField(exist = false)
private String warehouseName;
}然后修改储区查询接口代码:
@ApiOperation(value="储区表-分页列表查询", notes="储区表-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<WmsStorageZones>> queryPageList(WmsStorageZones wmsStorageZones,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<WmsStorageZones> queryWrapper = QueryGenerator.initQueryWrapper(wmsStorageZones, req.getParameterMap());
Page<WmsStorageZones> page = new Page<WmsStorageZones>(pageNo, pageSize);
IPage<WmsStorageZones> pageList = wmsStorageZonesService.page(page, queryWrapper);
if(pageList.getRecords().size()<=0){
return Result.OK(pageList);
}
// 查询仓库id
List<String> warehousIds = pageList.getRecords().stream().map(item -> item.getWarehouseId()).collect(Collectors.toList());
//获取mapper
BaseMapper<WmsWarehouses> baseMapper = wmsWarehousesService.getBaseMapper();
// 查询仓库信息
List<WmsWarehouses> wmsWarehouses = baseMapper.selectBatchIds(warehousIds);
if(wmsWarehouses==null||wmsWarehouses.isEmpty()){
return Result.OK(pageList);
}
// 设置仓库信息
pageList.getRecords().stream().forEach(item -> {
WmsWarehouses wmsWarehousesTemp = wmsWarehouses.stream().filter(warehouse -> warehouse.getId().equals(item.getWarehouseId())).findFirst().orElse(new WmsWarehouses());
item.setWarehouseName(wmsWarehousesTemp.getWarehouseName());
});
return Result.OK(pageList);
}2)前端
修改储区查询列表配置,指定所属仓库列的取值内容
{
title: '所属仓库',
align:"center",
dataIndex: 'warehouseName',
},修改后的效果:

3)测试
重启后端,测试储区查询列表,所属仓库列是否正常显示仓库名称。
7.6.3 关联查询所属仓库(方式二)
7.6.3.1 Mapper
如果需要关联查询的字段比较多,在一次查询请求中会多次请求数据库,影响查询性能,方式二则是一次查询出所有关联查询的字段。
首先定义mapper接口
public interface WmsStorageZonesMapper extends BaseMapper<WmsStorageZones> {
/**
* 分页查询库区信息
*/
public List<WmsStorageZones> queryList(WmsStorageZones wmsStorageZones);
}mapper映射文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.wms.warehouse.mapper.WmsStorageZonesMapper">
<!--根据入库单号、商品名称等综合查询条件查询入库单明细-->
<select id="queryList" resultType="org.jeecg.modules.wms.warehouse.entity.WmsStorageZones" parameterType="org.jeecg.modules.wms.warehouse.entity.WmsStorageZones">
select a.*,
b.warehouse_name
FROM wms_storage_zones a,wms_warehouses b
WHERE
a.warehouse_id=b.id
<if test="warehouseName != null and warehouseName != ''">
AND b.warehouse_name LIKE concat('%',#{warehouseName},'%')
</if>
<if test="status != null and status != ''">
AND a.status = #{status}
</if>
<if test="warehouseId != null and warehouseId != ''">
AND a.warehouse_id = #{warehouseId}
</if>
</select>
</mapper>7.6.3.2 Service
添加分页插件
这里需要用到分页插件,在wms模块的pom.xml添加依赖,并刷新pom.xml
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>6.1.1</version>
</dependency>拷贝课程资料/源码/后端/config下的PageHelperConfiguration.java到wms模块下的config包下。
通过通过PageInterceptor拦截,拦截sql执行,在执行前添加分页limit

Service
编写接口
public interface IWmsStorageZonesService extends IService<WmsStorageZones> {
/**
* 分页查询库区信息
*/
public IPage<WmsStorageZones> queryList(WmsStorageZones wmsStorageZones, Integer pageNo, Integer pageSize);
}实现类
@Service
public class WmsStorageZonesServiceImpl extends ServiceImpl<WmsStorageZonesMapper, WmsStorageZones> implements IWmsStorageZonesService {
@Override
public IPage<WmsStorageZones> queryList(WmsStorageZones wmsStorageZones, Integer pageNo, Integer pageSize) {
//开启分页查询,当执行查询时,插件进行相关的sql拦截进行分页操作,返回一个page对象
Page<WmsStorageZones> page = PageHelper.startPage(pageNo, pageSize);
List<WmsStorageZones> list = baseMapper.queryList(wmsStorageZones);
PageDTO<WmsStorageZones> wmsStorageZonesPageDTO = new PageDTO<>();
wmsStorageZonesPageDTO.setRecords(list);
wmsStorageZonesPageDTO.setTotal(page.getTotal());//总记录数
wmsStorageZonesPageDTO.setPages(page.getPages());//总页数
wmsStorageZonesPageDTO.setCurrent(page.getPageNum());//当前页码
wmsStorageZonesPageDTO.setSize(page.getPageSize());//每页记录数
return wmsStorageZonesPageDTO;
}
}Controller
@GetMapping(value = "/list")
public Result<IPage<WmsStorageZones>> queryPageList(WmsStorageZones wmsStorageZones,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
IPage<WmsStorageZones> wmsStorageZonesIPage = wmsStorageZonesService.queryList(wmsStorageZones, pageNo, pageSize);
return Result.OK(wmsStorageZonesIPage);
}测试
使用swagger进行测试,更改页码测试查询结果是否正确。

前后端测试:
测试储区查询列表,所属仓库列是否正常显示仓库名称。
7.6.4 分页插件原理
使用com.github.pagehelper分页插件实现分页功能,下边介绍它的执行原理。
PageHelperUtils是项目封装的一个工具类,进入selectPage方法,调用PageHelper.startPage方法设置分页参数,通过一层一层进入源码,最终将分页参数设置到ThreadLocal<Page>LOCAL_PAGE=newThreadLocal(); 中。

通过PageInterceptor拦截器拦截 MyBatis的Executor 的 query() 方法得到原始的sql语句,首先得到count总数,然后从newThreadLocal中取出分页参数,在原始sql语句中添加分页参数查询分页数据。
部分源码截图如下:



7.6.5 实战
1) 增加查询条件
在前端页面增加查询条件,所属仓库下拉框,这里用到了“ApiSelect”组件,文档:https://help.jeecg.com/ui/apiSelect
找到储区列表页面的searchFormSchema

修改:chuangxin-wms-vue-course\src\views\warehouse\WmsStorageZones.data.ts,增加仓库下拉查询条件:
import { list } from './WmsWarehouses.api';
//查询数据
export const searchFormSchema: FormSchema[] = [
//仓库
{
label: '所属仓库',
field: 'warehouseId',
component: 'ApiSelect',
componentProps: {
api: list,
resultField: 'records',
labelField: 'warehouseName',
valueField: 'id',
},
}
];2)所属仓库改为下拉框
在储区添加/修改页面,将所属仓库改为下拉框。
3)查询列表完善数据字典
查询列表显示对应的数据字典名称。
4)增加储区启动/禁用功能
参考需求文档、接口文档完成储区启动、禁用功能。
7.7 储位管理子模块
1) 需求分析
阅读需求文档,进行设计。
2) 下拉级联
参考仓库管理,储区管理模块的实现方式实现储位管理。
前端实现选择仓库级联选择储区的实现方式如下,阅读并理解代码。
修改前端工程:src\views\warehouse\WmsStorageLocations.data.ts 中 formSchema的定义,实现选择“所属仓库”级联列出储区下拉框。
代码如下:
import { list as warehouseList} from "@/views/warehouse/WmsWarehouses.api";
import {list as storageZones} from "@/views/warehouse/WmsStorageZones.api";
export const formSchema: FormSchema[] = [
.....
{
label: '所属仓库',
field: 'warehouseId',
component: 'ApiSelect',
componentProps: ({ formModel, formActionType }) => {
return {
api: warehouseList,
resultField: 'records',
labelField: 'warehouseName',
valueField: 'id',
onChange: (e: any) => {
formModel.zoneId = undefined;
formModel.isStart='2'
if (e === undefined) {
const { updateSchema } = formActionType;
updateSchema({
field: 'zoneId',
component: 'Select',
componentProps: {
options: [],
},
});
return ;
}else{
//请求库区数据
storageZones( {
warehouseId: e
}).then((res) => {
//抽取库区数据
const zones= res.records.map(item => ({
value: item.id,
label: item.zoneName
}));
// console.log(zones)
const { updateSchema } = formActionType;
updateSchema({
field: 'zoneId',
component: 'Select',
componentProps: {
options: zones,
},
});
});
}
},
};
},
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请选择所属仓库!'},
];
},
},
{
field: 'zoneId',
component: 'Select',
label: '所属库区',
componentProps: {
options: [], // defalut []
placeholder: '所属库区',
},
},修改前端工程实现进入修改储位页面时默认选中所属仓库和储区。
找到页面src\views\warehouse\components\WmsStorageLocationsModal.vue


代码如下:
import {ref, computed, unref} from 'vue';
import {BasicModal, useModalInner} from '/@/components/Modal';
import {BasicForm, useForm} from '/@/components/Form/index';
import {formSchema} from '../WmsStorageLocations.data';
import {saveOrUpdate} from '../WmsStorageLocations.api';
import {list as storageZones} from "@/views/warehouse/WmsStorageZones.api";
// Emits声明
const emit = defineEmits(['register','success']);
const isUpdate = ref(true);
const isDetail = ref(false);
//表单配置
const [registerForm, { setProps,resetFields, setFieldsValue, validate, scrollToField,updateSchema }] = useForm({
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: {span: 12}
});
//表单赋值
const [registerModal, {setModalProps, closeModal}] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({confirmLoading: false,showCancelBtn:!!data?.showFooter,showOkBtn:!!data?.showFooter});
isUpdate.value = !!data?.isUpdate;
isDetail.value = !!data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
storageZones( {
warehouseId: data.record.warehouseId
}).then((res) => {
//抽取库区数据
const zones= res.records.map(item => ({
value: item.id,
label: item.zoneName
}));
console.log(zones)
updateSchema({
field: 'zoneId',
component: 'Select',
componentProps: {
options: zones,
},
});
});
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter })
});
....效果如下:

3)完善功能(实战)
参照需求文档,完善储位管理功能。
8 商品管理
8.1 分析设计
1)需求分析
WMS的职责是管理货主的商品,当客户下单后,订单推送到WMS系统,由WMS进行拣货、打包、发货。
所以在WMS需要获取货主在各电商平台的商品信息。
基本流程如下:
- 货主在电商平台开店
- 客户在电商平台下单支付后由WMS进行拣货发货。
如下图:

WMS如何获取到电商平台的商品信息呢?
获取商品信息的方式:
- WMS通过与各电商平台接口对接拿到商品信息。
和各电商平台对接需要有真实的开店账号,这种方式不适合教学。
- 由货主手动添加或导入WMS。
此方式适合教学,这里采用手动维护商品信息的方式。
关于和外部系统接口对接的方法在出库模块会讲。
需要维护商品类别、商品品牌、商品信息、商品图片,部分界面原型如下:
商品分类管理:

商品品牌管理:

商品信息管理:

2)专业概念
电商项目中关于商品管理相关的业务有一些专业概念需要我们提前搞清楚。
商品品牌与分类
通常在商品查询界面提供商品品牌和商品分类检索条件,方便用户搜索到自己想要的商品。
如下图:

品牌:商品的品牌
分类:商品的分类,一般是多级商品分类。

商品规格
商品规格就是商品的销售属性,不同商品分类下商品的规格一般是不同的。
举例:
手机的规格如下图所示:
规格:屏幕刷新率、屏幕特色、屏幕尺寸等
规格值:120HZ、昆仑玻璃、6.82英寸

笔记本的规格:

spu与sku
SPU:Standard Product Unit,标准产品单元,可以理解为一个产品型号,比如小米17 就是一个标准的产品单元,它属于生产制造过程的一个标准品,标准品在缺乏具体规格信息的时候是不能直接售卖的。
SKU:Stock Keeping Unit,最小库存单元,仓库中管理商品的数量其实就是管理SKU的数量,SPU与SKU的关系是一对多,例如:小米17黑128G是一种SKU,小米17黑256也是一种SKU,其中小米17就是一个Spu,黑和128G就是销售属性即商品的规格,在发布商品时会编辑SKU,如下图,你会发现SKU就是不同的商品属性组合而成:

进入商品详细页面也会显示Sku相关的信息,如下图,当你选择一种SKU后才会显示出具体的库存数量。
Sku信息包括:库存信息,Sku名称、Sku图片、关联的存储容量,颜色等销售属性。

3)表设计
通过上边的学习我们清楚,SKU是最小库存单位,WMS中需要与电商系统的商品信息保持同步,其实就是与SKU信息保证同步。
在电商项目中对于商品信息的存储会有SPU表、SKU表,而在WMS中只需要有SKU表即可,所以在WMS 系统中只需要创建商品表来记录商品SKU信息即可。

分类表:树形结构,标志:parent_id
8.3 商品类别管理(实战)
1) 树形表online表单
商品分类是一个树形表,在创建表时需要选择下图中红色框的内容:

parent_id:树形表中的父节点ID
category_name:页面上树节点显示的名称
效果如下图:

对商品分类表生成代码

2) 实战
参考需求文档,新增分类时自动生成分类编码。
需求文档位置:

8.4 商品品牌管理
8.4.1 文件服务配置
1)文件服务介绍
jeecg-boot提供文件服务模块,对上传的文件进行统一管理。
文件存储介质支持阿里OSS和minIO。
本项目使用OSS存储图片。
文件信息存在oss_file表中,表结构如下:
create table oss_file
(
id varchar(32) not null comment '主键id'
primary key,
file_name varchar(255) null comment '文件名称',
url varchar(1000) null comment '文件地址',
create_by varchar(32) null comment '创建人登录名称',
create_time datetime null comment '创建日期',
update_by varchar(32) null comment '更新人登录名称',
update_time datetime null comment '更新日期'
)
comment 'Oss File' charset = utf8mb3;OSS参数配置类:

设置允许上传的文件类型

业务代码如下:

2)配置图片服务
首先每位同学注册阿里云账号(已有的不用重复注册)。
然后创建OSS bucket,地址:https://oss.console.aliyun.com/overview
创建OSS bucket请查阅资料创建。
创建完成需要拿到如下参数,并配置在application-dev.yml中,如下图:
jeecg:
# 本地:local、Minio:minio、阿里云:alioss
uploadType: alioss配置oss参数:

3)测试
重启后端工程
进入前端菜单“组件&功能-->Jeecg组件示例-->对象存储”

点击 OSS文件上传,测试是否正确上传文件。
8.4.2 商品品牌图片上传
1) 配置文件服务域名
在后端和前端配置自己的OSS域名:
后端:

前端:

2)商品品牌添加/修改表单
商品品牌图片上传使用JImageUpload组件。
{
label: '品牌logo',
field: 'logo',
component: 'JImageUpload',
},修改商品品牌回显图片:
\views\goods\components\WmsProductBrandModal.vue
导入全局函数:

在修改商品品牌时默认显示品牌的图片

3)查询列表显示图片
修改src\views\goods\WmsProductBrand.data.ts,配置商品品牌查询列表:
import { useGlobSetting } from '/@/hooks/setting';
const glob = useGlobSetting();
{
title: '品牌logo',
align:"center",
dataIndex: 'logo',
customRender: ({ text }) => {
if(!text){
return text;
}
text=glob.viewUrl+text;
return render.renderImage({text});
},
},4)保存图片路径去掉域名
在保存品牌接口中注意将前端传入的带域名的图片路径中的域名去掉,保存到数据库的图片路径不带域名,如下:

修改后端:jeecg-module-wms\src\main\java\org\jeecg\modules\wms\goods\controller\WmsProductBrandController.java

8.4.2 完善其它功能(实战)
请同学们参考需求文档完善品牌管理模块其它功能。
8.5 商品信息管理(实战)
请同学们参考需求文档、接口文档自行实现。
提示:货主下拉框建议使用“JSearchSelect”组件。
