WMS 仓库管理系统设计(基于RuoYi-Pro-Mini)
本系统基于 RuoYi-Pro-Mini 框架开发,充分复用框架提供的基础功能模块。
框架核心优势:
- ✅ 开箱即用的权限管理:用户、角色、菜单、权限一体化
- ✅ 完善的数据字典:支持动态配置、多级联动
- ✅ 操作日志审计:全链路操作记录、异常追踪
- ✅ 代码生成器:快速生成CRUD代码,提升开发效率80%
- ✅ 轻量级设计:相比RuoYi-Vue-Pro,更轻量、更简洁
技术栈:
- 后端:Spring Boot 3.2.x + MyBatis-Plus 3.5.x + Spring Security + JWT
- 前端:Vue 3 + Element Plus + TypeScript + Pinia
- 数据库:MySQL 8.0 + Redis 7.0
- 消息队列:RocketMQ 5.x(可选)
- 工具库:Hutool、Lombok、MapStruct
表命名规范:
- 系统基础表:
system_*(由RuoYi框架提供,无需创建) - WMS业务表:
wms_*(本系统业务表,需要创建)
一、项目概述
1.1 系统简介
WMS(Warehouse Management System)仓库管理系统是一个基于 RuoYi-Pro-Mini 框架的智能仓储管理平台,旨在通过数字化手段提升仓库运营效率,实现库存实时可视化、出入库自动化、智能拣货调度等核心功能。
与传统WMS的差异:
- 基于成熟框架,开发周期缩短50%
- 复用权限管理,无需重复开发
- 统一技术栈,降低维护成本
- 代码生成器加速开发
1.2 核心价值
| 价值点 | 说明 | 预期效果 |
|---|---|---|
| 🎯 提升效率 | 自动化拣货路径优化 | 拣货效率提升 40% |
| 📊 实时可视 | 库存实时监控和预警 | 库存准确率达 99.5% |
| 🔄 降低成本 | 减少人工盘点和错误 | 运营成本降低 30% |
| 📈 数据分析 | 智能报表和决策支持 | 决策效率提升 50% |
| 🚀 快速响应 | 订单自动分配和处理 | 出库时效提升 35% |
1.3 应用场景
📦 电商仓储
- 高频出入库操作
- 多SKU商品管理
- 订单快速履约
- 库存周转优化
🏭 制造业仓库
- 原材料管理
- 成品/半成品存储
- 生产线配送
- 呆滞库存预警
🏪 零售配送中心
- 多门店配货
- 补货计划管理
- 退货处理
- 库存调拨
1.4 技术架构
架构设计思想
为什么采用微服务架构?
- 业务解耦:仓储业务复杂度高,入库、出库、拣货等模块独立性强,微服务化便于团队协作和独立演进
- 弹性扩展:库存查询、拣货任务等高并发场景可独立扩展,避免资源浪费
- 故障隔离:某个服务故障不影响整体系统,提高可用性
- 技术异构:报表服务可使用ElasticSearch,日志服务可用MongoDB,各取所长
核心组件选型说明
| 组件类型 | 技术选型 | 选型理由 | 替代方案 |
|---|---|---|---|
| 服务注册 | Nacos | 国产化、配置中心集成、社区活跃 | Consul, Eureka |
| 网关 | Spring Cloud Gateway | 异步非阻塞、性能优秀、Spring生态 | Zuul 2.0, Kong |
| 负载均衡 | Spring Cloud LoadBalancer | 轻量级、可定制 | Ribbon(已停更) |
| 服务调用 | OpenFeign + OkHttp | 声明式、可读性强、支持HTTP/2 | Dubbo, gRPC |
| 熔断限流 | Sentinel | 实时监控、规则丰富、国产化 | Hystrix(停更), Resilience4j |
| 链路追踪 | SkyWalking | APM全栈、国产、无侵入 | Zipkin, Jaeger |
| 消息队列 | RocketMQ | 顺序消息、事务消息、高吞吐 | Kafka, RabbitMQ |
| 数据库 | MySQL 8.0 | ACID保证、成熟稳定、生态完善 | PostgreSQL, TiDB |
| 缓存 | Redis 7.0 | 高性能、数据结构丰富、分布式锁 | Memcached, Hazelcast |
| 搜索引擎 | ElasticSearch | 全文检索、日志分析、实时聚合 | Solr, OpenSearch |
| 对象存储 | MinIO | 兼容S3、部署简单、私有化 | FastDFS, OSS |
| 定时任务 | XXL-Job | 分布式、可视化、失败重试 | Quartz, Elastic-Job |
1.5 技术栈
后端技术栈
1<properties>2 <!-- Spring 全家桶 -->3 <spring-boot.version>3.1.5</spring-boot.version>4 <spring-cloud.version>2022.0.4</spring-cloud.version>5 <spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>6 7 <!-- 数据库 -->8 <mybatis-plus.version>3.5.4.1</mybatis-plus.version>9 <mysql.version>8.0.33</mysql.version>10 <redis.version>3.1.5</redis.version>11 12 <!-- 消息队列 -->13 <rocketmq.version>2.2.3</rocketmq.version>14 15 <!-- 工具类 -->16 <hutool.version>5.8.22</hutool.version>17 <lombok.version>1.18.30</lombok.version>18 <mapstruct.version>1.5.5.Final</mapstruct.version>19</properties>前端技术栈
1{2 "dependencies": {3 "vue": "^3.3.4",4 "vue-router": "^4.2.5",5 "pinia": "^2.1.7",6 "element-plus": "^2.4.2",7 "axios": "^1.6.0",8 "echarts": "^5.4.3",9 "vxe-table": "^4.5.0"10 }11}二、核心功能模块
2.1 功能架构图
2.2 库存管理
2.2.1 核心能力
| 功能 | 说明 | 关键指标 | 实现难点 |
|---|---|---|---|
| 实时库存 | 库存数据实时更新 | 延迟 < 100ms | 高并发下的数据一致性 |
| 批次管理 | 生产批次追溯 | 批次准确率 100% | 先进先出(FIFO)策略 |
| 库龄分析 | 库存周转分析 | 预警准确率 95% | 大数据量统计性能 |
| 安全库存 | 库存上下限预警 | 缺货率 < 1% | 动态阈值计算 |
| 库存锁定 | 订单库存预占 | 并发支持 10000+ | 分布式锁+乐观锁 |
2.2.2 库存状态流转
状态说明:
- 可用库存:正常可销售/可用状态
- 锁定库存:已分配给订单但未出库(订单取消后自动释放)
- 拣货中:正在执行拣货任务
- 冻结库存:质检不合格、临期商品等
- 报损库存:已损坏需报损处理
2.2.3 库存扣减策略
为什么采用"下单锁定 + 出库扣减"模式?
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 下单直接扣减 | 实现简单 | 订单取消需回滚、库存占用率低 | 低退款率业务 |
| 下单锁定+出库扣减 ✅ | 库存利用率高、支持超卖控制 | 实现复杂、需锁定机制 | 高并发电商场景 |
| 预扣+异步确认 | 性能好 | 最终一致性、补偿逻辑复杂 | 秒杀场景 |
我们采用方案2的理由:
- 提高库存利用率:订单支付前库存仍可销售(设置锁定时效)
- 防止超卖:通过Redis分布式锁保证原子性
- 支持灵活策略:可配置锁定时长、自动释放规则
2.2.4 并发库存扣减方案
核心挑战: 高并发场景下如何保证库存扣减的准确性和性能?
1/**2 * 库存扣减 - 三重保障机制3 * 1. Redis分布式锁:保证同一商品同一时刻只有一个线程操作4 * 2. 数据库行锁(FOR UPDATE):保证数据库层面的并发安全5 * 3. 乐观锁(版本号):作为兜底机制6 */7@Override8@Transactional(rollbackFor = Exception.class)9public boolean deductInventory(Long goodsId, BigDecimal quantity) {10 // 分布式锁Key11 String lockKey = "inventory:lock:" + goodsId;12 RLock lock = redissonClient.getLock(lockKey);13 14 try {15 // 1. 尝试获取分布式锁(等待3秒,锁定10秒)16 boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);17 if (!acquired) {18 throw new BizException("系统繁忙,请稍后重试");19 }20 21 // 2. 查询库存(行锁)22 Inventory inventory = inventoryMapper.selectForUpdate(goodsId);23 24 // 3. 检查库存充足性25 if (inventory.getAvailableQuantity().compareTo(quantity) < 0) {26 throw new BizException("库存不足");27 }28 29 // 4. 扣减库存(乐观锁)30 int updated = inventoryMapper.deductWithVersion(31 goodsId, quantity, inventory.getVersion()32 );33 34 if (updated == 0) {35 throw new BizException("库存更新失败,请重试");36 }37 38 // 5. 记录库存流水(异步)39 inventoryLogProducer.sendLog(inventory, quantity);40 41 // 6. 清除缓存42 redisTemplate.delete("inventory:" + goodsId);43 44 return true;45 46 } finally {47 // 释放锁48 if (lock.isHeldByCurrentThread()) {49 lock.unlock();50 }51 }52}性能优化点:
- 锁粒度:按商品ID加锁,不同商品并行处理
- 锁等待:3秒超时快速失败,避免线程堆积
- 异步日志:库存流水异步记录,不阻塞主流程
- 缓存删除:扣减后立即删除缓存,保证下次查询最新数据
2.2.5 库存预警机制
设计思路: 多维度、智能化的库存预警体系
预警规则配置:
| 预警类型 | 触发条件 | 预警级别 | 处理建议 |
|---|---|---|---|
| 安全库存 | 可用库存 < 安全库存 | ⚠️ 警告 | 及时补货 |
| 缺货 | 可用库存 = 0 | 🔴 严重 | 紧急采购 |
| 库龄超期 | 入库天数 > 90天 | ⚠️ 警告 | 促销清仓 |
| 临期商品 | 距离过期 < 30天 | 🔴 严重 | 加速销售 |
| 滞销 | 30天销量 = 0 | ⚠️ 警告 | 调整策略 |
| 库位占满 | 库位利用率 > 95% | ⚠️ 警告 | 扩容/清理 |
1/**2 * 库存预警定时任务3 * 每小时执行一次全量扫描4 */5@Scheduled(cron = "0 0 * * * ?")6public void checkInventoryAlert() {7 // 1. 安全库存预警8 List<Inventory> lowStock = inventoryMapper.selectBelowSafetyStock();9 lowStock.forEach(inv -> {10 AlertMessage alert = AlertMessage.builder()11 .type("SAFETY_STOCK")12 .level("WARNING")13 .goodsName(inv.getGoodsName())14 .currentQty(inv.getAvailableQuantity())15 .safetyQty(inv.getSafetyStock())16 .suggestion("建议补货数量:" + (inv.getSafetyStock().multiply(new BigDecimal("1.5"))))17 .build();18 alertService.send(alert);19 });20 21 // 2. 库龄预警(超过90天)22 LocalDateTime deadline = LocalDateTime.now().minusDays(90);23 List<Inventory> aged = inventoryMapper.selectByInboundDateBefore(deadline);24 // ... 发送预警25 26 // 3. 临期商品预警(30天内过期)27 LocalDate expireDeadline = LocalDate.now().plusDays(30);28 List<Inventory> nearExpire = inventoryMapper.selectByExpireDateBefore(expireDeadline);29 // ... 发送预警30}2.2.6 库存快照与对账
为什么需要库存快照?
- 数据追溯:出现库存差异时快速定位问题时间点
- 报表统计:月末/年末库存报表生成
- 审计合规:满足财务审计要求
1/**2 * 库存快照 - 每日凌晨自动生成3 */4@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点5public void createDailySnapshot() {6 String snapshotDate = LocalDate.now().toString();7 8 // 1. 查询所有库存9 List<Inventory> inventories = inventoryMapper.selectAll();10 11 // 2. 批量插入快照表12 List<InventorySnapshot> snapshots = inventories.stream()13 .map(inv -> InventorySnapshot.builder()14 .snapshotDate(snapshotDate)15 .warehouseId(inv.getWarehouseId())16 .goodsId(inv.getGoodsId())17 .quantity(inv.getQuantity())18 .lockQuantity(inv.getLockQuantity())19 .build())20 .collect(Collectors.toList());21 22 snapshotMapper.batchInsert(snapshots);23 24 // 3. 触发库存对账任务25 reconciliationService.reconcile(snapshotDate);26}2728/**29 * 库存对账 - 系统库存 vs 实际盘点30 */31public ReconciliationResult reconcile(String date) {32 // 1. 获取系统库存快照33 List<InventorySnapshot> systemStock = snapshotMapper.selectByDate(date);34 35 // 2. 获取实际盘点数据36 List<StockTaking> actualStock = stockTakingMapper.selectByDate(date);37 38 // 3. 对比差异39 List<InventoryDiff> diffs = compareInventory(systemStock, actualStock);40 41 // 4. 生成对账报告42 return ReconciliationResult.builder()43 .date(date)44 .totalItems(systemStock.size())45 .diffItems(diffs.size())46 .diffRate(diffs.size() * 100.0 / systemStock.size())47 .details(diffs)48 .build();49}2.3 入库管理
入库流程
入库类型
- 采购入库:供应商采购到货
- 退货入库:客户退货入库
- 调拨入库:其他仓库调入
- 盘盈入库:盘点发现多余库存
- 生产入库:生产完工入库
2.4 出库管理
出库流程
2.5 拣货管理
2.5.1 拣货策略对比
策略详细对比:
| 策略 | 适用场景 | 优势 | 劣势 | 效率提升 |
|---|---|---|---|---|
| 单品拣货 | B2B大单、特殊商品 | 准确率高、流程简单 | 效率低、重复路径 | 基准 |
| 批次拣货 ✅ | 电商多单、相同SKU | 减少行走路径、提升效率 | 需二次分拣 | 提升30% |
| 分区拣货 | 大型仓库、多库区 | 并行作业、降低拥堵 | 需交接区、协调复杂 | 提升50% |
| 波次拣货 ✅ | 高并发订单、大促场景 | 最大化效率、智能调度 | 系统复杂度高 | 提升60% |
我们采用"波次拣货+批次拣货"组合策略的原因:
- 订单聚合:将时间窗口内的订单聚合成波次,一次性处理
- 路径优化:波次内统一规划拣货路径,减少重复行走
- 人员均衡:根据拣货员位置和工作量智能分配任务
- 灵活调整:支持紧急订单插入、优先级调整
2.5.2 波次生成算法
核心目标: 在满足时效要求的前提下,最大化拣货效率
1/**2 * 波次生成策略3 * 考虑因素:订单优先级、商品位置、拣货员状态、截单时间4 */5@Service6public class WaveGenerationService {7 8 /**9 * 智能生成波次10 * @param orders 待处理订单列表11 * @return 波次列表12 */13 public List<PickingWave> generateWaves(List<Order> orders) {14 List<PickingWave> waves = new ArrayList<>();15 16 // 1. 订单预处理:按优先级、截单时间排序17 orders.sort(Comparator18 .comparing(Order::getPriority).reversed()19 .thenComparing(Order::getDeadline));20 21 // 2. 订单聚类:相同库区、相似路径的订单分为一组22 Map<String, List<Order>> clusters = clusterOrders(orders);23 24 // 3. 生成波次:每个聚类生成一个波次25 for (Map.Entry<String, List<Order>> entry : clusters.entrySet()) {26 List<Order> clusterOrders = entry.getValue();27 28 // 波次大小控制:30-50单/波次(根据仓库规模调整)29 int waveSize = 40;30 for (int i = 0; i < clusterOrders.size(); i += waveSize) {31 List<Order> waveOrders = clusterOrders.subList(32 i, Math.min(i + waveSize, clusterOrders.size())33 );34 35 PickingWave wave = PickingWave.builder()36 .waveNo(generateWaveNo())37 .orders(waveOrders)38 .priority(calculateWavePriority(waveOrders))39 .estimatedTime(estimatePickingTime(waveOrders))40 .status(WaveStatus.PENDING)41 .build();42 43 waves.add(wave);44 }45 }46 47 return waves;48 }49 50 /**51 * 订单聚类算法(K-means变种)52 * 根据商品位置信息,将订单聚类到相似区域53 */54 private Map<String, List<Order>> clusterOrders(List<Order> orders) {55 Map<String, List<Order>> clusters = new HashMap<>();56 57 for (Order order : orders) {58 // 计算订单的"重心位置"(所有商品位置的平均值)59 Location centerLocation = calculateCenterLocation(order);60 61 // 分配到最近的库区62 String zoneCode = locationService.getNearestZone(centerLocation);63 64 clusters.computeIfAbsent(zoneCode, k -> new ArrayList<>()).add(order);65 }66 67 return clusters;68 }69 70 /**71 * 估算拣货时间72 * 公式:基础时间 + 商品数量×单品时间 + 行走距离×移动时间73 */74 private int estimatePickingTime(List<Order> orders) {75 int baseTime = 60; // 基础准备时间:60秒76 int itemTime = 10; // 每个商品拣选时间:10秒77 int moveSpeed = 1; // 移动速度:1米/秒78 79 // 统计商品总数80 int totalItems = orders.stream()81 .mapToInt(order -> order.getItems().size())82 .sum();83 84 // 计算拣货路径总长度85 double totalDistance = calculatePathDistance(orders);86 87 return baseTime + (totalItems * itemTime) + (int)(totalDistance / moveSpeed);88 }89}2.5.3 拣货路径优化算法
问题本质: 旅行商问题(TSP)的变种 - 访问所有库位并回到起点,路径最短
算法选择:
| 算法 | 时间复杂度 | 优化效果 | 适用规模 | 是否采用 |
|---|---|---|---|---|
| 暴力枚举 | O(n!) | 100%最优 | n < 10 | ❌ 不实用 |
| 动态规划 | O(n²·2ⁿ) | 100%最优 | n < 20 | ❌ 性能差 |
| 贪心算法 | O(n²) | 80-90%优化 | n < 1000 | ✅ 采用 |
| 遗传算法 | O(n·g·p) | 85-95%优化 | n > 1000 | ⚪ 备选 |
我们采用贪心算法的原因:
- 实时性要求:拣货任务需要秒级响应,不能等待长时间计算
- 效果足够:80-90%的优化效果已能显著提升效率
- 实现简单:便于维护和调整
1/**2 * 拣货路径优化 - 改进的贪心算法3 * 从库区入口开始,每次选择距离当前位置最近且未访问的库位4 */5@Service6public class PickingPathOptimizer {7 8 /**9 * 优化拣货路径10 * @param locations 需要访问的库位列表11 * @return 优化后的库位顺序12 */13 public List<Location> optimizePath(List<Location> locations) {14 if (locations.size() <= 1) {15 return locations;16 }17 18 List<Location> optimizedPath = new ArrayList<>();19 Set<Location> unvisited = new HashSet<>(locations);20 21 // 1. 起点:选择距离库区入口最近的库位22 Location entrance = getWarehouseEntrance();23 Location current = findNearest(entrance, unvisited);24 optimizedPath.add(current);25 unvisited.remove(current);26 27 // 2. 贪心选择:每次选择最近的未访问库位28 while (!unvisited.isEmpty()) {29 Location nearest = findNearest(current, unvisited);30 optimizedPath.add(nearest);31 unvisited.remove(nearest);32 current = nearest;33 }34 35 // 3. 路径微调:检测并消除交叉路径(可选优化)36 optimizedPath = eliminateCrossings(optimizedPath);37 38 return optimizedPath;39 }40 41 /**42 * 计算两个库位之间的曼哈顿距离43 * 仓库通道为直角结构,不能斜穿,因此使用曼哈顿距离而非欧式距离44 */45 private double calculateDistance(Location loc1, Location loc2) {46 // 横向距离47 int rowDiff = Math.abs(loc1.getRowNo() - loc2.getRowNo());48 // 纵向距离 49 int colDiff = Math.abs(loc1.getColumnNo() - loc2.getColumnNo());50 // 层间距离(爬楼梯成本更高)51 int layerDiff = Math.abs(loc1.getLayerNo() - loc2.getLayerNo());52 53 // 加权计算:层间移动成本是水平移动的2倍54 return rowDiff + colDiff + layerDiff * 2.0;55 }56 57 /**58 * 查找距离目标位置最近的库位59 */60 private Location findNearest(Location target, Set<Location> candidates) {61 return candidates.stream()62 .min(Comparator.comparingDouble(loc -> calculateDistance(target, loc)))63 .orElseThrow(() -> new BizException("没有可用的候选库位"));64 }65 66 /**67 * 消除交叉路径(2-opt优化)68 * 检测路径中的交叉点,并进行局部调整69 */70 private List<Location> eliminateCrossings(List<Location> path) {71 boolean improved = true;72 List<Location> optimized = new ArrayList<>(path);73 74 // 迭代优化,直到没有改进75 while (improved) {76 improved = false;77 78 // 检查所有可能的边交换79 for (int i = 0; i < optimized.size() - 2; i++) {80 for (int j = i + 2; j < optimized.size() - 1; j++) {81 // 计算当前距离82 double currentDist = 83 calculateDistance(optimized.get(i), optimized.get(i+1)) +84 calculateDistance(optimized.get(j), optimized.get(j+1));85 86 // 计算交换后的距离87 double newDist = 88 calculateDistance(optimized.get(i), optimized.get(j)) +89 calculateDistance(optimized.get(i+1), optimized.get(j+1));90 91 // 如果交换后更短,则执行交换92 if (newDist < currentDist) {93 // 反转 i+1 到 j 之间的路径94 Collections.reverse(95 optimized.subList(i + 1, j + 1)96 );97 improved = true;98 }99 }100 }101 }102 103 return optimized;104 }105}优化效果对比:
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 平均拣货路径 | 450米 | 280米 | ↓ 38% |
| 平均拣货时间 | 25分钟 | 16分钟 | ↓ 36% |
| 拣货员日产能 | 180单 | 280单 | ↑ 56% |
| 路径交叉次数 | 8次 | 1次 | ↓ 88% |
2.5.4 拣货任务分配策略
目标: 实现拣货员工作负载均衡,提高整体效率
1/**2 * 拣货任务分配 - 综合评分算法3 * 考虑因素:拣货员当前位置、工作负载、技能等级、任务优先级4 */5@Service6public class TaskAssignmentService {7 8 /**9 * 为波次分配最合适的拣货员10 */11 public Picker assignPicker(PickingWave wave) {12 // 1. 获取所有空闲或即将空闲的拣货员13 List<Picker> availablePickers = pickerService.getAvailablePickers();14 15 if (availablePickers.isEmpty()) {16 throw new BizException("暂无可用拣货员");17 }18 19 // 2. 计算每个拣货员的综合评分20 Picker bestPicker = availablePickers.stream()21 .max(Comparator.comparingDouble(picker -> 22 calculatePickerScore(picker, wave)))23 .orElseThrow();24 25 // 3. 分配任务26 wave.setPickerId(bestPicker.getId());27 wave.setStatus(WaveStatus.ASSIGNED);28 waveMapper.updateById(wave);29 30 // 4. 通知拣货员(推送到PDA)31 pdaService.pushTask(bestPicker.getId(), wave);32 33 return bestPicker;34 }35 36 /**37 * 拣货员评分算法38 * 评分越高,越适合执行该任务39 */40 private double calculatePickerScore(Picker picker, PickingWave wave) {41 double score = 0;42 43 // 1. 位置得分(40%权重):拣货员距离任务起点越近,得分越高44 Location waveStartLocation = wave.getStartLocation();45 double distance = calculateDistance(picker.getCurrentLocation(), waveStartLocation);46 double locationScore = Math.max(0, 100 - distance); // 距离每增加1米减1分47 score += locationScore * 0.4;48 49 // 2. 负载得分(30%权重):当前工作量越少,得分越高50 int currentTasks = picker.getCurrentTaskCount();51 double loadScore = Math.max(0, 100 - currentTasks * 10); // 每个任务减10分52 score += loadScore * 0.3;53 54 // 3. 技能得分(20%权重):技能等级越高,得分越高55 double skillScore = picker.getSkillLevel() * 20; // 1-5级,每级20分56 score += skillScore * 0.2;57 58 // 4. 效率得分(10%权重):历史效率越高,得分越高59 double efficiencyScore = picker.getEfficiencyRate(); // 0-10060 score += efficiencyScore * 0.1;61 62 return score;63 }64}2.5.5 拣货异常处理
常见异常场景及处理方案:
| 异常类型 | 触发条件 | 处理方案 | 是否需要人工介入 |
|---|---|---|---|
| 库存不足 | 实际库存 < 待拣数量 | 自动减单或转采购 | ⚠️ 需确认 |
| 商品破损 | 质检发现问题 | 标记残次、寻找替代库位 | ✅ 需介入 |
| 库位空缺 | 扫描库位无货 | 触发盘点任务、查找其他批次 | ✅ 需介入 |
| 拣货超时 | 执行时间 > 预估时间×1.5 | 发送提醒、触发协助请求 | ⚠️ 视情况 |
| 拣错商品 | 复核发现SKU不符 | 回退重拣、记录错误率 | ❌ 自动处理 |
1/**2 * 拣货异常处理服务3 */4@Service5public class PickingExceptionHandler {6 7 /**8 * 处理库位空缺异常9 * 策略:自动寻找替代库位 -> 失败则转人工处理10 */11 @Transactional(rollbackFor = Exception.class)12 public void handleEmptyLocation(PickingTask task, Location emptyLocation) {13 // 1. 记录异常14 PickingException exception = PickingException.builder()15 .taskId(task.getId())16 .type(ExceptionType.EMPTY_LOCATION)17 .locationId(emptyLocation.getId())18 .build();19 exceptionMapper.insert(exception);20 21 // 2. 触发盘点任务(异步)22 stockTakingService.createUrgentTask(emptyLocation);23 24 // 3. 查找替代库位25 List<Location> alternativeLocations = inventoryService26 .findAlternativeLocations(task.getGoodsId(), task.getBatchNo());27 28 if (!alternativeLocations.isEmpty()) {29 // 有替代库位,自动切换30 Location alternative = alternativeLocations.get(0);31 task.setLocationId(alternative.getId());32 task.setStatus(TaskStatus.RETRY);33 taskMapper.updateById(task);34 35 // 推送新库位给拣货员36 pdaService.pushLocationChange(task.getPickerId(), alternative);37 } else {38 // 无替代库位,转人工处理39 task.setStatus(TaskStatus.EXCEPTION);40 taskMapper.updateById(task);41 42 // 通知仓库主管43 alertService.notifyManager(44 "拣货异常:商品无替代库位",45 task.getGoodsName()46 );47 }48 }49}三、数据库设计
3.1 核心表结构
3.2 完整建表语句
1-- ================================2-- WMS 仓库管理系统数据库3-- ================================45CREATE DATABASE IF NOT EXISTS `wms` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;6USE `wms`;78-- ================================9-- 1. 仓库基础表10-- ================================1112-- 仓库表13CREATE TABLE `warehouse` (14 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',15 `warehouse_code` VARCHAR(50) NOT NULL COMMENT '仓库编码',16 `warehouse_name` VARCHAR(100) NOT NULL COMMENT '仓库名称',17 `warehouse_type` TINYINT(4) DEFAULT 1 COMMENT '仓库类型:1-成品仓,2-原料仓,3-半成品仓',18 `province` VARCHAR(50) DEFAULT NULL COMMENT '省份',19 `city` VARCHAR(50) DEFAULT NULL COMMENT '城市',20 `district` VARCHAR(50) DEFAULT NULL COMMENT '区县',21 `address` VARCHAR(200) DEFAULT NULL COMMENT '详细地址',22 `contact_person` VARCHAR(50) DEFAULT NULL COMMENT '联系人',23 `contact_phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话',24 `total_area` DECIMAL(10,2) DEFAULT NULL COMMENT '总面积(平方米)',25 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-启用,0-禁用',26 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',27 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',28 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',29 PRIMARY KEY (`id`),30 UNIQUE KEY `uk_warehouse_code` (`warehouse_code`)31) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='仓库表';3233-- 库区表34CREATE TABLE `warehouse_area` (35 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',36 `warehouse_id` BIGINT(20) NOT NULL COMMENT '仓库ID',37 `area_code` VARCHAR(50) NOT NULL COMMENT '库区编码',38 `area_name` VARCHAR(100) NOT NULL COMMENT '库区名称',39 `area_type` VARCHAR(20) DEFAULT NULL COMMENT '库区类型:STORAGE-存储区,PICKING-拣货区,STAGING-暂存区',40 `floor` INT(11) DEFAULT 1 COMMENT '楼层',41 `area_size` DECIMAL(10,2) DEFAULT NULL COMMENT '面积',42 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-启用,0-禁用',43 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',44 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',45 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',46 PRIMARY KEY (`id`),47 UNIQUE KEY `uk_area_code` (`warehouse_id`, `area_code`),48 KEY `idx_warehouse` (`warehouse_id`)49) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库区表';5051-- 库位表52CREATE TABLE `warehouse_location` (53 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',54 `warehouse_id` BIGINT(20) NOT NULL COMMENT '仓库ID',55 `area_id` BIGINT(20) NOT NULL COMMENT '库区ID',56 `location_code` VARCHAR(50) NOT NULL COMMENT '库位编码',57 `location_type` VARCHAR(20) DEFAULT 'NORMAL' COMMENT '库位类型:NORMAL-普通,TEMP-临时,DEFECT-残次品',58 `row_no` INT(11) DEFAULT NULL COMMENT '排号',59 `column_no` INT(11) DEFAULT NULL COMMENT '列号',60 `layer_no` INT(11) DEFAULT NULL COMMENT '层号',61 `capacity` DECIMAL(10,2) DEFAULT NULL COMMENT '容量',62 `max_weight` DECIMAL(10,2) DEFAULT NULL COMMENT '最大承重(KG)',63 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-空闲,2-占用,3-锁定,0-禁用',64 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',65 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',66 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',67 PRIMARY KEY (`id`),68 UNIQUE KEY `uk_location_code` (`warehouse_id`, `location_code`),69 KEY `idx_area` (`area_id`),70 KEY `idx_status` (`status`)71) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库位表';7273-- ================================74-- 2. 商品管理表75-- ================================7677-- 商品分类表78CREATE TABLE `goods_category` (79 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',80 `category_code` VARCHAR(50) NOT NULL COMMENT '分类编码',81 `category_name` VARCHAR(100) NOT NULL COMMENT '分类名称',82 `parent_id` BIGINT(20) DEFAULT 0 COMMENT '父分类ID',83 `level` INT(11) DEFAULT 1 COMMENT '层级',84 `sort_order` INT(11) DEFAULT 0 COMMENT '排序',85 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',86 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',87 PRIMARY KEY (`id`),88 UNIQUE KEY `uk_category_code` (`category_code`)89) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表';9091-- 商品信息表92CREATE TABLE `goods` (93 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',94 `sku_code` VARCHAR(50) NOT NULL COMMENT 'SKU编码',95 `goods_name` VARCHAR(200) NOT NULL COMMENT '商品名称',96 `category_id` BIGINT(20) DEFAULT NULL COMMENT '分类ID',97 `brand` VARCHAR(100) DEFAULT NULL COMMENT '品牌',98 `model` VARCHAR(100) DEFAULT NULL COMMENT '型号',99 `barcode` VARCHAR(50) DEFAULT NULL COMMENT '条形码',100 `unit` VARCHAR(20) DEFAULT 'PCS' COMMENT '计量单位',101 `spec` VARCHAR(200) DEFAULT NULL COMMENT '规格',102 `weight` DECIMAL(10,3) DEFAULT NULL COMMENT '重量(KG)',103 `volume` DECIMAL(10,3) DEFAULT NULL COMMENT '体积(立方米)',104 `shelf_life` INT(11) DEFAULT NULL COMMENT '保质期(天)',105 `storage_temp_min` DECIMAL(5,2) DEFAULT NULL COMMENT '最低存储温度',106 `storage_temp_max` DECIMAL(5,2) DEFAULT NULL COMMENT '最高存储温度',107 `need_batch` TINYINT(4) DEFAULT 0 COMMENT '是否批次管理:1-是,0-否',108 `need_serial` TINYINT(4) DEFAULT 0 COMMENT '是否序列号管理:1-是,0-否',109 `safety_stock` DECIMAL(10,2) DEFAULT 0 COMMENT '安全库存',110 `max_stock` DECIMAL(10,2) DEFAULT NULL COMMENT '最大库存',111 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-启用,0-禁用',112 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',113 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',114 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',115 PRIMARY KEY (`id`),116 UNIQUE KEY `uk_sku_code` (`sku_code`),117 KEY `idx_category` (`category_id`),118 KEY `idx_barcode` (`barcode`)119) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';120121-- ================================122-- 3. 库存管理表123-- ================================124125-- 库存表126CREATE TABLE `inventory` (127 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',128 `warehouse_id` BIGINT(20) NOT NULL COMMENT '仓库ID',129 `location_id` BIGINT(20) DEFAULT NULL COMMENT '库位ID',130 `goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',131 `batch_no` VARCHAR(50) DEFAULT NULL COMMENT '批次号',132 `serial_no` VARCHAR(50) DEFAULT NULL COMMENT '序列号',133 `quantity` DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '库存数量',134 `lock_quantity` DECIMAL(10,2) DEFAULT 0 COMMENT '锁定数量',135 `available_quantity` DECIMAL(10,2) GENERATED ALWAYS AS (`quantity` - `lock_quantity`) VIRTUAL COMMENT '可用数量',136 `production_date` DATE DEFAULT NULL COMMENT '生产日期',137 `expire_date` DATE DEFAULT NULL COMMENT '过期日期',138 `inbound_date` DATETIME DEFAULT NULL COMMENT '入库日期',139 `supplier_id` BIGINT(20) DEFAULT NULL COMMENT '供应商ID',140 `supplier_name` VARCHAR(100) DEFAULT NULL COMMENT '供应商名称',141 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-正常,2-冻结,3-待检,4-损坏',142 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',143 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',144 PRIMARY KEY (`id`),145 UNIQUE KEY `uk_inventory` (`warehouse_id`, `location_id`, `goods_id`, `batch_no`, `serial_no`),146 KEY `idx_goods` (`goods_id`),147 KEY `idx_location` (`location_id`),148 KEY `idx_batch` (`batch_no`),149 KEY `idx_status` (`status`)150) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';151152-- 库存流水表153CREATE TABLE `inventory_log` (154 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',155 `warehouse_id` BIGINT(20) NOT NULL COMMENT '仓库ID',156 `goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',157 `location_id` BIGINT(20) DEFAULT NULL COMMENT '库位ID',158 `batch_no` VARCHAR(50) DEFAULT NULL COMMENT '批次号',159 `operation_type` VARCHAR(20) NOT NULL COMMENT '操作类型:INBOUND-入库,OUTBOUND-出库,MOVE-移库,LOCK-锁定,UNLOCK-解锁',160 `quantity_before` DECIMAL(10,2) DEFAULT NULL COMMENT '操作前数量',161 `quantity_change` DECIMAL(10,2) NOT NULL COMMENT '变化数量',162 `quantity_after` DECIMAL(10,2) DEFAULT NULL COMMENT '操作后数量',163 `business_type` VARCHAR(50) DEFAULT NULL COMMENT '业务类型',164 `business_no` VARCHAR(50) DEFAULT NULL COMMENT '业务单号',165 `operator` VARCHAR(50) DEFAULT NULL COMMENT '操作人',166 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',167 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',168 PRIMARY KEY (`id`),169 KEY `idx_warehouse_goods` (`warehouse_id`, `goods_id`),170 KEY `idx_business` (`business_type`, `business_no`),171 KEY `idx_create_time` (`create_time`)172) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表';173174-- ================================175-- 4. 入库管理表176-- ================================177178-- 入库单表179CREATE TABLE `inbound_order` (180 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',181 `inbound_no` VARCHAR(50) NOT NULL COMMENT '入库单号',182 `warehouse_id` BIGINT(20) NOT NULL COMMENT '仓库ID',183 `inbound_type` VARCHAR(20) NOT NULL COMMENT '入库类型:PURCHASE-采购,RETURN-退货,TRANSFER-调拨,PROFIT-盘盈,PRODUCTION-生产,OTHER-其他',184 `source_no` VARCHAR(50) DEFAULT NULL COMMENT '来源单号',185 `supplier_id` BIGINT(20) DEFAULT NULL COMMENT '供应商ID',186 `supplier_name` VARCHAR(100) DEFAULT NULL COMMENT '供应商名称',187 `expect_time` DATETIME DEFAULT NULL COMMENT '预计到货时间',188 `actual_time` DATETIME DEFAULT NULL COMMENT '实际到货时间',189 `total_quantity` DECIMAL(10,2) DEFAULT 0 COMMENT '总数量',190 `actual_quantity` DECIMAL(10,2) DEFAULT 0 COMMENT '实收数量',191 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-待审核,2-待入库,3-入库中,4-已完成,5-已取消',192 `audit_user` VARCHAR(50) DEFAULT NULL COMMENT '审核人',193 `audit_time` DATETIME DEFAULT NULL COMMENT '审核时间',194 `operator` VARCHAR(50) DEFAULT NULL COMMENT '操作人',195 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',196 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',197 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',198 PRIMARY KEY (`id`),199 UNIQUE KEY `uk_inbound_no` (`inbound_no`),200 KEY `idx_warehouse` (`warehouse_id`),201 KEY `idx_status` (`status`),202 KEY `idx_create_time` (`create_time`)203) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入库单表';204205-- 入库单明细表206CREATE TABLE `inbound_detail` (207 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',208 `inbound_id` BIGINT(20) NOT NULL COMMENT '入库单ID',209 `inbound_no` VARCHAR(50) NOT NULL COMMENT '入库单号',210 `goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',211 `sku_code` VARCHAR(50) NOT NULL COMMENT 'SKU编码',212 `goods_name` VARCHAR(200) NOT NULL COMMENT '商品名称',213 `batch_no` VARCHAR(50) DEFAULT NULL COMMENT '批次号',214 `production_date` DATE DEFAULT NULL COMMENT '生产日期',215 `expire_date` DATE DEFAULT NULL COMMENT '过期日期',216 `plan_quantity` DECIMAL(10,2) NOT NULL COMMENT '计划数量',217 `actual_quantity` DECIMAL(10,2) DEFAULT 0 COMMENT '实收数量',218 `location_id` BIGINT(20) DEFAULT NULL COMMENT '上架库位ID',219 `location_code` VARCHAR(50) DEFAULT NULL COMMENT '上架库位编码',220 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-待收货,2-已收货,3-已上架',221 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',222 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',223 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',224 PRIMARY KEY (`id`),225 KEY `idx_inbound` (`inbound_id`),226 KEY `idx_goods` (`goods_id`)227) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入库单明细表';228229-- ================================230-- 5. 出库管理表231-- ================================232233-- 出库单表234CREATE TABLE `outbound_order` (235 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',236 `outbound_no` VARCHAR(50) NOT NULL COMMENT '出库单号',237 `warehouse_id` BIGINT(20) NOT NULL COMMENT '仓库ID',238 `outbound_type` VARCHAR(20) NOT NULL COMMENT '出库类型:SALE-销售,TRANSFER-调拨,SCRAP-报损,RETURN-退货,LOSS-盘亏,OTHER-其他',239 `source_no` VARCHAR(50) DEFAULT NULL COMMENT '来源单号(订单号/调拨单号)',240 `customer_code` VARCHAR(50) DEFAULT NULL COMMENT '客户编码',241 `customer_name` VARCHAR(100) DEFAULT NULL COMMENT '客户名称',242 `delivery_address` VARCHAR(200) DEFAULT NULL COMMENT '收货地址',243 `contact_person` VARCHAR(50) DEFAULT NULL COMMENT '联系人',244 `contact_phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话',245 `expect_time` DATETIME DEFAULT NULL COMMENT '期望发货时间',246 `actual_time` DATETIME DEFAULT NULL COMMENT '实际发货时间',247 `total_quantity` DECIMAL(10,2) DEFAULT 0 COMMENT '总数量',248 `actual_quantity` DECIMAL(10,2) DEFAULT 0 COMMENT '实发数量',249 `priority` TINYINT(4) DEFAULT 1 COMMENT '优先级:1-普通,2-紧急,3-特急',250 `carrier` VARCHAR(50) DEFAULT NULL COMMENT '承运商',251 `tracking_no` VARCHAR(50) DEFAULT NULL COMMENT '物流单号',252 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-待审核,2-待出库,3-拣货中,4-待复核,5-待发货,6-已发货,7-已取消',253 `audit_user` VARCHAR(50) DEFAULT NULL COMMENT '审核人',254 `audit_time` DATETIME DEFAULT NULL COMMENT '审核时间',255 `audit_remark` VARCHAR(500) DEFAULT NULL COMMENT '审核备注',256 `picker_id` BIGINT(20) DEFAULT NULL COMMENT '拣货员ID',257 `picker_name` VARCHAR(50) DEFAULT NULL COMMENT '拣货员姓名',258 `picking_time` DATETIME DEFAULT NULL COMMENT '拣货完成时间',259 `reviewer_id` BIGINT(20) DEFAULT NULL COMMENT '复核员ID',260 `reviewer_name` VARCHAR(50) DEFAULT NULL COMMENT '复核员姓名',261 `review_time` DATETIME DEFAULT NULL COMMENT '复核时间',262 `operator` VARCHAR(50) DEFAULT NULL COMMENT '操作人',263 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',264 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',265 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',266 PRIMARY KEY (`id`),267 UNIQUE KEY `uk_outbound_no` (`outbound_no`),268 KEY `idx_warehouse` (`warehouse_id`),269 KEY `idx_customer` (`customer_code`),270 KEY `idx_status` (`status`),271 KEY `idx_priority` (`priority`),272 KEY `idx_create_time` (`create_time`)273) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库单表';274275-- 出库单明细表276CREATE TABLE `outbound_detail` (277 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',278 `outbound_id` BIGINT(20) NOT NULL COMMENT '出库单ID',279 `outbound_no` VARCHAR(50) NOT NULL COMMENT '出库单号',280 `goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',281 `sku_code` VARCHAR(50) NOT NULL COMMENT 'SKU编码',282 `goods_name` VARCHAR(200) NOT NULL COMMENT '商品名称',283 `batch_no` VARCHAR(50) DEFAULT NULL COMMENT '批次号',284 `plan_quantity` DECIMAL(10,2) NOT NULL COMMENT '计划数量',285 `actual_quantity` DECIMAL(10,2) DEFAULT 0 COMMENT '实发数量',286 `picked_quantity` DECIMAL(10,2) DEFAULT 0 COMMENT '已拣数量',287 `location_id` BIGINT(20) DEFAULT NULL COMMENT '拣货库位ID',288 `location_code` VARCHAR(50) DEFAULT NULL COMMENT '拣货库位编码',289 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-待拣货,2-拣货中,3-已拣货,4-已复核,5-已发货',290 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',291 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',292 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',293 PRIMARY KEY (`id`),294 KEY `idx_outbound` (`outbound_id`),295 KEY `idx_goods` (`goods_id`),296 KEY `idx_location` (`location_id`)297) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库单明细表';298299-- ================================300-- 6. 拣货管理表301-- ================================302303-- 拣货波次表304CREATE TABLE `picking_wave` (305 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',306 `wave_no` VARCHAR(50) NOT NULL COMMENT '波次号',307 `warehouse_id` BIGINT(20) NOT NULL COMMENT '仓库ID',308 `wave_type` VARCHAR(20) DEFAULT 'BATCH' COMMENT '波次类型:BATCH-批次拣货,ZONE-分区拣货,SINGLE-单品拣货',309 `order_count` INT(11) DEFAULT 0 COMMENT '订单数量',310 `item_count` INT(11) DEFAULT 0 COMMENT '商品种类数',311 `total_quantity` DECIMAL(10,2) DEFAULT 0 COMMENT '总数量',312 `priority` TINYINT(4) DEFAULT 1 COMMENT '优先级:1-普通,2-紧急,3-特急',313 `picker_id` BIGINT(20) DEFAULT NULL COMMENT '拣货员ID',314 `picker_name` VARCHAR(50) DEFAULT NULL COMMENT '拣货员姓名',315 `estimated_time` INT(11) DEFAULT NULL COMMENT '预计耗时(秒)',316 `actual_time` INT(11) DEFAULT NULL COMMENT '实际耗时(秒)',317 `start_time` DATETIME DEFAULT NULL COMMENT '开始时间',318 `end_time` DATETIME DEFAULT NULL COMMENT '结束时间',319 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-待分配,2-已分配,3-拣货中,4-已完成,5-已取消',320 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',321 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',322 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',323 PRIMARY KEY (`id`),324 UNIQUE KEY `uk_wave_no` (`wave_no`),325 KEY `idx_warehouse` (`warehouse_id`),326 KEY `idx_status` (`status`),327 KEY `idx_picker` (`picker_id`),328 KEY `idx_create_time` (`create_time`)329) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拣货波次表';330331-- 拣货波次订单关联表332CREATE TABLE `picking_wave_order` (333 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',334 `wave_id` BIGINT(20) NOT NULL COMMENT '波次ID',335 `wave_no` VARCHAR(50) NOT NULL COMMENT '波次号',336 `outbound_id` BIGINT(20) NOT NULL COMMENT '出库单ID',337 `outbound_no` VARCHAR(50) NOT NULL COMMENT '出库单号',338 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',339 PRIMARY KEY (`id`),340 UNIQUE KEY `uk_wave_outbound` (`wave_id`, `outbound_id`),341 KEY `idx_wave` (`wave_id`),342 KEY `idx_outbound` (`outbound_id`)343) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拣货波次订单关联表';344345-- 拣货任务表346CREATE TABLE `picking_task` (347 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',348 `task_no` VARCHAR(50) NOT NULL COMMENT '任务编号',349 `wave_id` BIGINT(20) DEFAULT NULL COMMENT '波次ID',350 `wave_no` VARCHAR(50) DEFAULT NULL COMMENT '波次号',351 `outbound_id` BIGINT(20) NOT NULL COMMENT '出库单ID',352 `outbound_no` VARCHAR(50) NOT NULL COMMENT '出库单号',353 `warehouse_id` BIGINT(20) NOT NULL COMMENT '仓库ID',354 `goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',355 `sku_code` VARCHAR(50) NOT NULL COMMENT 'SKU编码',356 `goods_name` VARCHAR(200) NOT NULL COMMENT '商品名称',357 `batch_no` VARCHAR(50) DEFAULT NULL COMMENT '批次号',358 `location_id` BIGINT(20) NOT NULL COMMENT '库位ID',359 `location_code` VARCHAR(50) NOT NULL COMMENT '库位编码',360 `plan_quantity` DECIMAL(10,2) NOT NULL COMMENT '计划拣货数量',361 `actual_quantity` DECIMAL(10,2) DEFAULT 0 COMMENT '实际拣货数量',362 `sort_order` INT(11) DEFAULT 0 COMMENT '拣货顺序',363 `picker_id` BIGINT(20) DEFAULT NULL COMMENT '拣货员ID',364 `picker_name` VARCHAR(50) DEFAULT NULL COMMENT '拣货员姓名',365 `picking_time` DATETIME DEFAULT NULL COMMENT '拣货时间',366 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-待拣货,2-拣货中,3-已完成,4-异常',367 `exception_type` VARCHAR(20) DEFAULT NULL COMMENT '异常类型:EMPTY-库位空,SHORT-库存不足,DAMAGED-商品损坏',368 `exception_remark` VARCHAR(500) DEFAULT NULL COMMENT '异常说明',369 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',370 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',371 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',372 PRIMARY KEY (`id`),373 UNIQUE KEY `uk_task_no` (`task_no`),374 KEY `idx_wave` (`wave_id`),375 KEY `idx_outbound` (`outbound_id`),376 KEY `idx_picker` (`picker_id`),377 KEY `idx_location` (`location_id`),378 KEY `idx_status` (`status`)379) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拣货任务表';380381-- ================================382-- 7. 盘点管理表383-- ================================384385-- 盘点计划表386CREATE TABLE `stock_taking_plan` (387 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',388 `plan_no` VARCHAR(50) NOT NULL COMMENT '盘点计划编号',389 `plan_name` VARCHAR(100) NOT NULL COMMENT '盘点计划名称',390 `warehouse_id` BIGINT(20) NOT NULL COMMENT '仓库ID',391 `taking_type` VARCHAR(20) NOT NULL COMMENT '盘点类型:FULL-全盘,CYCLE-循环盘,SPOT-抽盘,DYNAMIC-动态盘',392 `scope_type` VARCHAR(20) NOT NULL COMMENT '盘点范围:WAREHOUSE-全仓,AREA-库区,LOCATION-库位,GOODS-商品',393 `scope_value` VARCHAR(500) DEFAULT NULL COMMENT '范围值(JSON数组)',394 `plan_start_time` DATETIME NOT NULL COMMENT '计划开始时间',395 `plan_end_time` DATETIME NOT NULL COMMENT '计划结束时间',396 `actual_start_time` DATETIME DEFAULT NULL COMMENT '实际开始时间',397 `actual_end_time` DATETIME DEFAULT NULL COMMENT '实际结束时间',398 `total_count` INT(11) DEFAULT 0 COMMENT '盘点总数',399 `completed_count` INT(11) DEFAULT 0 COMMENT '已完成数',400 `diff_count` INT(11) DEFAULT 0 COMMENT '差异数',401 `creator` VARCHAR(50) DEFAULT NULL COMMENT '创建人',402 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-待审核,2-待执行,3-执行中,4-已完成,5-已取消',403 `audit_user` VARCHAR(50) DEFAULT NULL COMMENT '审核人',404 `audit_time` DATETIME DEFAULT NULL COMMENT '审核时间',405 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',406 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',407 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',408 PRIMARY KEY (`id`),409 UNIQUE KEY `uk_plan_no` (`plan_no`),410 KEY `idx_warehouse` (`warehouse_id`),411 KEY `idx_status` (`status`),412 KEY `idx_plan_time` (`plan_start_time`, `plan_end_time`)413) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='盘点计划表';414415-- 盘点单表416CREATE TABLE `stock_taking` (417 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',418 `taking_no` VARCHAR(50) NOT NULL COMMENT '盘点单号',419 `plan_id` BIGINT(20) DEFAULT NULL COMMENT '盘点计划ID',420 `plan_no` VARCHAR(50) DEFAULT NULL COMMENT '盘点计划编号',421 `warehouse_id` BIGINT(20) NOT NULL COMMENT '仓库ID',422 `location_id` BIGINT(20) DEFAULT NULL COMMENT '库位ID',423 `location_code` VARCHAR(50) DEFAULT NULL COMMENT '库位编码',424 `goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',425 `sku_code` VARCHAR(50) NOT NULL COMMENT 'SKU编码',426 `goods_name` VARCHAR(200) NOT NULL COMMENT '商品名称',427 `batch_no` VARCHAR(50) DEFAULT NULL COMMENT '批次号',428 `book_quantity` DECIMAL(10,2) DEFAULT 0 COMMENT '账面数量',429 `actual_quantity` DECIMAL(10,2) DEFAULT NULL COMMENT '实盘数量',430 `diff_quantity` DECIMAL(10,2) GENERATED ALWAYS AS (`actual_quantity` - `book_quantity`) STORED COMMENT '差异数量',431 `diff_reason` VARCHAR(200) DEFAULT NULL COMMENT '差异原因',432 `operator` VARCHAR(50) DEFAULT NULL COMMENT '盘点人',433 `operate_time` DATETIME DEFAULT NULL COMMENT '盘点时间',434 `reviewer` VARCHAR(50) DEFAULT NULL COMMENT '复核人',435 `review_time` DATETIME DEFAULT NULL COMMENT '复核时间',436 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-待盘点,2-已盘点,3-已复核,4-已调整',437 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',438 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',439 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',440 PRIMARY KEY (`id`),441 UNIQUE KEY `uk_taking_no` (`taking_no`),442 KEY `idx_plan` (`plan_id`),443 KEY `idx_warehouse_goods` (`warehouse_id`, `goods_id`),444 KEY `idx_location` (`location_id`),445 KEY `idx_status` (`status`)446) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='盘点单表';447448-- ================================449-- 8. 库位调整表450-- ================================451452-- 移库单表453CREATE TABLE `stock_move` (454 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',455 `move_no` VARCHAR(50) NOT NULL COMMENT '移库单号',456 `warehouse_id` BIGINT(20) NOT NULL COMMENT '仓库ID',457 `move_type` VARCHAR(20) NOT NULL COMMENT '移库类型:LOCATION-库位调整,AREA-库区调整,WAREHOUSE-仓库调拨',458 `goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',459 `sku_code` VARCHAR(50) NOT NULL COMMENT 'SKU编码',460 `goods_name` VARCHAR(200) NOT NULL COMMENT '商品名称',461 `batch_no` VARCHAR(50) DEFAULT NULL COMMENT '批次号',462 `from_location_id` BIGINT(20) NOT NULL COMMENT '源库位ID',463 `from_location_code` VARCHAR(50) NOT NULL COMMENT '源库位编码',464 `to_location_id` BIGINT(20) NOT NULL COMMENT '目标库位ID',465 `to_location_code` VARCHAR(50) NOT NULL COMMENT '目标库位编码',466 `quantity` DECIMAL(10,2) NOT NULL COMMENT '移库数量',467 `move_reason` VARCHAR(200) DEFAULT NULL COMMENT '移库原因',468 `operator` VARCHAR(50) DEFAULT NULL COMMENT '操作人',469 `operate_time` DATETIME DEFAULT NULL COMMENT '操作时间',470 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-待执行,2-执行中,3-已完成,4-已取消',471 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',472 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',473 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',474 PRIMARY KEY (`id`),475 UNIQUE KEY `uk_move_no` (`move_no`),476 KEY `idx_warehouse` (`warehouse_id`),477 KEY `idx_goods` (`goods_id`),478 KEY `idx_from_location` (`from_location_id`),479 KEY `idx_to_location` (`to_location_id`),480 KEY `idx_status` (`status`)481) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='移库单表';482483-- ================================484-- 9. 供应商/客户管理表485-- ================================486487-- 供应商表488CREATE TABLE `supplier` (489 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',490 `supplier_code` VARCHAR(50) NOT NULL COMMENT '供应商编码',491 `supplier_name` VARCHAR(100) NOT NULL COMMENT '供应商名称',492 `supplier_type` VARCHAR(20) DEFAULT 'NORMAL' COMMENT '供应商类型:NORMAL-普通,VIP-VIP,STRATEGIC-战略',493 `contact_person` VARCHAR(50) DEFAULT NULL COMMENT '联系人',494 `contact_phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话',495 `contact_email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',496 `province` VARCHAR(50) DEFAULT NULL COMMENT '省份',497 `city` VARCHAR(50) DEFAULT NULL COMMENT '城市',498 `district` VARCHAR(50) DEFAULT NULL COMMENT '区县',499 `address` VARCHAR(200) DEFAULT NULL COMMENT '详细地址',500 `credit_level` VARCHAR(20) DEFAULT 'B' COMMENT '信用等级:A-优秀,B-良好,C-一般,D-较差',501 `cooperation_start_date` DATE DEFAULT NULL COMMENT '合作开始日期',502 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-启用,0-禁用',503 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',504 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',505 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',506 PRIMARY KEY (`id`),507 UNIQUE KEY `uk_supplier_code` (`supplier_code`),508 KEY `idx_supplier_type` (`supplier_type`),509 KEY `idx_status` (`status`)510) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商表';511512-- 客户表513CREATE TABLE `customer` (514 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',515 `customer_code` VARCHAR(50) NOT NULL COMMENT '客户编码',516 `customer_name` VARCHAR(100) NOT NULL COMMENT '客户名称',517 `customer_type` VARCHAR(20) DEFAULT 'RETAIL' COMMENT '客户类型:RETAIL-零售,WHOLESALE-批发,ENTERPRISE-企业',518 `customer_level` VARCHAR(20) DEFAULT 'NORMAL' COMMENT '客户等级:VIP,GOLD,SILVER,NORMAL',519 `contact_person` VARCHAR(50) DEFAULT NULL COMMENT '联系人',520 `contact_phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话',521 `contact_email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',522 `delivery_province` VARCHAR(50) DEFAULT NULL COMMENT '收货省份',523 `delivery_city` VARCHAR(50) DEFAULT NULL COMMENT '收货城市',524 `delivery_district` VARCHAR(50) DEFAULT NULL COMMENT '收货区县',525 `delivery_address` VARCHAR(200) DEFAULT NULL COMMENT '收货地址',526 `credit_limit` DECIMAL(15,2) DEFAULT 0 COMMENT '信用额度',527 `total_orders` INT(11) DEFAULT 0 COMMENT '累计订单数',528 `total_amount` DECIMAL(15,2) DEFAULT 0 COMMENT '累计金额',529 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-启用,0-禁用',530 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',531 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',532 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',533 PRIMARY KEY (`id`),534 UNIQUE KEY `uk_customer_code` (`customer_code`),535 KEY `idx_customer_type` (`customer_type`),536 KEY `idx_customer_level` (`customer_level`),537 KEY `idx_status` (`status`)538) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户表';539540-- 承运商表541CREATE TABLE `carrier` (542 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',543 `carrier_code` VARCHAR(50) NOT NULL COMMENT '承运商编码',544 `carrier_name` VARCHAR(100) NOT NULL COMMENT '承运商名称',545 `carrier_type` VARCHAR(20) DEFAULT 'EXPRESS' COMMENT '承运商类型:EXPRESS-快递,LOGISTICS-物流,SPECIAL-专线',546 `contact_person` VARCHAR(50) DEFAULT NULL COMMENT '联系人',547 `contact_phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话',548 `contact_email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',549 `service_area` VARCHAR(500) DEFAULT NULL COMMENT '服务区域',550 `price_standard` VARCHAR(500) DEFAULT NULL COMMENT '收费标准',551 `time_limit` VARCHAR(100) DEFAULT NULL COMMENT '时效要求',552 `rating` DECIMAL(3,2) DEFAULT 5.00 COMMENT '服务评分(1-5分)',553 `cooperation_start_date` DATE DEFAULT NULL COMMENT '合作开始日期',554 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-启用,0-禁用',555 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',556 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',557 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',558 PRIMARY KEY (`id`),559 UNIQUE KEY `uk_carrier_code` (`carrier_code`),560 KEY `idx_carrier_type` (`carrier_type`),561 KEY `idx_status` (`status`)562) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='承运商表';563564-- ================================565-- 10. 系统配置表566-- ================================567568-- 数据字典表569CREATE TABLE `sys_dict` (570 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',571 `dict_code` VARCHAR(50) NOT NULL COMMENT '字典编码',572 `dict_name` VARCHAR(100) NOT NULL COMMENT '字典名称',573 `parent_code` VARCHAR(50) DEFAULT NULL COMMENT '父字典编码',574 `dict_value` VARCHAR(100) DEFAULT NULL COMMENT '字典值',575 `dict_label` VARCHAR(100) DEFAULT NULL COMMENT '字典标签',576 `sort_order` INT(11) DEFAULT 0 COMMENT '排序',577 `css_class` VARCHAR(50) DEFAULT NULL COMMENT '样式类',578 `list_class` VARCHAR(50) DEFAULT NULL COMMENT '列表类',579 `is_default` TINYINT(4) DEFAULT 0 COMMENT '是否默认:1-是,0-否',580 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-启用,0-禁用',581 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',582 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',583 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',584 PRIMARY KEY (`id`),585 UNIQUE KEY `uk_dict` (`dict_code`, `dict_value`),586 KEY `idx_parent` (`parent_code`)587) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据字典表';588589-- 用户表590CREATE TABLE `sys_user` (591 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',592 `username` VARCHAR(50) NOT NULL COMMENT '用户名',593 `password` VARCHAR(100) NOT NULL COMMENT '密码',594 `real_name` VARCHAR(50) NOT NULL COMMENT '真实姓名',595 `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',596 `user_type` VARCHAR(20) DEFAULT 'EMPLOYEE' COMMENT '用户类型:ADMIN-管理员,EMPLOYEE-员工,PICKER-拣货员',597 `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',598 `email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',599 `avatar` VARCHAR(200) DEFAULT NULL COMMENT '头像',600 `dept_id` BIGINT(20) DEFAULT NULL COMMENT '部门ID',601 `dept_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称',602 `warehouse_id` BIGINT(20) DEFAULT NULL COMMENT '所属仓库ID',603 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-正常,0-禁用',604 `login_date` DATETIME DEFAULT NULL COMMENT '最后登录时间',605 `login_ip` VARCHAR(50) DEFAULT NULL COMMENT '最后登录IP',606 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',607 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',608 PRIMARY KEY (`id`),609 UNIQUE KEY `uk_username` (`username`),610 KEY `idx_phone` (`phone`),611 KEY `idx_dept` (`dept_id`),612 KEY `idx_warehouse` (`warehouse_id`)613) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';614615-- 角色表616CREATE TABLE `sys_role` (617 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',618 `role_code` VARCHAR(50) NOT NULL COMMENT '角色编码',619 `role_name` VARCHAR(100) NOT NULL COMMENT '角色名称',620 `role_type` VARCHAR(20) DEFAULT 'CUSTOM' COMMENT '角色类型:SYSTEM-系统,CUSTOM-自定义',621 `data_scope` TINYINT(4) DEFAULT 1 COMMENT '数据范围:1-全部,2-自定义,3-本部门,4-本人',622 `sort_order` INT(11) DEFAULT 0 COMMENT '排序',623 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-正常,0-禁用',624 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',625 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',626 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',627 PRIMARY KEY (`id`),628 UNIQUE KEY `uk_role_code` (`role_code`)629) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';630631-- 菜单表632CREATE TABLE `sys_menu` (633 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',634 `menu_code` VARCHAR(50) NOT NULL COMMENT '菜单编码',635 `menu_name` VARCHAR(100) NOT NULL COMMENT '菜单名称',636 `parent_id` BIGINT(20) DEFAULT 0 COMMENT '父菜单ID',637 `menu_type` VARCHAR(20) DEFAULT 'MENU' COMMENT '菜单类型:CATALOG-目录,MENU-菜单,BUTTON-按钮',638 `path` VARCHAR(200) DEFAULT NULL COMMENT '路由地址',639 `component` VARCHAR(200) DEFAULT NULL COMMENT '组件路径',640 `permission` VARCHAR(100) DEFAULT NULL COMMENT '权限标识',641 `icon` VARCHAR(100) DEFAULT NULL COMMENT '图标',642 `sort_order` INT(11) DEFAULT 0 COMMENT '排序',643 `visible` TINYINT(4) DEFAULT 1 COMMENT '是否显示:1-显示,0-隐藏',644 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-正常,0-禁用',645 `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',646 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',647 `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',648 PRIMARY KEY (`id`),649 UNIQUE KEY `uk_menu_code` (`menu_code`),650 KEY `idx_parent` (`parent_id`)651) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';652653-- 用户角色关联表654CREATE TABLE `sys_user_role` (655 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',656 `user_id` BIGINT(20) NOT NULL COMMENT '用户ID',657 `role_id` BIGINT(20) NOT NULL COMMENT '角色ID',658 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',659 PRIMARY KEY (`id`),660 UNIQUE KEY `uk_user_role` (`user_id`, `role_id`),661 KEY `idx_user` (`user_id`),662 KEY `idx_role` (`role_id`)663) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';664665-- 角色菜单关联表666CREATE TABLE `sys_role_menu` (667 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',668 `role_id` BIGINT(20) NOT NULL COMMENT '角色ID',669 `menu_id` BIGINT(20) NOT NULL COMMENT '菜单ID',670 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',671 PRIMARY KEY (`id`),672 UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`),673 KEY `idx_role` (`role_id`),674 KEY `idx_menu` (`menu_id`)675) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表';676677-- 操作日志表678CREATE TABLE `sys_operation_log` (679 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',680 `module` VARCHAR(50) DEFAULT NULL COMMENT '操作模块',681 `business_type` VARCHAR(50) DEFAULT NULL COMMENT '业务类型',682 `business_id` BIGINT(20) DEFAULT NULL COMMENT '业务ID',683 `method` VARCHAR(200) DEFAULT NULL COMMENT '请求方法',684 `request_method` VARCHAR(10) DEFAULT NULL COMMENT '请求方式',685 `operator_type` VARCHAR(20) DEFAULT 'WEB' COMMENT '操作类型:WEB-后台,MOBILE-手机,API-接口',686 `operator` VARCHAR(50) DEFAULT NULL COMMENT '操作人',687 `dept_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称',688 `url` VARCHAR(500) DEFAULT NULL COMMENT '请求URL',689 `ip` VARCHAR(50) DEFAULT NULL COMMENT '操作IP',690 `location` VARCHAR(200) DEFAULT NULL COMMENT '操作地点',691 `param` TEXT DEFAULT NULL COMMENT '请求参数',692 `result` TEXT DEFAULT NULL COMMENT '返回结果',693 `status` TINYINT(4) DEFAULT 1 COMMENT '状态:1-成功,0-失败',694 `error_msg` TEXT DEFAULT NULL COMMENT '错误消息',695 `cost_time` INT(11) DEFAULT NULL COMMENT '消耗时间(毫秒)',696 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',697 PRIMARY KEY (`id`),698 KEY `idx_module` (`module`),699 KEY `idx_business` (`business_type`, `business_id`),700 KEY `idx_operator` (`operator`),701 KEY `idx_create_time` (`create_time`)702) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';703704---705706## 四、核心业务实现707708### 4.1 库存服务实现709710#### 实体类定义711712```java713package com.wms.inventory.entity;714715import com.baomidou.mybatisplus.annotation.*;716import lombok.Data;717import java.math.BigDecimal;718import java.time.LocalDate;719import java.time.LocalDateTime;720721/**722 * 库存实体723 */724@Data725@TableName("inventory")726public class Inventory {727 728 @TableId(type = IdType.AUTO)729 private Long id;730 731 /**732 * 仓库ID733 */734 private Long warehouseId;735 736 /**737 * 库位ID738 */739 private Long locationId;740 741 /**742 * 商品ID743 */744 private Long goodsId;745 746 /**747 * 批次号748 */749 private String batchNo;750 751 /**752 * 序列号753 */754 private String serialNo;755 756 /**757 * 库存数量758 */759 private BigDecimal quantity;760 761 /**762 * 锁定数量763 */764 private BigDecimal lockQuantity;765 766 /**767 * 可用数量(虚拟列,自动计算)768 */769 private BigDecimal availableQuantity;770 771 /**772 * 生产日期773 */774 private LocalDate productionDate;775 776 /**777 * 过期日期778 */779 private LocalDate expireDate;780 781 /**782 * 入库日期783 */784 private LocalDateTime inboundDate;785 786 /**787 * 供应商ID788 */789 private Long supplierId;790 791 /**792 * 供应商名称793 */794 private String supplierName;795 796 /**797 * 状态:1-正常,2-冻结,3-待检,4-损坏798 */799 private Integer status;800 801 @TableField(fill = FieldFill.INSERT)802 private LocalDateTime createTime;803 804 @TableField(fill = FieldFill.INSERT_UPDATE)805 private LocalDateTime updateTime;806}库存服务接口
1package com.wms.inventory.service;23import com.wms.inventory.dto.InventoryLockDTO;4import com.wms.inventory.dto.InventoryQueryDTO;5import com.wms.inventory.vo.InventoryVO;6import java.math.BigDecimal;7import java.util.List;89/**10 * 库存服务接口11 */12public interface InventoryService {13 14 /**15 * 查询库存16 */17 List<InventoryVO> queryInventory(InventoryQueryDTO queryDTO);18 19 /**20 * 锁定库存21 */22 boolean lockInventory(InventoryLockDTO lockDTO);23 24 /**25 * 解锁库存26 */27 boolean unlockInventory(InventoryLockDTO lockDTO);28 29 /**30 * 扣减库存31 */32 boolean deductInventory(Long warehouseId, Long goodsId, String batchNo, BigDecimal quantity);33 34 /**35 * 增加库存36 */37 boolean addInventory(Long warehouseId, Long locationId, Long goodsId, String batchNo, BigDecimal quantity);38 39 /**40 * 获取可用库存数量41 */42 BigDecimal getAvailableQuantity(Long warehouseId, Long goodsId, String batchNo);43 44 /**45 * 库存预警检查46 */47 List<InventoryVO> checkInventoryAlert();48}库存服务实现(核心逻辑)
1package com.wms.inventory.service.impl;23import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;4import com.wms.inventory.entity.Inventory;5import com.wms.inventory.entity.InventoryLog;6import com.wms.inventory.mapper.InventoryMapper;7import com.wms.inventory.mapper.InventoryLogMapper;8import com.wms.inventory.service.InventoryService;9import lombok.RequiredArgsConstructor;10import lombok.extern.slf4j.Slf4j;11import org.springframework.data.redis.core.RedisTemplate;12import org.springframework.stereotype.Service;13import org.springframework.transaction.annotation.Transactional;1415import java.math.BigDecimal;16import java.time.LocalDateTime;17import java.util.concurrent.TimeUnit;1819/**20 * 库存服务实现21 */22@Slf4j23@Service24@RequiredArgsConstructor25public class InventoryServiceImpl implements InventoryService {26 27 private final InventoryMapper inventoryMapper;28 private final InventoryLogMapper inventoryLogMapper;29 private final RedisTemplate<String, Object> redisTemplate;30 31 private static final String LOCK_KEY_PREFIX = "inventory:lock:";32 private static final String CACHE_KEY_PREFIX = "inventory:cache:";33 34 /**35 * 锁定库存(支持分布式锁)36 */37 @Override38 @Transactional(rollbackFor = Exception.class)39 public boolean lockInventory(InventoryLockDTO lockDTO) {40 String lockKey = LOCK_KEY_PREFIX + lockDTO.getWarehouseId() + ":" + lockDTO.getGoodsId();41 42 try {43 // 获取分布式锁44 Boolean acquired = redisTemplate.opsForValue()45 .setIfAbsent(lockKey, "LOCKED", 10, TimeUnit.SECONDS);46 47 if (Boolean.FALSE.equals(acquired)) {48 log.warn("获取库存锁失败: {}", lockKey);49 return false;50 }51 52 // 查询可用库存53 Inventory inventory = getInventoryForUpdate(54 lockDTO.getWarehouseId(), 55 lockDTO.getGoodsId(), 56 lockDTO.getBatchNo()57 );58 59 if (inventory == null) {60 log.error("库存不存在: warehouseId={}, goodsId={}", 61 lockDTO.getWarehouseId(), lockDTO.getGoodsId());62 return false;63 }64 65 // 检查可用库存是否充足66 BigDecimal available = inventory.getQuantity().subtract(inventory.getLockQuantity());67 if (available.compareTo(lockDTO.getQuantity()) < 0) {68 log.warn("库存不足: 可用={}, 需要={}", available, lockDTO.getQuantity());69 return false;70 }71 72 // 更新锁定数量73 inventory.setLockQuantity(inventory.getLockQuantity().add(lockDTO.getQuantity()));74 inventoryMapper.updateById(inventory);75 76 // 记录库存流水77 saveInventoryLog(inventory, "LOCK", lockDTO.getQuantity(), 78 lockDTO.getBusinessType(), lockDTO.getBusinessNo());79 80 // 清除缓存81 clearInventoryCache(lockDTO.getWarehouseId(), lockDTO.getGoodsId());82 83 log.info("库存锁定成功: {}", lockDTO);84 return true;85 86 } finally {87 // 释放分布式锁88 redisTemplate.delete(lockKey);89 }90 }91 92 /**93 * 扣减库存(出库时调用)94 */95 @Override96 @Transactional(rollbackFor = Exception.class)97 public boolean deductInventory(Long warehouseId, Long goodsId, String batchNo, BigDecimal quantity) {98 String lockKey = LOCK_KEY_PREFIX + warehouseId + ":" + goodsId;99 100 try {101 Boolean acquired = redisTemplate.opsForValue()102 .setIfAbsent(lockKey, "LOCKED", 10, TimeUnit.SECONDS);103 104 if (Boolean.FALSE.equals(acquired)) {105 return false;106 }107 108 Inventory inventory = getInventoryForUpdate(warehouseId, goodsId, batchNo);109 110 if (inventory == null) {111 log.error("扣减失败,库存不存在");112 return false;113 }114 115 // 扣减库存数量和锁定数量116 BigDecimal newQuantity = inventory.getQuantity().subtract(quantity);117 BigDecimal newLockQuantity = inventory.getLockQuantity().subtract(quantity);118 119 if (newQuantity.compareTo(BigDecimal.ZERO) < 0) {120 log.error("扣减失败,库存不足");121 return false;122 }123 124 inventory.setQuantity(newQuantity);125 inventory.setLockQuantity(newLockQuantity.max(BigDecimal.ZERO));126 inventoryMapper.updateById(inventory);127 128 // 记录流水129 saveInventoryLog(inventory, "OUTBOUND", quantity.negate(), "OUTBOUND", null);130 131 clearInventoryCache(warehouseId, goodsId);132 133 return true;134 135 } finally {136 redisTemplate.delete(lockKey);137 }138 }139 140 /**141 * 增加库存(入库时调用)142 */143 @Override144 @Transactional(rollbackFor = Exception.class)145 public boolean addInventory(Long warehouseId, Long locationId, Long goodsId, 146 String batchNo, BigDecimal quantity) {147 148 // 查询是否已有库存记录149 LambdaQueryWrapper<Inventory> wrapper = new LambdaQueryWrapper<>();150 wrapper.eq(Inventory::getWarehouseId, warehouseId)151 .eq(Inventory::getLocationId, locationId)152 .eq(Inventory::getGoodsId, goodsId)153 .eq(batchNo != null, Inventory::getBatchNo, batchNo);154 155 Inventory inventory = inventoryMapper.selectOne(wrapper);156 157 if (inventory != null) {158 // 已存在,累加数量159 inventory.setQuantity(inventory.getQuantity().add(quantity));160 inventoryMapper.updateById(inventory);161 } else {162 // 新增库存记录163 inventory = new Inventory();164 inventory.setWarehouseId(warehouseId);165 inventory.setLocationId(locationId);166 inventory.setGoodsId(goodsId);167 inventory.setBatchNo(batchNo);168 inventory.setQuantity(quantity);169 inventory.setLockQuantity(BigDecimal.ZERO);170 inventory.setStatus(1);171 inventory.setInboundDate(LocalDateTime.now());172 inventoryMapper.insert(inventory);173 }174 175 // 记录流水176 saveInventoryLog(inventory, "INBOUND", quantity, "INBOUND", null);177 178 clearInventoryCache(warehouseId, goodsId);179 180 return true;181 }182 183 /**184 * 查询库存(带行锁)185 */186 private Inventory getInventoryForUpdate(Long warehouseId, Long goodsId, String batchNo) {187 LambdaQueryWrapper<Inventory> wrapper = new LambdaQueryWrapper<>();188 wrapper.eq(Inventory::getWarehouseId, warehouseId)189 .eq(Inventory::getGoodsId, goodsId)190 .eq(batchNo != null, Inventory::getBatchNo, batchNo)191 .eq(Inventory::getStatus, 1)192 .last("FOR UPDATE");193 194 return inventoryMapper.selectOne(wrapper);195 }196 197 /**198 * 保存库存流水199 */200 private void saveInventoryLog(Inventory inventory, String operationType, 201 BigDecimal quantityChange, String businessType, String businessNo) {202 InventoryLog log = new InventoryLog();203 log.setWarehouseId(inventory.getWarehouseId());204 log.setGoodsId(inventory.getGoodsId());205 log.setLocationId(inventory.getLocationId());206 log.setBatchNo(inventory.getBatchNo());207 log.setOperationType(operationType);208 log.setQuantityBefore(inventory.getQuantity().subtract(quantityChange));209 log.setQuantityChange(quantityChange);210 log.setQuantityAfter(inventory.getQuantity());211 log.setBusinessType(businessType);212 log.setBusinessNo(businessNo);213 214 inventoryLogMapper.insert(log);215 }216 217 /**218 * 清除库存缓存219 */220 private void clearInventoryCache(Long warehouseId, Long goodsId) {221 String cacheKey = CACHE_KEY_PREFIX + warehouseId + ":" + goodsId;222 redisTemplate.delete(cacheKey);223 }224}4.2 出库服务实现
出库订单实体
1package com.wms.outbound.entity;23import com.baomidou.mybatisplus.annotation.*;4import lombok.Data;5import java.math.BigDecimal;6import java.time.LocalDateTime;78@Data9@TableName("outbound_order")10public class OutboundOrder {11 12 @TableId(type = IdType.AUTO)13 private Long id;14 15 private String outboundNo;16 private Long warehouseId;17 private String outboundType; // SALE-销售, TRANSFER-调拨, SCRAP-报损18 private String customerCode;19 private String customerName;20 private String deliveryAddress;21 private String contactPhone;22 private LocalDateTime expectTime;23 private LocalDateTime actualTime;24 private BigDecimal totalQuantity;25 private BigDecimal actualQuantity;26 private Integer priority; // 优先级: 1-普通, 2-紧急, 3-特急27 private Integer status; // 1-待审核, 2-待出库, 3-拣货中, 4-已完成, 5-已取消28 private String auditUser;29 private LocalDateTime auditTime;30 private String operator;31 private String remark;32 33 @TableField(fill = FieldFill.INSERT)34 private LocalDateTime createTime;35 36 @TableField(fill = FieldFill.INSERT_UPDATE)37 private LocalDateTime updateTime;38}出库服务实现
1package com.wms.outbound.service.impl;23import com.wms.outbound.entity.OutboundOrder;4import com.wms.outbound.entity.OutboundDetail;5import com.wms.outbound.mapper.OutboundOrderMapper;6import com.wms.inventory.service.InventoryService;7import lombok.RequiredArgsConstructor;8import lombok.extern.slf4j.Slf4j;9import org.springframework.stereotype.Service;10import org.springframework.transaction.annotation.Transactional;1112/**13 * 出库服务实现14 */15@Slf4j16@Service17@RequiredArgsConstructor18public class OutboundServiceImpl {19 20 private final OutboundOrderMapper outboundOrderMapper;21 private final InventoryService inventoryService;22 private final PickingService pickingService;23 24 /**25 * 创建出库单26 */27 @Transactional(rollbackFor = Exception.class)28 public Long createOutboundOrder(OutboundOrderDTO dto) {29 // 1. 创建出库单30 OutboundOrder order = new OutboundOrder();31 order.setOutboundNo(generateOutboundNo());32 order.setWarehouseId(dto.getWarehouseId());33 order.setOutboundType(dto.getOutboundType());34 order.setCustomerCode(dto.getCustomerCode());35 order.setCustomerName(dto.getCustomerName());36 order.setStatus(1); // 待审核37 outboundOrderMapper.insert(order);38 39 // 2. 创建出库明细40 dto.getDetails().forEach(detail -> {41 OutboundDetail outboundDetail = new OutboundDetail();42 outboundDetail.setOutboundId(order.getId());43 outboundDetail.setGoodsId(detail.getGoodsId());44 outboundDetail.setPlanQuantity(detail.getQuantity());45 // ... 保存明细46 });47 48 return order.getId();49 }50 51 /**52 * 审核出库单53 */54 @Transactional(rollbackFor = Exception.class)55 public boolean auditOutboundOrder(Long orderId, boolean approved) {56 OutboundOrder order = outboundOrderMapper.selectById(orderId);57 58 if (approved) {59 // 审核通过,锁定库存60 boolean locked = lockInventoryForOrder(order);61 if (!locked) {62 throw new BizException("库存不足,审核失败");63 }64 order.setStatus(2); // 待出库65 } else {66 order.setStatus(5); // 已取消67 }68 69 order.setAuditTime(LocalDateTime.now());70 outboundOrderMapper.updateById(order);71 72 return true;73 }74 75 /**76 * 创建拣货任务77 */78 @Transactional(rollbackFor = Exception.class)79 public boolean createPickingTask(Long orderId) {80 OutboundOrder order = outboundOrderMapper.selectById(orderId);81 82 if (order.getStatus() != 2) {83 throw new BizException("订单状态不正确");84 }85 86 // 创建拣货任务87 pickingService.createPickingTask(order);88 89 order.setStatus(3); // 拣货中90 outboundOrderMapper.updateById(order);91 92 return true;93 }94 95 /**96 * 出库完成97 */98 @Transactional(rollbackFor = Exception.class)99 public boolean completeOutbound(Long orderId) {100 OutboundOrder order = outboundOrderMapper.selectById(orderId);101 102 // 扣减库存103 boolean deducted = deductInventoryForOrder(order);104 if (!deducted) {105 throw new BizException("库存扣减失败");106 }107 108 order.setStatus(4); // 已完成109 order.setActualTime(LocalDateTime.now());110 outboundOrderMapper.updateById(order);111 112 return true;113 }114 115 /**116 * 为订单锁定库存117 */118 private boolean lockInventoryForOrder(OutboundOrder order) {119 // 查询出库明细,逐个锁定库存120 // ... 实现逻辑121 return true;122 }123 124 /**125 * 为订单扣减库存126 */127 private boolean deductInventoryForOrder(OutboundOrder order) {128 // 查询出库明细,逐个扣减库存129 // ... 实现逻辑130 return true;131 }132 133 /**134 * 生成出库单号135 */136 private String generateOutboundNo() {137 return "OUT" + System.currentTimeMillis();138 }139}4.3 拣货服务实现
拣货路径优化算法
1package com.wms.picking.service;23import lombok.Data;4import java.util.*;56/**7 * 拣货路径优化服务8 */9@Service10public class PickingPathOptimizer {11 12 /**13 * 使用贪心算法优化拣货路径14 */15 public List<PickingLocation> optimizePath(List<PickingLocation> locations) {16 if (locations.size() <= 1) {17 return locations;18 }19 20 List<PickingLocation> optimized = new ArrayList<>();21 Set<PickingLocation> unvisited = new HashSet<>(locations);22 23 // 从起点(库区入口)开始24 PickingLocation current = findNearestToEntrance(unvisited);25 optimized.add(current);26 unvisited.remove(current);27 28 // 贪心算法:每次选择距离当前位置最近的点29 while (!unvisited.isEmpty()) {30 PickingLocation nearest = findNearest(current, unvisited);31 optimized.add(nearest);32 unvisited.remove(nearest);33 current = nearest;34 }35 36 return optimized;37 }38 39 /**40 * 计算两个库位之间的距离41 */42 private double calculateDistance(PickingLocation loc1, PickingLocation loc2) {43 // 曼哈顿距离44 int rowDiff = Math.abs(loc1.getRowNo() - loc2.getRowNo());45 int colDiff = Math.abs(loc1.getColumnNo() - loc2.getColumnNo());46 int layerDiff = Math.abs(loc1.getLayerNo() - loc2.getLayerNo());47 48 return rowDiff + colDiff + layerDiff * 2; // 层间移动成本更高49 }50 51 /**52 * 找到距离当前位置最近的库位53 */54 private PickingLocation findNearest(PickingLocation current, Set<PickingLocation> candidates) {55 return candidates.stream()56 .min(Comparator.comparingDouble(loc -> calculateDistance(current, loc)))57 .orElseThrow();58 }59 60 /**61 * 找到距离入口最近的库位62 */63 private PickingLocation findNearestToEntrance(Set<PickingLocation> locations) {64 PickingLocation entrance = new PickingLocation(0, 0, 0);65 return findNearest(entrance, locations);66 }67}6869@Data70class PickingLocation {71 private Long locationId;72 private String locationCode;73 private Integer rowNo;74 private Integer columnNo;75 private Integer layerNo;76 77 public PickingLocation(int row, int col, int layer) {78 this.rowNo = row;79 this.columnNo = col;80 this.layerNo = layer;81 }82}五、前端实现
5.1 库存管理页面
1<template>2 <div class="inventory-container">3 <!-- 搜索区域 -->4 <el-card class="search-card">5 <el-form :inline="true" :model="searchForm">6 <el-form-item label="商品名称">7 <el-input v-model="searchForm.goodsName" placeholder="请输入商品名称" clearable />8 </el-form-item>9 <el-form-item label="SKU编码">10 <el-input v-model="searchForm.skuCode" placeholder="请输入SKU编码" clearable />11 </el-form-item>12 <el-form-item label="仓库">13 <el-select v-model="searchForm.warehouseId" placeholder="请选择仓库" clearable>14 <el-option15 v-for="item in warehouseList"16 :key="item.id"17 :label="item.warehouseName"18 :value="item.id"19 />20 </el-select>21 </el-form-item>22 <el-form-item label="库存状态">23 <el-select v-model="searchForm.status" placeholder="请选择状态" clearable>24 <el-option label="正常" :value="1" />25 <el-option label="冻结" :value="2" />26 <el-option label="待检" :value="3" />27 </el-select>28 </el-form-item>29 <el-form-item>30 <el-button type="primary" @click="handleSearch" icon="Search">查询</el-button>31 <el-button @click="handleReset" icon="Refresh">重置</el-button>32 <el-button type="success" @click="handleExport" icon="Download">导出</el-button>33 </el-form-item>34 </el-form>35 </el-card>3637 <!-- 库存预警提示 -->38 <el-alert39 v-if="alertCount > 0"40 title="库存预警"41 type="warning"42 :description="`当前有 ${alertCount} 个商品库存不足,请及时补货`"43 show-icon44 :closable="false"45 :style="{ margin: '20px 0' }"46 />4748 <!-- 数据表格 -->49 <el-card class="table-card">50 <vxe-table51 ref="tableRef"52 :data="tableData"53 :loading="loading"54 border55 stripe56 height="600"57 :row-config="{ isHover: true }"58 >59 <vxe-column type="seq" title="序号" width="60" />60 <vxe-column field="warehouseName" title="仓库" width="120" />61 <vxe-column field="locationCode" title="库位" width="100" />62 <vxe-column field="skuCode" title="SKU编码" width="150" />63 <vxe-column field="goodsName" title="商品名称" min-width="200" />64 <vxe-column field="batchNo" title="批次号" width="120" />65 <vxe-column field="quantity" title="总库存" width="100" align="right">66 <template #default="{ row }">67 <span :class="{ 'text-danger': row.quantity < row.safetyStock }">68 {{ row.quantity }}69 </span>70 </template>71 </vxe-column>72 <vxe-column field="lockQuantity" title="锁定数量" width="100" align="right" />73 <vxe-column field="availableQuantity" title="可用库存" width="100" align="right">74 <template #default="{ row }">75 <el-tag :type="getStockTagType(row)">76 {{ row.availableQuantity }}77 </el-tag>78 </template>79 </vxe-column>80 <vxe-column field="status" title="状态" width="80">81 <template #default="{ row }">82 <el-tag :type="getStatusType(row.status)">83 {{ getStatusText(row.status) }}84 </el-tag>85 </template>86 </vxe-column>87 <vxe-column field="inboundDate" title="入库日期" width="150" />88 <vxe-column title="操作" width="200" fixed="right">89 <template #default="{ row }">90 <el-button link type="primary" @click="handleView(row)">详情</el-button>91 <el-button link type="warning" @click="handleMove(row)">移库</el-button>92 <el-button link type="danger" @click="handleFreeze(row)">冻结</el-button>93 </template>94 </vxe-column>95 </vxe-table>9697 <!-- 分页 -->98 <el-pagination99 v-model:current-page="pagination.page"100 v-model:page-size="pagination.size"101 :total="pagination.total"102 :page-sizes="[10, 20, 50, 100]"103 layout="total, sizes, prev, pager, next, jumper"104 @size-change="handleSearch"105 @current-change="handleSearch"106 />107 </el-card>108 </div>109</template>110111<script setup>112import { ref, reactive, onMounted } from 'vue'113import { ElMessage, ElMessageBox } from 'element-plus'114import { getInventoryList, freezeInventory } from '@/api/inventory'115116// 搜索表单117const searchForm = reactive({118 goodsName: '',119 skuCode: '',120 warehouseId: null,121 status: null122})123124// 表格数据125const tableData = ref([])126const loading = ref(false)127const alertCount = ref(0)128129// 分页130const pagination = reactive({131 page: 1,132 size: 20,133 total: 0134})135136// 查询数据137const handleSearch = async () => {138 loading.value = true139 try {140 const params = {141 ...searchForm,142 page: pagination.page,143 size: pagination.size144 }145 const { data } = await getInventoryList(params)146 tableData.value = data.records147 pagination.total = data.total148 alertCount.value = data.alertCount || 0149 } catch (error) {150 ElMessage.error('查询失败')151 } finally {152 loading.value = false153 }154}155156// 重置157const handleReset = () => {158 Object.assign(searchForm, {159 goodsName: '',160 skuCode: '',161 warehouseId: null,162 status: null163 })164 handleSearch()165}166167// 库存标签类型168const getStockTagType = (row) => {169 if (row.availableQuantity <= 0) return 'danger'170 if (row.availableQuantity < row.safetyStock) return 'warning'171 return 'success'172}173174// 状态类型175const getStatusType = (status) => {176 const map = { 1: 'success', 2: 'danger', 3: 'warning', 4: 'info' }177 return map[status] || ''178}179180const getStatusText = (status) => {181 const map = { 1: '正常', 2: '冻结', 3: '待检', 4: '损坏' }182 return map[status] || ''183}184185// 冻结库存186const handleFreeze = async (row) => {187 try {188 await ElMessageBox.confirm('确定要冻结该库存吗?', '提示', {189 type: 'warning'190 })191 await freezeInventory(row.id)192 ElMessage.success('冻结成功')193 handleSearch()194 } catch (error) {195 if (error !== 'cancel') {196 ElMessage.error('操作失败')197 }198 }199}200201onMounted(() => {202 handleSearch()203})204</script>205206<style scoped>207.inventory-container {208 padding: 20px;209}210211.search-card {212 margin-bottom: 20px;213}214215.text-danger {216 color: #f56c6c;217 font-weight: bold;218}219</style>5.2 入库管理页面(流程图)
5.3 仓库大屏监控
1<template>2 <div class="warehouse-screen">3 <div class="screen-header">4 <h1>🏢 智能仓库监控大屏</h1>5 <div class="datetime">{{ currentTime }}</div>6 </div>78 <div class="screen-content">9 <!-- 统计卡片 -->10 <div class="stats-row">11 <div class="stat-card" v-for="stat in stats" :key="stat.label">12 <div class="stat-icon" :style="{ background: stat.color }">13 {{ stat.icon }}14 </div>15 <div class="stat-content">16 <div class="stat-value">{{ stat.value }}</div>17 <div class="stat-label">{{ stat.label }}</div>18 </div>19 </div>20 </div>2122 <!-- 图表区域 -->23 <div class="charts-row">24 <div class="chart-container">25 <h3>📊 出入库趋势</h3>26 <div ref="trendChart" class="chart"></div>27 </div>28 <div class="chart-container">29 <h3>🥧 库存分布</h3>30 <div ref="pieChart" class="chart"></div>31 </div>32 <div class="chart-container">33 <h3>📈 库位利用率</h3>34 <div ref="barChart" class="chart"></div>35 </div>36 </div>3738 <!-- 实时任务 -->39 <div class="tasks-row">40 <div class="task-list">41 <h3>🎯 拣货任务</h3>42 <el-scrollbar height="300px">43 <div v-for="task in pickingTasks" :key="task.id" class="task-item">44 <div class="task-info">45 <span class="task-no">{{ task.taskNo }}</span>46 <el-tag :type="getTaskTagType(task.status)">47 {{ task.statusText }}48 </el-tag>49 </div>50 <el-progress :percentage="task.progress" />51 </div>52 </el-scrollbar>53 </div>54 </div>55 </div>56 </div>57</template>5859<script setup>60import { ref, onMounted, onUnmounted } from 'vue'61import * as echarts from 'echarts'62import dayjs from 'dayjs'6364const currentTime = ref(dayjs().format('YYYY-MM-DD HH:mm:ss'))6566// 统计数据67const stats = ref([68 { label: '总库存', value: '12,345', icon: '📦', color: '#409EFF' },69 { label: '今日入库', value: '856', icon: '📥', color: '#67C23A' },70 { label: '今日出库', value: '1,032', icon: '📤', color: '#E6A23C' },71 { label: '预警商品', value: '23', icon: '⚠️', color: '#F56C6C' }72])7374// ECharts 图表初始化75const trendChart = ref(null)76const pieChart = ref(null)77const barChart = ref(null)7879const initCharts = () => {80 // 趋势图81 const trend = echarts.init(trendChart.value)82 trend.setOption({83 tooltip: { trigger: 'axis' },84 legend: { data: ['入库', '出库'] },85 xAxis: { type: 'category', data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] },86 yAxis: { type: 'value' },87 series: [88 { name: '入库', type: 'line', data: [820, 932, 901, 934, 1290, 1330, 1320], smooth: true },89 { name: '出库', type: 'line', data: [680, 732, 701, 734, 1090, 1130, 1120], smooth: true }90 ]91 })9293 // 饼图94 const pie = echarts.init(pieChart.value)95 pie.setOption({96 tooltip: { trigger: 'item' },97 series: [{98 type: 'pie',99 radius: '60%',100 data: [101 { value: 4500, name: '电子产品' },102 { value: 3200, name: '日用品' },103 { value: 2800, name: '食品' },104 { value: 1845, name: '其他' }105 ]106 }]107 })108109 // 柱状图110 const bar = echarts.init(barChart.value)111 bar.setOption({112 tooltip: { trigger: 'axis' },113 xAxis: { type: 'category', data: ['A区', 'B区', 'C区', 'D区', 'E区'] },114 yAxis: { type: 'value', max: 100 },115 series: [{116 type: 'bar',117 data: [85, 92, 78, 88, 95],118 itemStyle: { color: '#409EFF' }119 }]120 })121}122123onMounted(() => {124 initCharts()125 // 定时刷新时间126 const timer = setInterval(() => {127 currentTime.value = dayjs().format('YYYY-MM-DD HH:mm:ss')128 }, 1000)129 130 onUnmounted(() => clearInterval(timer))131})132</script>133134<style scoped>135.warehouse-screen {136 width: 100vw;137 height: 100vh;138 background: linear-gradient(135deg, #0f2027 0%, #203a43 50%, #2c5364 100%);139 color: #fff;140 padding: 20px;141 overflow: hidden;142}143144.screen-header {145 display: flex;146 justify-content: space-between;147 align-items: center;148 margin-bottom: 30px;149}150151.stats-row {152 display: grid;153 grid-template-columns: repeat(4, 1fr);154 gap: 20px;155 margin-bottom: 30px;156}157158.stat-card {159 display: flex;160 align-items: center;161 background: rgba(255, 255, 255, 0.1);162 backdrop-filter: blur(10px);163 border-radius: 10px;164 padding: 20px;165}166167.stat-icon {168 font-size: 48px;169 width: 80px;170 height: 80px;171 display: flex;172 align-items: center;173 justify-content: center;174 border-radius: 10px;175 margin-right: 20px;176}177178.stat-value {179 font-size: 32px;180 font-weight: bold;181}182183.charts-row {184 display: grid;185 grid-template-columns: repeat(3, 1fr);186 gap: 20px;187 margin-bottom: 30px;188}189190.chart-container {191 background: rgba(255, 255, 255, 0.1);192 backdrop-filter: blur(10px);193 border-radius: 10px;194 padding: 20px;195}196197.chart {198 height: 300px;199}200</style>六、部署方案
6.1 微服务架构部署
1# docker-compose.yml2version: '3.8'34services:5 # Nacos 注册中心6 nacos:7 image: nacos/nacos-server:v2.2.38 container_name: wms-nacos9 environment:10 - MODE=standalone11 - SPRING_DATASOURCE_PLATFORM=mysql12 - MYSQL_SERVICE_HOST=mysql13 - MYSQL_SERVICE_DB_NAME=nacos14 - MYSQL_SERVICE_USER=root15 - MYSQL_SERVICE_PASSWORD=12345616 ports:17 - "8848:8848"18 - "9848:9848"19 networks:20 - wms-network21 depends_on:22 - mysql2324 # MySQL 数据库25 mysql:26 image: mysql:8.027 container_name: wms-mysql28 environment:29 - MYSQL_ROOT_PASSWORD=12345630 - MYSQL_DATABASE=wms31 ports:32 - "3306:3306"33 volumes:34 - mysql-data:/var/lib/mysql35 - ./init-sql:/docker-entrypoint-initdb.d36 networks:37 - wms-network3839 # Redis 缓存40 redis:41 image: redis:7.0-alpine42 container_name: wms-redis43 command: redis-server --requirepass 12345644 ports:45 - "6379:6379"46 networks:47 - wms-network4849 # RocketMQ NameServer50 rocketmq-nameserver:51 image: apache/rocketmq:5.1.052 container_name: wms-rocketmq-nameserver53 command: sh mqnamesrv54 ports:55 - "9876:9876"56 networks:57 - wms-network5859 # RocketMQ Broker60 rocketmq-broker:61 image: apache/rocketmq:5.1.062 container_name: wms-rocketmq-broker63 command: sh mqbroker -n rocketmq-nameserver:987664 ports:65 - "10909:10909"66 - "10911:10911"67 environment:68 - NAMESRV_ADDR=rocketmq-nameserver:987669 depends_on:70 - rocketmq-nameserver71 networks:72 - wms-network7374 # API 网关75 gateway:76 image: wms/gateway:latest77 container_name: wms-gateway78 ports:79 - "8080:8080"80 environment:81 - NACOS_SERVER=nacos:884882 - REDIS_HOST=redis83 depends_on:84 - nacos85 - redis86 networks:87 - wms-network8889 # 库存服务90 inventory-service:91 image: wms/inventory-service:latest92 container_name: wms-inventory-service93 environment:94 - NACOS_SERVER=nacos:884895 - MYSQL_HOST=mysql96 - REDIS_HOST=redis97 depends_on:98 - nacos99 - mysql100 - redis101 networks:102 - wms-network103 deploy:104 replicas: 2105106 # 入库服务107 inbound-service:108 image: wms/inbound-service:latest109 container_name: wms-inbound-service110 environment:111 - NACOS_SERVER=nacos:8848112 - MYSQL_HOST=mysql113 - ROCKETMQ_NAMESRV=rocketmq-nameserver:9876114 depends_on:115 - nacos116 - mysql117 - rocketmq-nameserver118 networks:119 - wms-network120121 # 出库服务122 outbound-service:123 image: wms/outbound-service:latest124 container_name: wms-outbound-service125 environment:126 - NACOS_SERVER=nacos:8848127 - MYSQL_HOST=mysql128 - ROCKETMQ_NAMESRV=rocketmq-nameserver:9876129 depends_on:130 - nacos131 - mysql132 - rocketmq-nameserver133 networks:134 - wms-network135136networks:137 wms-network:138 driver: bridge139140volumes:141 mysql-data:6.2 Kubernetes 部署配置
1# k8s-deployment.yaml2---3apiVersion: apps/v14kind: Deployment5metadata:6 name: inventory-service7 namespace: wms8spec:9 replicas: 310 selector:11 matchLabels:12 app: inventory-service13 template:14 metadata:15 labels:16 app: inventory-service17 spec:18 containers:19 - name: inventory-service20 image: wms/inventory-service:v1.0.021 ports:22 - containerPort: 808123 env:24 - name: NACOS_SERVER25 value: "nacos.wms.svc.cluster.local:8848"26 - name: MYSQL_HOST27 value: "mysql.wms.svc.cluster.local"28 - name: REDIS_HOST29 value: "redis.wms.svc.cluster.local"30 resources:31 requests:32 memory: "512Mi"33 cpu: "500m"34 limits:35 memory: "1Gi"36 cpu: "1000m"37 livenessProbe:38 httpGet:39 path: /actuator/health40 port: 808141 initialDelaySeconds: 6042 periodSeconds: 1043 readinessProbe:44 httpGet:45 path: /actuator/health/readiness46 port: 808147 initialDelaySeconds: 3048 periodSeconds: 549---50apiVersion: v151kind: Service52metadata:53 name: inventory-service54 namespace: wms55spec:56 selector:57 app: inventory-service58 ports:59 - protocol: TCP60 port: 808161 targetPort: 808162 type: ClusterIP63---64apiVersion: autoscaling/v265kind: HorizontalPodAutoscaler66metadata:67 name: inventory-service-hpa68 namespace: wms69spec:70 scaleTargetRef:71 apiVersion: apps/v172 kind: Deployment73 name: inventory-service74 minReplicas: 275 maxReplicas: 1076 metrics:77 - type: Resource78 resource:79 name: cpu80 target:81 type: Utilization82 averageUtilization: 7083 - type: Resource84 resource:85 name: memory86 target:87 type: Utilization88 averageUtilization: 806.3 监控配置(Prometheus + Grafana)
1# prometheus.yml2global:3 scrape_interval: 15s4 evaluation_interval: 15s56scrape_configs:7 # Spring Boot Actuator 监控8 - job_name: 'wms-services'9 metrics_path: '/actuator/prometheus'10 static_configs:11 - targets:12 - 'inventory-service:8081'13 - 'inbound-service:8082'14 - 'outbound-service:8083'15 - 'picking-service:8084'16 relabel_configs:17 - source_labels: [__address__]18 target_label: instance19 regex: '([^:]+)(:[0-9]+)?'20 replacement: '${1}'2122 # MySQL 监控23 - job_name: 'mysql'24 static_configs:25 - targets: ['mysql-exporter:9104']2627 # Redis 监控28 - job_name: 'redis'29 static_configs:30 - targets: ['redis-exporter:9121']七、性能优化
7.1 数据库优化
7.1.1 分库分表策略
为什么需要分库分表?
| 问题 | 数据量阈值 | 影响 | 解决方案 |
|---|---|---|---|
| 单表数据过大 | > 500万行 | 查询慢、索引失效 | 分表 |
| 并发写入瓶颈 | > 5000 TPS | 锁等待、性能下降 | 分库 |
| 历史数据冗余 | 增长无限制 | 磁盘占用、备份慢 | 归档 |
我们的分表策略:
1/**2 * 库存流水表分表策略3 * 按月份分表: inventory_log_202401, inventory_log_202402 ...4 * 5 * 选择月份分表的原因:6 * 1. 流水数据按时间查询居多,月份分表查询效率高7 * 2. 每月数据量可控(约100-200万条)8 * 3. 便于历史数据归档(超过12个月自动归档到冷存储)9 */10@Component11public class InventoryLogShardingAlgorithm implements StandardShardingAlgorithm<LocalDateTime> {12 13 @Override14 public String doSharding(Collection<String> availableTargetNames, 15 PreciseShardingValue<LocalDateTime> shardingValue) {16 LocalDateTime createTime = shardingValue.getValue();17 String suffix = createTime.format(DateTimeFormatter.ofPattern("yyyyMM"));18 return "inventory_log_" + suffix;19 }20 21 @Override22 public Collection<String> doSharding(Collection<String> availableTargetNames,23 RangeShardingValue<LocalDateTime> shardingValue) {24 // 范围查询:跨月查询时路由到多个分表25 LocalDateTime start = shardingValue.getValueRange().lowerEndpoint();26 LocalDateTime end = shardingValue.getValueRange().upperEndpoint();27 28 Set<String> tables = new HashSet<>();29 YearMonth startMonth = YearMonth.from(start);30 YearMonth endMonth = YearMonth.from(end);31 32 while (!startMonth.isAfter(endMonth)) {33 String tableName = "inventory_log_" + startMonth.format(DateTimeFormatter.ofPattern("yyyyMM"));34 tables.add(tableName);35 startMonth = startMonth.plusMonths(1);36 }37 38 return tables;39 }40}配置示例:
1# ShardingSphere 配置2spring:3 shardingsphere:4 rules:5 sharding:6 tables:7 # 库存流水表分表配置8 inventory_log:9 actual-data-nodes: ds0.inventory_log_$->{202401..202412}10 table-strategy:11 standard:12 sharding-column: create_time13 sharding-algorithm-name: inventory-log-sharding14 sharding-algorithms:15 inventory-log-sharding:16 type: CLASS_BASED17 props:18 strategy: STANDARD19 algorithm-class-name: com.wms.config.InventoryLogShardingAlgorithm7.1.2 索引优化
索引设计原则:
- 覆盖索引优先:查询字段全部在索引中,避免回表
- 最左前缀匹配:组合索引按查询频率排列
- 避免过度索引:每个索引都有维护成本
- 定期分析索引:删除未使用的索引
1-- ========================================2-- 核心索引设计3-- ========================================45-- 1. 库存表组合索引(覆盖常用查询)6-- 查询场景:按仓库、商品、批次查询库存7CREATE INDEX idx_warehouse_goods_batch 8ON inventory(warehouse_id, goods_id, batch_no, quantity, lock_quantity);910-- 2. 库存表状态索引11-- 查询场景:查询异常库存、冻结库存12CREATE INDEX idx_status_goods 13ON inventory(status, goods_id) WHERE status != 1;1415-- 3. 出库单状态 + 创建时间索引16-- 查询场景:查询待处理订单、今日订单17CREATE INDEX idx_status_create_time 18ON outbound_order(status, create_time DESC)19WHERE status IN (1, 2, 3); -- 只索引未完成状态2021-- 4. 拣货任务分配索引22-- 查询场景:查找可分配任务23CREATE INDEX idx_warehouse_status_priority 24ON picking_task(warehouse_id, status, priority DESC, create_time)25WHERE status = 1; -- 只索引待分配状态2627-- 5. 库存流水时间范围查询28-- 查询场景:统计某段时间的出入库记录29CREATE INDEX idx_create_time_operation 30ON inventory_log(create_time, operation_type, quantity_change);3132-- 6. 商品名称全文索引(用于搜索)33CREATE FULLTEXT INDEX idx_goods_name_fulltext 34ON goods(goods_name, brand, model);3536-- ========================================37-- 索引监控与优化38-- ========================================3940-- 查看未使用的索引41SELECT 42 t.table_schema,43 t.table_name,44 s.index_name,45 s.rows_read,46 s.rows_inserted47FROM information_schema.tables t48LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage s 49 ON t.table_name = s.object_name50WHERE t.table_schema = 'wms'51 AND s.index_name IS NOT NULL52 AND s.index_name != 'PRIMARY'53 AND s.rows_read = 054ORDER BY t.table_name;5556-- 查看索引使用频率57SELECT 58 object_name AS table_name,59 index_name,60 count_star AS queries,61 sum_timer_wait / 1000000000000 AS total_latency_sec,62 avg_timer_wait / 1000000000 AS avg_latency_ms63FROM performance_schema.table_io_waits_summary_by_index_usage64WHERE object_schema = 'wms'65ORDER BY sum_timer_wait DESC66LIMIT 20;索引优化效果:
| 优化项 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 库存查询响应时间 | 450ms | 35ms | ↓ 92% |
| 订单列表查询 | 1.2s | 180ms | ↓ 85% |
| 拣货任务分配 | 680ms | 45ms | ↓ 93% |
| 索引数量 | 28个 | 18个 | ↓ 36% |
7.1.3 SQL优化实践
慢查询优化案例:
1-- ❌ 优化前:查询某商品的可用库存总量2-- 问题:全表扫描,未使用索引3SELECT SUM(quantity - lock_quantity) AS available_qty4FROM inventory5WHERE goods_id = 123456 AND status = 1;78-- 执行时间:850ms(扫描50万行)910-- ✅ 优化后:添加索引 + 使用覆盖索引11CREATE INDEX idx_goods_status_qty 12ON inventory(goods_id, status, quantity, lock_quantity);1314-- 执行时间:15ms(只扫描相关行)151617-- ❌ 优化前:分页查询订单列表(偏移量大时性能差)18SELECT * FROM outbound_order19WHERE status = 220ORDER BY create_time DESC21LIMIT 10000, 20;2223-- 执行时间:2.3s(需要跳过10000条记录)2425-- ✅ 优化后:使用延迟关联 + 子查询优化26SELECT o.* FROM outbound_order o27INNER JOIN (28 SELECT id FROM outbound_order29 WHERE status = 230 ORDER BY create_time DESC31 LIMIT 10000, 2032) AS t ON o.id = t.id;3334-- 执行时间:180ms(子查询只查ID,减少数据传输)353637-- ❌ 优化前:统计每个商品的库存分布(N+1查询)38-- Java代码中循环查询,产生大量SQL39for (Goods goods : goodsList) {40 List<Inventory> invs = inventoryMapper.selectByGoodsId(goods.getId());41 // ... 处理42}4344-- 执行时间:1000 * 50ms = 50秒4546-- ✅ 优化后:批量查询 + JOIN47SELECT g.id, g.goods_name, i.warehouse_id, SUM(i.quantity) AS total_qty48FROM goods g49LEFT JOIN inventory i ON g.id = i.goods_id50WHERE g.id IN (1, 2, 3, ..., 1000)51 AND i.status = 152GROUP BY g.id, i.warehouse_id;5354-- 执行时间:350ms7.2 缓存策略
1/**2 * 多级缓存策略3 */4@Service5public class InventoryCacheService {6 7 @Autowired8 private RedisTemplate<String, Object> redisTemplate;9 10 private final Cache<String, InventoryVO> localCache = CacheBuilder.newBuilder()11 .maximumSize(10000)12 .expireAfterWrite(5, TimeUnit.MINUTES)13 .build();14 15 /**16 * 查询库存(多级缓存)17 */18 public InventoryVO getInventory(Long warehouseId, Long goodsId) {19 String key = "inv:" + warehouseId + ":" + goodsId;20 21 // 1. 查询本地缓存22 InventoryVO cached = localCache.getIfPresent(key);23 if (cached != null) {24 return cached;25 }26 27 // 2. 查询 Redis28 cached = (InventoryVO) redisTemplate.opsForValue().get(key);29 if (cached != null) {30 localCache.put(key, cached);31 return cached;32 }33 34 // 3. 查询数据库35 InventoryVO inventory = inventoryMapper.selectByWarehouseAndGoods(warehouseId, goodsId);36 37 // 4. 写入缓存38 if (inventory != null) {39 redisTemplate.opsForValue().set(key, inventory, 10, TimeUnit.MINUTES);40 localCache.put(key, inventory);41 }42 43 return inventory;44 }45}7.3 消息队列异步处理
1/**2 * 出库消息生产者3 */4@Service5public class OutboundMessageProducer {6 7 @Autowired8 private RocketMQTemplate rocketMQTemplate;9 10 /**11 * 发送出库完成消息12 */13 public void sendOutboundCompletedMessage(Long orderId) {14 OutboundCompletedEvent event = new OutboundCompletedEvent();15 event.setOrderId(orderId);16 event.setTimestamp(LocalDateTime.now());17 18 // 异步发送19 rocketMQTemplate.asyncSend("OUTBOUND_COMPLETED_TOPIC", event, new SendCallback() {20 @Override21 public void onSuccess(SendResult sendResult) {22 log.info("出库消息发送成功: orderId={}", orderId);23 }24 25 @Override26 public void onException(Throwable throwable) {27 log.error("出库消息发送失败: orderId={}", orderId, throwable);28 // 重试或记录补偿任务29 }30 });31 }32}3334/**35 * 库存扣减消费者36 */37@Service38@RocketMQMessageListener(39 topic = "OUTBOUND_COMPLETED_TOPIC",40 consumerGroup = "inventory-consumer-group"41)42public class InventoryDeductConsumer implements RocketMQListener<OutboundCompletedEvent> {43 44 @Autowired45 private InventoryService inventoryService;46 47 @Override48 public void onMessage(OutboundCompletedEvent event) {49 try {50 // 扣减库存51 inventoryService.deductInventoryForOrder(event.getOrderId());52 log.info("库存扣减完成: orderId={}", event.getOrderId());53 } catch (Exception e) {54 log.error("库存扣减失败: orderId={}", event.getOrderId(), e);55 throw new RuntimeException("库存扣减失败,触发重试", e);56 }57 }58}八、总结与展望
8.1 系统特点
| 特点 | 说明 |
|---|---|
| ⚡ 高性能 | Redis 缓存 + 数据库优化,支持 10000+ TPS |
| 🔒 高可靠 | 分布式锁 + 事务保证,库存准确率 99.9% |
| 📈 可扩展 | 微服务架构,支持水平扩展 |
| 🎯 智能化 | 拣货路径优化,效率提升 40% |
| 📊 可视化 | 实时监控大屏,数据一目了然 |
8.2 未来规划
🤖 AI 智能化
- 库存需求预测
- 智能补货建议
- 异常检测告警
📱 移动化
- PDA 手持终端
- 语音拣货
- AR 辅助拣货
🌐 IoT 物联网
- RFID 自动识别
- 温湿度监控
- 自动化立体库
🔗 区块链追溯
- 商品溯源
- 防伪验证
- 供应链协同
附录
A. 常见问题
Q1: 如何处理库存并发问题?
A: 使用 Redis 分布式锁 + 数据库行锁(FOR UPDATE)双重保证。
Q2: 拣货路径优化算法复杂度如何?
A: 贪心算法时间复杂度 O(n²),适用于中小规模(< 1000 个库位)。
Q3: 如何保证数据一致性?
A: 采用分布式事务(Seata)+ 最终一致性(消息队列补偿)。
B. 参考资料
🎉 WMS 仓库管理系统设计文档
- 版本: v1.0.0
- 更新时间: 2025-10-16
- 作者: 系统架构团队
- 标签: WMS, 仓库管理, Spring Cloud, 微服务架构
九、完整数据字典
9.1 状态码字典
入库单状态 (inbound_status)
| 状态值 | 状态名称 | 说明 | 可操作 |
|---|---|---|---|
| 1 | 待审核 | 入库单已创建,等待审核 | 审核通过/驳回 |
| 2 | 待入库 | 审核通过,等待收货 | 开始收货 |
| 3 | 入库中 | 正在收货上架 | 继续入库/完成入库 |
| 4 | 已完成 | 入库完成 | 查看 |
| 5 | 已取消 | 入库单已取消 | 查看 |
出库单状态 (outbound_status)
| 状态值 | 状态名称 | 说明 | 可操作 |
|---|---|---|---|
| 1 | 待审核 | 出库单已创建,等待审核 | 审核通过/驳回 |
| 2 | 待出库 | 审核通过,等待创建拣货任务 | 创建波次 |
| 3 | 拣货中 | 正在执行拣货 | 查看进度 |
| 4 | 待复核 | 拣货完成,等待复核 | 开始复核 |
| 5 | 待发货 | 复核完成,等待打包发货 | 确认发货 |
| 6 | 已发货 | 商品已发出 | 查看物流 |
| 7 | 已取消 | 出库单已取消 | 查看 |
库存状态 (inventory_status)
| 状态值 | 状态名称 | 颜色标识 | 说明 |
|---|---|---|---|
| 1 | 正常 | success | 可正常出库 |
| 2 | 冻结 | danger | 不可出库,待处理 |
| 3 | 待检 | warning | 质检中,不可出库 |
| 4 | 损坏 | info | 已损坏,待报损 |
拣货任务状态 (picking_task_status)
| 状态值 | 状态名称 | 说明 |
|---|---|---|
| 1 | 待拣货 | 任务已分配,未开始 |
| 2 | 拣货中 | 拣货员正在执行 |
| 3 | 已完成 | 拣货完成 |
| 4 | 异常 | 拣货异常(库位空/库存不足/商品损坏) |
盘点单状态 (stock_taking_status)
| 状态值 | 状态名称 | 说明 |
|---|---|---|
| 1 | 待盘点 | 盘点单已生成,未盘点 |
| 2 | 已盘点 | 实盘完成,待复核 |
| 3 | 已复核 | 复核完成,待调整库存 |
| 4 | 已调整 | 库存已调整 |
9.2 类型字典
入库类型 (inbound_type)
| 类型值 | 类型名称 | 图标 | 业务场景 |
|---|---|---|---|
| PURCHASE | 采购入库 | 📦 | 供应商采购到货 |
| RETURN | 退货入库 | 🔙 | 客户退货入仓 |
| TRANSFER | 调拨入库 | 🔄 | 其他仓库调入 |
| PROFIT | 盘盈入库 | ➕ | 盘点发现多余库存 |
| PRODUCTION | 生产入库 | 🏭 | 生产完工入库 |
| OTHER | 其他入库 | 📝 | 其他入库场景 |
出库类型 (outbound_type)
| 类型值 | 类型名称 | 图标 | 业务场景 |
|---|---|---|---|
| SALE | 销售出库 | 🛒 | 客户订单发货 |
| TRANSFER | 调拨出库 | 🔄 | 调拨到其他仓库 |
| SCRAP | 报损出库 | ❌ | 损坏商品报损 |
| RETURN | 退货出库 | 🔙 | 退货给供应商 |
| LOSS | 盘亏出库 | ➖ | 盘点发现短缺 |
| OTHER | 其他出库 | 📝 | 其他出库场景 |
盘点类型 (stock_taking_type)
| 类型值 | 类型名称 | 适用场景 | 频率建议 |
|---|---|---|---|
| FULL | 全盘 | 全仓所有商品盘点 | 每年1-2次 |
| CYCLE | 循环盘 | 按ABC分类分批盘点 | 每月1次 |
| SPOT | 抽盘 | 随机抽取部分商品 | 每周1次 |
| DYNAMIC | 动态盘 | 零库存商品盘点 | 随时 |
移库类型 (stock_move_type)
| 类型值 | 类型名称 | 说明 |
|---|---|---|
| LOCATION | 库位调整 | 同一库区内库位间移动 |
| AREA | 库区调整 | 同一仓库内库区间移动 |
| WAREHOUSE | 仓库调拨 | 不同仓库间调拨 |
9.3 枚举字典
优先级 (priority)
| 优先级值 | 优先级名称 | 颜色 | 处理时效 |
|---|---|---|---|
| 1 | 普通 | default | 24小时内 |
| 2 | 紧急 | warning | 8小时内 |
| 3 | 特急 | danger | 2小时内 |
计量单位 (unit)
| 单位代码 | 单位名称 | 英文 |
|---|---|---|
| PCS | 个/件 | Piece |
| BOX | 箱 | Box |
| CTN | 纸箱 | Carton |
| PLT | 托盘 | Pallet |
| KG | 千克 | Kilogram |
| METER | 米 | Meter |
| LITER | 升 | Liter |
异常类型 (exception_type)
| 异常代码 | 异常名称 | 处理方式 |
|---|---|---|
| EMPTY | 库位空缺 | 触发盘点,查找替代库位 |
| SHORT | 库存不足 | 减单或等待补货 |
| DAMAGED | 商品损坏 | 标记残次,查找替代批次 |
| EXPIRED | 商品过期 | 冻结库存,安排报损 |
| WRONG | 拣错商品 | 回退重拣,记录错误率 |
9.4 补充字典
仓库类型 (warehouse_type)
| 类型值 | 类型名称 | 说明 |
|---|---|---|
| 1 | 成品仓 | 存储成品商品 |
| 2 | 原料仓 | 存储原材料 |
| 3 | 半成品仓 | 存储半成品 |
库区类型 (warehouse_area_type)
| 类型值 | 类型名称 | 说明 |
|---|---|---|
| STORAGE | 存储区 | 长期存储区域 |
| PICKING | 拣货区 | 高频出库商品存储 |
| STAGING | 暂存区 | 临时存放区域 |
| RECEIVING | 收货区 | 收货验收区域 |
| SHIPPING | 发货区 | 打包发货区域 |
库位类型 (location_type)
| 类型值 | 类型名称 | 说明 |
|---|---|---|
| NORMAL | 普通库位 | 标准存储库位 |
| TEMP | 临时库位 | 临时存放 |
| DEFECT | 残次品库位 | 存放残次品 |
| FROZEN | 冷冻库位 | 需冷藏商品 |
库位状态 (location_status)
| 状态值 | 状态名称 | 说明 |
|---|---|---|
| 1 | 空闲 | 库位空闲可用 |
| 2 | 占用 | 库位已存放商品 |
| 3 | 锁定 | 库位锁定不可用 |
| 0 | 禁用 | 库位禁用 |
拣货波次类型 (wave_type)
| 类型值 | 类型名称 | 说明 |
|---|---|---|
| BATCH | 批次拣货 | 多订单批量拣货 |
| ZONE | 分区拣货 | 按库区分区拣货 |
| SINGLE | 单品拣货 | 单个订单拣货 |
拣货波次状态 (wave_status)
| 状态值 | 状态名称 | 说明 |
|---|---|---|
| 1 | 待分配 | 波次已生成,待分配拣货员 |
| 2 | 已分配 | 已分配拣货员,待开始 |
| 3 | 拣货中 | 正在执行拣货 |
| 4 | 已完成 | 拣货完成 |
| 5 | 已取消 | 波次取消 |
盘点计划状态 (stock_taking_plan_status)
| 状态值 | 状态名称 | 说明 |
|---|---|---|
| 1 | 待审核 | 计划已创建,待审核 |
| 2 | 待执行 | 审核通过,待执行 |
| 3 | 执行中 | 正在盘点 |
| 4 | 已完成 | 盘点完成 |
| 5 | 已取消 | 计划取消 |
移库单状态 (stock_move_status)
| 状态值 | 状态名称 | 说明 |
|---|---|---|
| 1 | 待执行 | 移库单已创建,待执行 |
| 2 | 执行中 | 正在移库 |
| 3 | 已完成 | 移库完成 |
| 4 | 已取消 | 移库取消 |
入库单明细状态 (inbound_detail_status)
| 状态值 | 状态名称 | 说明 |
|---|---|---|
| 1 | 待收货 | 待收货验收 |
| 2 | 已收货 | 已收货待上架 |
| 3 | 已上架 | 已上架完成 |
出库单明细状态 (outbound_detail_status)
| 状态值 | 状态名称 | 说明 |
|---|---|---|
| 1 | 待拣货 | 待拣货 |
| 2 | 拣货中 | 正在拣货 |
| 3 | 已拣货 | 拣货完成待复核 |
| 4 | 已复核 | 复核完成待发货 |
| 5 | 已发货 | 已发货 |
供应商类型 (supplier_type)
| 类型值 | 类型名称 | 说明 |
|---|---|---|
| NORMAL | 普通供应商 | 一般合作供应商 |
| VIP | VIP供应商 | 重要合作伙伴 |
| STRATEGIC | 战略供应商 | 战略合作伙伴 |
客户类型 (customer_type)
| 类型值 | 类型名称 | 说明 |
|---|---|---|
| RETAIL | 零售客户 | 零售终端客户 |
| WHOLESALE | 批发客户 | 批发商客户 |
| ENTERPRISE | 企业客户 | 企业级客户 |
客户等级 (customer_level)
| 等级值 | 等级名称 | 说明 |
|---|---|---|
| VIP | VIP客户 | 最高级客户 |
| GOLD | 金牌客户 | 高价值客户 |
| SILVER | 银牌客户 | 中等价值客户 |
| NORMAL | 普通客户 | 一般客户 |
承运商类型 (carrier_type)
| 类型值 | 类型名称 | 说明 |
|---|---|---|
| EXPRESS | 快递 | 快递公司 |
| LOGISTICS | 物流 | 物流公司 |
| SPECIAL | 专线 | 专线物流 |
十、完整系统功能清单
10.1 菜单结构树
1WMS 仓库管理系统2├── 📊 工作台3│ ├── 首页仪表盘4│ ├── 待办任务5│ └── 数据大屏6├── 📦 库存管理7│ ├── 库存查询8│ ├── 库存预警9│ ├── 库存流水10│ ├── 库存快照11│ └── 库存对账12├── 📥 入库管理13│ ├── 入库单管理14│ ├── 采购入库15│ ├── 退货入库16│ ├── 调拨入库17│ └── 收货记录18├── 📤 出库管理19│ ├── 出库单管理20│ ├── 销售出库21│ ├── 调拨出库22│ ├── 报损出库23│ └── 发货记录24├── 🎯 拣货管理25│ ├── 拣货波次26│ ├── 拣货任务27│ ├── 拣货绩效28│ └── 路径优化配置29├── 🔍 盘点管理30│ ├── 盘点计划31│ ├── 盘点执行32│ ├── 盘点结果33│ └── 库存调整34├── 🏢 仓库管理35│ ├── 仓库信息36│ ├── 库区管理37│ ├── 库位管理38│ └── 容量规划39├── 🎁 商品管理40│ ├── 商品档案41│ ├── 商品分类42│ ├── 商品导入43│ └── 商品标签打印44├── 🔄 移库管理45│ ├── 移库单管理46│ ├── 库位调整47│ ├── 库区调整48│ └── 移库记录49├── 👥 往来单位50│ ├── 供应商管理51│ ├── 客户管理52│ └── 承运商管理53├── 📈 报表中心54│ ├── 库存报表55│ ├── 出入库统计56│ ├── 周转率分析57│ ├── 库龄分析58│ ├── ABC分类59│ └── 自定义报表60├── ⚙️ 系统管理61│ ├── 用户管理62│ ├── 角色管理63│ ├── 菜单管理64│ ├── 部门管理65│ ├── 数据字典66│ ├── 系统配置67│ └── 操作日志68└── 📱 移动端(PDA)69 ├── 收货上架70 ├── 拣货出库71 ├── 库存盘点72 ├── 库存移动73 └── 标签打印10.2 核心功能页面详细设计
10.2.1 库存查询页面
页面路径: /inventory/list
功能说明: 查询和管理所有库存信息
检索条件:
| 字段名 | 字段类型 | 是否必填 | 说明 |
|---|---|---|---|
| 仓库 | 下拉选择 | 否 | 支持多选 |
| 商品名称 | 文本输入 | 否 | 支持模糊查询 |
| SKU编码 | 文本输入 | 否 | 精确查询 |
| 商品分类 | 树形选择 | 否 | 支持多级分类 |
| 库位编码 | 文本输入 | 否 | 支持模糊查询 |
| 批次号 | 文本输入 | 否 | 精确查询 |
| 库存状态 | 下拉选择 | 否 | 正常/冻结/待检/损坏 |
| 库存预警 | 复选框 | 否 | 勾选显示低于安全库存的商品 |
| 库龄范围 | 日期区间 | 否 | 入库日期范围 |
表格列:
| 列名 | 宽度 | 是否固定 | 排序 | 说明 |
|---|---|---|---|---|
| 序号 | 60px | 左 | - | 自增序号 |
| 仓库名称 | 120px | - | ✅ | - |
| 库区 | 100px | - | ✅ | - |
| 库位编码 | 120px | - | ✅ | 可点击查看库位详情 |
| SKU编码 | 150px | - | ✅ | - |
| 商品名称 | 200px | - | ✅ | 悬浮显示完整名称 |
| 规格型号 | 150px | - | - | - |
| 批次号 | 120px | - | - | - |
| 总库存 | 100px | 右对齐 | ✅ | 低于安全库存红色显示 |
| 锁定数量 | 100px | 右对齐 | ✅ | - |
| 可用库存 | 100px | 右对齐 | ✅ | Tag标签样式 |
| 计量单位 | 80px | - | - | - |
| 库存状态 | 80px | - | ✅ | Tag标签样式 |
| 入库日期 | 150px | - | ✅ | - |
| 库龄(天) | 80px | 右对齐 | ✅ | 超过90天黄色提示 |
| 操作 | 180px | 右 | - | 详情/移库/冻结/调整 |
操作按钮:
- 详情: 查看库存详细信息(含流水记录)
- 移库: 跳转到移库单创建页面,自动填充商品信息
- 冻结: 冻结该库存,弹窗输入冻结原因
- 调整: 手动调整库存(需权限,记录操作日志)
批量操作:
- 批量导出(Excel)
- 批量冻结
- 批量打印标签
统计信息(页面顶部卡片):
- 总库存量
- 可用库存量
- 锁定库存量
- 预警商品数
10.2.2 入库单管理页面
页面路径: /inbound/list
功能说明: 管理所有入库单据
检索条件:
| 字段名 | 字段类型 | 是否必填 | 说明 |
|---|---|---|---|
| 入库单号 | 文本输入 | 否 | 精确查询 |
| 入库类型 | 下拉选择 | 否 | 采购/退货/调拨/盘盈/生产/其他 |
| 仓库 | 下拉选择 | 否 | - |
| 供应商 | 下拉选择 | 否 | 支持搜索 |
| 状态 | 下拉选择 | 否 | 待审核/待入库/入库中/已完成/已取消 |
| 创建日期 | 日期区间 | 否 | - |
| 入库日期 | 日期区间 | 否 | - |
| 创建人 | 文本输入 | 否 | - |
表格列:
| 列名 | 宽度 | 是否固定 | 排序 | 说明 |
|---|---|---|---|---|
| 序号 | 60px | 左 | - | - |
| 入库单号 | 180px | 左 | - | 点击跳转详情页 |
| 入库类型 | 100px | - | ✅ | Tag标签 |
| 仓库名称 | 120px | - | ✅ | - |
| 供应商 | 150px | - | - | - |
| 来源单号 | 150px | - | - | 采购订单号等 |
| 商品种类 | 80px | 右对齐 | ✅ | - |
| 计划数量 | 100px | 右对齐 | ✅ | - |
| 实收数量 | 100px | 右对齐 | ✅ | - |
| 状态 | 90px | - | ✅ | Tag标签 |
| 创建人 | 100px | - | - | - |
| 创建时间 | 150px | - | ✅ | - |
| 审核人 | 100px | - | - | - |
| 审核时间 | 150px | - | ✅ | - |
| 操作 | 200px | 右 | - | 查看/编辑/审核/开始入库/取消 |
操作按钮:
- 查看: 查看入库单详情(只读)
- 编辑: 编辑入库单(仅待审核状态可编辑)
- 审核: 审核入库单(弹窗,通过/驳回)
- 开始入库: 进入收货上架流程
- 打印: 打印入库单
- 取消: 取消入库单(需输入原因)
新增入库单表单:
| 字段名 | 字段类型 | 是否必填 | 校验规则 |
|---|---|---|---|
| 入库类型 | 单选 | 是 | - |
| 仓库 | 下拉选择 | 是 | - |
| 供应商 | 下拉选择 | 否 | 采购入库时必填 |
| 来源单号 | 文本输入 | 否 | - |
| 预计到货时间 | 日期时间选择器 | 否 | 不能早于当前时间 |
| 备注 | 多行文本 | 否 | 最多500字 |
| 商品明细 | 表格编辑 | 是 | 至少一条明细 |
商品明细表格列:
| 列名 | 字段类型 | 是否必填 | 说明 |
|---|---|---|---|
| SKU编码 | 搜索选择 | 是 | 搜索商品,自动带出商品信息 |
| 商品名称 | 显示 | - | 自动带出 |
| 规格 | 显示 | - | 自动带出 |
| 单位 | 显示 | - | 自动带出 |
| 批次号 | 文本输入 | 否 | 需批次管理商品必填 |
| 生产日期 | 日期选择器 | 否 | - |
| 过期日期 | 日期选择器 | 否 | 自动校验是否过期 |
| 计划数量 | 数字输入 | 是 | 必须>0 |
| 备注 | 文本输入 | 否 | - |
| 操作 | 按钮 | - | 删除行 |
10.2.3 出库单管理页面
页面路径: /outbound/list
功能说明: 管理所有出库单据
检索条件:
| 字段名 | 字段类型 | 是否必填 | 说明 |
|---|---|---|---|
| 出库单号 | 文本输入 | 否 | 精确查询 |
| 出库类型 | 下拉选择 | 否 | 销售/调拨/报损/退货/盘亏/其他 |
| 仓库 | 下拉选择 | 否 | - |
| 客户 | 下拉选择 | 否 | 支持搜索 |
| 优先级 | 下拉选择 | 否 | 普通/紧急/特急 |
| 状态 | 下拉选择 | 否 | 待审核/待出库/拣货中/待复核/待发货/已发货/已取消 |
| 创建日期 | 日期区间 | 否 | - |
| 期望发货时间 | 日期区间 | 否 | - |
表格列:
| 列名 | 宽度 | 是否固定 | 排序 | 说明 |
|---|---|---|---|---|
| 序号 | 60px | 左 | - | - |
| 出库单号 | 180px | 左 | - | 点击跳转详情页 |
| 出库类型 | 100px | - | ✅ | Tag标签 |
| 仓库名称 | 120px | - | ✅ | - |
| 客户名称 | 150px | - | - | - |
| 收货地址 | 200px | - | - | 悬浮显示完整地址 |
| 商品种类 | 80px | 右对齐 | ✅ | - |
| 计划数量 | 100px | 右对齐 | ✅ | - |
| 实发数量 | 100px | 右对齐 | ✅ | - |
| 优先级 | 80px | - | ✅ | Tag标签,特急红色 |
| 状态 | 90px | - | ✅ | Tag标签 |
| 承运商 | 100px | - | - | - |
| 物流单号 | 150px | - | - | 点击跳转物流追踪 |
| 期望发货时间 | 150px | - | ✅ | 超时红色标注 |
| 创建时间 | 150px | - | ✅ | - |
| 操作 | 220px | 右 | - | 查看/编辑/审核/创建波次/发货/取消 |
状态流转:
10.2.4 拣货波次管理页面
页面路径: /picking/wave/list
功能说明: 管理拣货波次,智能生成和分配拣货任务
检索条件:
| 字段名 | 字段类型 | 是否必填 | 说明 |
|---|---|---|---|
| 波次号 | 文本输入 | 否 | 精确查询 |
| 仓库 | 下拉选择 | 否 | - |
| 波次类型 | 下拉选择 | 否 | 批次拣货/分区拣货/单品拣货 |
| 拣货员 | 下拉选择 | 否 | - |
| 状态 | 下拉选择 | 否 | 待分配/已分配/拣货中/已完成/已取消 |
| 创建日期 | 日期区间 | 否 | - |
表格列:
| 列名 | 宽度 | 说明 |
|---|---|---|
| 波次号 | 180px | 点击查看详情 |
| 波次类型 | 100px | Tag标签 |
| 订单数量 | 80px | - |
| 商品种类 | 80px | - |
| 总数量 | 100px | - |
| 优先级 | 80px | Tag标签 |
| 拣货员 | 100px | - |
| 预计耗时 | 100px | 分钟 |
| 实际耗时 | 100px | 分钟,超时红色 |
| 拣货进度 | 120px | 进度条 |
| 状态 | 90px | Tag标签 |
| 开始时间 | 150px | - |
| 完成时间 | 150px | - |
| 操作 | 180px | 查看/分配/开始/取消 |
智能生成波次配置:
- 波次大小: 30-50单/波次
- 时间窗口: 30分钟内订单聚合
- 聚类算法: 按库区和商品位置聚类
- 优先级规则: 特急订单优先处理
波次详情页包含:
- 波次基本信息
- 包含的出库单列表
- 拣货路径可视化(库位地图)
- 拣货任务明细
- 拣货进度实时监控
10.2.5 盘点计划管理页面
页面路径: /stock-taking/plan/list
功能说明: 创建和管理盘点计划
检索条件:
| 字段名 | 字段类型 | 是否必填 | 说明 |
|---|---|---|---|
| 计划编号 | 文本输入 | 否 | - |
| 计划名称 | 文本输入 | 否 | 模糊查询 |
| 仓库 | 下拉选择 | 否 | - |
| 盘点类型 | 下拉选择 | 否 | 全盘/循环盘/抽盘/动态盘 |
| 状态 | 下拉选择 | 否 | 待审核/待执行/执行中/已完成/已取消 |
| 计划时间 | 日期区间 | 否 | - |
新增盘点计划表单:
| 字段名 | 字段类型 | 是否必填 | 说明 |
|---|---|---|---|
| 计划名称 | 文本输入 | 是 | 如:"2025年第一季度全盘" |
| 仓库 | 下拉选择 | 是 | - |
| 盘点类型 | 单选 | 是 | 全盘/循环盘/抽盘/动态盘 |
| 盘点范围 | 动态表单 | 是 | 根据盘点类型显示不同配置 |
| 计划开始时间 | 日期时间选择器 | 是 | - |
| 计划结束时间 | 日期时间选择器 | 是 | 必须晚于开始时间 |
| 盘点人员 | 多选 | 是 | 选择参与盘点的人员 |
| 备注 | 多行文本 | 否 | - |
盘点范围配置:
- 全盘: 自动包含仓库内所有商品
- 循环盘: 选择商品分类或ABC分类
- 抽盘: 输入抽样比例(如10%)和随机种子
- 动态盘: 选择零库存或指定商品
盘点执行流程:
- 审核通过后生成盘点单
- 盘点人员PDA扫描盘点
- 提交盘点结果
- 复核差异数据
- 生成盘盈盘亏单
- 调整库存
10.3 移动端(PDA)功能设计
10.3.1 收货上架
流程步骤:
- 扫描入库单号 → 显示商品明细
- 逐个扫描商品条码 → 输入实收数量
- 扫描目标库位 → 确认上架
- 打印库位标签
- 完成入库
页面元素:
- 大号输入框(支持扫码枪)
- 商品图片显示
- 数量调整按钮(+/-)
- 异常上报按钮
10.3.2 拣货出库
流程步骤:
- 登录 → 查看待拣货任务列表
- 选择任务 → 显示优化后的拣货路径
- 按顺序到达库位 → 扫描库位码
- 扫描商品条码 → 输入拣货数量
- 扫描集货容器 → 完成拣货
- 提交复核
特色功能:
- 路径导航(AR箭头指引)
- 语音播报库位和数量
- 拣货进度实时同步
- 异常一键上报
10.3.3 库存盘点
流程步骤:
- 扫描盘点单号 → 显示待盘点库位列表
- 到达库位 → 扫描库位码
- 逐个扫描商品条码 → 输入实盘数量
- 对比账面数量 → 标记差异
- 拍照上传(异常情况)
- 提交盘点结果
智能提示:
- 账面数量实时对比
- 差异自动标红
- 遗漏提示
- 盘点进度显示
十一、数据库设计补充(关联关系图)
11.1 完整ER图
11.2 核心表关联关系说明
| 主表 | 从表 | 关联字段 | 关系类型 | 约束 |
|---|---|---|---|---|
| warehouse | warehouse_area | warehouse_id | 1:N | CASCADE |
| warehouse_area | warehouse_location | area_id | 1:N | CASCADE |
| warehouse_location | inventory | location_id | 1:N | RESTRICT |
| goods | inventory | goods_id | 1:N | RESTRICT |
| goods_category | goods | category_id | 1:N | SET NULL |
| supplier | inbound_order | supplier_id | 1:N | SET NULL |
| customer | outbound_order | customer_code | 1:N | SET NULL |
| inbound_order | inbound_detail | inbound_id | 1:N | CASCADE |
| outbound_order | outbound_detail | outbound_id | 1:N | CASCADE |
| picking_wave | picking_wave_order | wave_id | 1:N | CASCADE |
| picking_wave | picking_task | wave_id | 1:N | CASCADE |
| stock_taking_plan | stock_taking | plan_id | 1:N | CASCADE |
约束说明:
- CASCADE: 主表记录删除时,从表记录同步删除
- RESTRICT: 存在从表记录时,主表记录不可删除
- SET NULL: 主表记录删除时,从表外键字段设为NULL
十二、初始化数据SQL
12.1 数据字典初始化
1-- 清空字典表2TRUNCATE TABLE sys_dict;34-- 入库类型字典5INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES6('inbound_type', '入库类型', 'PURCHASE', '采购入库', 1, 'success', 1),7('inbound_type', '入库类型', 'RETURN', '退货入库', 2, 'warning', 1),8('inbound_type', '入库类型', 'TRANSFER', '调拨入库', 3, 'info', 1),9('inbound_type', '入库类型', 'PROFIT', '盘盈入库', 4, 'success', 1),10('inbound_type', '入库类型', 'PRODUCTION', '生产入库', 5, 'primary', 1),11('inbound_type', '入库类型', 'OTHER', '其他入库', 6, 'default', 1);1213-- 出库类型字典14INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES15('outbound_type', '出库类型', 'SALE', '销售出库', 1, 'success', 1),16('outbound_type', '出库类型', 'TRANSFER', '调拨出库', 2, 'info', 1),17('outbound_type', '出库类型', 'SCRAP', '报损出库', 3, 'danger', 1),18('outbound_type', '出库类型', 'RETURN', '退货出库', 4, 'warning', 1),19('outbound_type', '出库类型', 'LOSS', '盘亏出库', 5, 'danger', 1),20('outbound_type', '出库类型', 'OTHER', '其他出库', 6, 'default', 1);2122-- 入库单状态字典23INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES24('inbound_status', '入库单状态', '1', '待审核', 1, 'info', 1),25('inbound_status', '入库单状态', '2', '待入库', 2, 'warning', 1),26('inbound_status', '入库单状态', '3', '入库中', 3, 'primary', 1),27('inbound_status', '入库单状态', '4', '已完成', 4, 'success', 1),28('inbound_status', '入库单状态', '5', '已取消', 5, 'danger', 1);2930-- 出库单状态字典31INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES32('outbound_status', '出库单状态', '1', '待审核', 1, 'info', 1),33('outbound_status', '出库单状态', '2', '待出库', 2, 'warning', 1),34('outbound_status', '出库单状态', '3', '拣货中', 3, 'primary', 1),35('outbound_status', '出库单状态', '4', '待复核', 4, 'primary', 1),36('outbound_status', '出库单状态', '5', '待发货', 5, 'warning', 1),37('outbound_status', '出库单状态', '6', '已发货', 6, 'success', 1),38('outbound_status', '出库单状态', '7', '已取消', 7, 'danger', 1);3940-- 库存状态字典41INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES42('inventory_status', '库存状态', '1', '正常', 1, 'success', 1),43('inventory_status', '库存状态', '2', '冻结', 2, 'danger', 1),44('inventory_status', '库存状态', '3', '待检', 3, 'warning', 1),45('inventory_status', '库存状态', '4', '损坏', 4, 'info', 1);4647-- 优先级字典48INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES49('priority', '优先级', '1', '普通', 1, 'default', 1),50('priority', '优先级', '2', '紧急', 2, 'warning', 1),51('priority', '优先级', '3', '特急', 3, 'danger', 1);5253-- 计量单位字典54INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES55('unit', '计量单位', 'PCS', '个/件', 1, 1),56('unit', '计量单位', 'BOX', '箱', 2, 1),57('unit', '计量单位', 'CTN', '纸箱', 3, 1),58('unit', '计量单位', 'PLT', '托盘', 4, 1),59('unit', '计量单位', 'KG', '千克', 5, 1),60('unit', '计量单位', 'METER', '米', 6, 1),61('unit', '计量单位', 'LITER', '升', 7, 1);6263-- 仓库类型字典64INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES65('warehouse_type', '仓库类型', '1', '成品仓', 1, 1),66('warehouse_type', '仓库类型', '2', '原料仓', 2, 1),67('warehouse_type', '仓库类型', '3', '半成品仓', 3, 1);6869-- 库区类型字典70INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES71('warehouse_area_type', '库区类型', 'STORAGE', '存储区', 1, 1),72('warehouse_area_type', '库区类型', 'PICKING', '拣货区', 2, 1),73('warehouse_area_type', '库区类型', 'STAGING', '暂存区', 3, 1),74('warehouse_area_type', '库区类型', 'RECEIVING', '收货区', 4, 1),75('warehouse_area_type', '库区类型', 'SHIPPING', '发货区', 5, 1);7677-- 库位类型字典78INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES79('location_type', '库位类型', 'NORMAL', '普通库位', 1, 1),80('location_type', '库位类型', 'TEMP', '临时库位', 2, 1),81('location_type', '库位类型', 'DEFECT', '残次品库位', 3, 1),82('location_type', '库位类型', 'FROZEN', '冷冻库位', 4, 1);8384-- 库位状态字典85INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES86('location_status', '库位状态', '1', '空闲', 1, 'success', 1),87('location_status', '库位状态', '2', '占用', 2, 'warning', 1),88('location_status', '库位状态', '3', '锁定', 3, 'danger', 1),89('location_status', '库位状态', '0', '禁用', 4, 'info', 1);9091-- 波次类型字典92INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES93('wave_type', '波次类型', 'BATCH', '批次拣货', 1, 1),94('wave_type', '波次类型', 'ZONE', '分区拣货', 2, 1),95('wave_type', '波次类型', 'SINGLE', '单品拣货', 3, 1);9697-- 波次状态字典98INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES99('wave_status', '波次状态', '1', '待分配', 1, 'info', 1),100('wave_status', '波次状态', '2', '已分配', 2, 'warning', 1),101('wave_status', '波次状态', '3', '拣货中', 3, 'primary', 1),102('wave_status', '波次状态', '4', '已完成', 4, 'success', 1),103('wave_status', '波次状态', '5', '已取消', 5, 'danger', 1);104105-- 盘点计划状态字典106INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES107('stock_taking_plan_status', '盘点计划状态', '1', '待审核', 1, 'info', 1),108('stock_taking_plan_status', '盘点计划状态', '2', '待执行', 2, 'warning', 1),109('stock_taking_plan_status', '盘点计划状态', '3', '执行中', 3, 'primary', 1),110('stock_taking_plan_status', '盘点计划状态', '4', '已完成', 4, 'success', 1),111('stock_taking_plan_status', '盘点计划状态', '5', '已取消', 5, 'danger', 1);112113-- 盘点类型字典114INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES115('stock_taking_type', '盘点类型', 'FULL', '全盘', 1, 1),116('stock_taking_type', '盘点类型', 'CYCLE', '循环盘', 2, 1),117('stock_taking_type', '盘点类型', 'SPOT', '抽盘', 3, 1),118('stock_taking_type', '盘点类型', 'DYNAMIC', '动态盘', 4, 1);119120-- 盘点单状态字典121INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES122('stock_taking_status', '盘点单状态', '1', '待盘点', 1, 'info', 1),123('stock_taking_status', '盘点单状态', '2', '已盘点', 2, 'warning', 1),124('stock_taking_status', '盘点单状态', '3', '已复核', 3, 'primary', 1),125('stock_taking_status', '盘点单状态', '4', '已调整', 4, 'success', 1);126127-- 移库类型字典128INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES129('stock_move_type', '移库类型', 'LOCATION', '库位调整', 1, 1),130('stock_move_type', '移库类型', 'AREA', '库区调整', 2, 1),131('stock_move_type', '移库类型', 'WAREHOUSE', '仓库调拨', 3, 1);132133-- 移库单状态字典134INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES135('stock_move_status', '移库单状态', '1', '待执行', 1, 'info', 1),136('stock_move_status', '移库单状态', '2', '执行中', 2, 'primary', 1),137('stock_move_status', '移库单状态', '3', '已完成', 3, 'success', 1),138('stock_move_status', '移库单状态', '4', '已取消', 4, 'danger', 1);139140-- 供应商类型字典141INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES142('supplier_type', '供应商类型', 'NORMAL', '普通供应商', 1, 1),143('supplier_type', '供应商类型', 'VIP', 'VIP供应商', 2, 1),144('supplier_type', '供应商类型', 'STRATEGIC', '战略供应商', 3, 1);145146-- 客户类型字典147INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES148('customer_type', '客户类型', 'RETAIL', '零售客户', 1, 1),149('customer_type', '客户类型', 'WHOLESALE', '批发客户', 2, 1),150('customer_type', '客户类型', 'ENTERPRISE', '企业客户', 3, 1);151152-- 客户等级字典153INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES154('customer_level', '客户等级', 'VIP', 'VIP客户', 1, 1),155('customer_level', '客户等级', 'GOLD', '金牌客户', 2, 1),156('customer_level', '客户等级', 'SILVER', '银牌客户', 3, 1),157('customer_level', '客户等级', 'NORMAL', '普通客户', 4, 1);158159-- 承运商类型字典160INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES161('carrier_type', '承运商类型', 'EXPRESS', '快递', 1, 1),162('carrier_type', '承运商类型', 'LOGISTICS', '物流', 2, 1),163('carrier_type', '承运商类型', 'SPECIAL', '专线', 3, 1);164165-- 入库单明细状态字典166INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES167('inbound_detail_status', '入库单明细状态', '1', '待收货', 1, 'info', 1),168('inbound_detail_status', '入库单明细状态', '2', '已收货', 2, 'warning', 1),169('inbound_detail_status', '入库单明细状态', '3', '已上架', 3, 'success', 1);170171-- 出库单明细状态字典172INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES173('outbound_detail_status', '出库单明细状态', '1', '待拣货', 1, 'info', 1),174('outbound_detail_status', '出库单明细状态', '2', '拣货中', 2, 'primary', 1),175('outbound_detail_status', '出库单明细状态', '3', '已拣货', 3, 'warning', 1),176('outbound_detail_status', '出库单明细状态', '4', '已复核', 4, 'warning', 1),177('outbound_detail_status', '出库单明细状态', '5', '已发货', 5, 'success', 1);178179-- 异常类型字典180INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES181('exception_type', '异常类型', 'EMPTY', '库位空缺', 1, 1),182('exception_type', '异常类型', 'SHORT', '库存不足', 2, 1),183('exception_type', '异常类型', 'DAMAGED', '商品损坏', 3, 1),184('exception_type', '异常类型', 'EXPIRED', '商品过期', 4, 1),185('exception_type', '异常类型', 'WRONG', '拣错商品', 5, 1);12.2 示例数据初始化
1-- 插入测试仓库2INSERT INTO warehouse (warehouse_code, warehouse_name, warehouse_type, province, city, district, address, contact_person, contact_phone, total_area, status) VALUES3('WH001', '北京总仓', 1, '北京市', '朝阳区', '望京街道', '望京SOHO T1座', '张三', '13800138000', 50000.00, 1),4('WH002', '上海分仓', 1, '上海市', '浦东新区', '张江', '张江高科技园区', '李四', '13900139000', 30000.00, 1);56-- 插入库区7INSERT INTO warehouse_area (warehouse_id, area_code, area_name, area_type, floor, area_size, status) VALUES8(1, 'A01', 'A区存储区', 'STORAGE', 1, 10000.00, 1),9(1, 'B01', 'B区拣货区', 'PICKING', 1, 8000.00, 1),10(1, 'C01', 'C区暂存区', 'STAGING', 1, 5000.00, 1);1112-- 插入示例库位13INSERT INTO warehouse_location (warehouse_id, area_id, location_code, location_type, row_no, column_no, layer_no, capacity, max_weight, status) VALUES14(1, 1, 'A01-01-01-01', 'NORMAL', 1, 1, 1, 1000.00, 500.00, 1),15(1, 1, 'A01-01-01-02', 'NORMAL', 1, 1, 2, 1000.00, 500.00, 1),16(1, 1, 'A01-01-02-01', 'NORMAL', 1, 2, 1, 1000.00, 500.00, 1);1718-- 插入商品分类19INSERT INTO goods_category (category_code, category_name, parent_id, level, sort_order) VALUES20('ELEC', '电子产品', 0, 1, 1),21('DAILY', '日用品', 0, 1, 2),22('FOOD', '食品', 0, 1, 3);2324-- 插入示例商品25INSERT INTO goods (sku_code, goods_name, category_id, brand, unit, weight, need_batch, safety_stock, status) VALUES26('SKU001', 'iPhone 15 Pro Max 256GB 黑色', 1, 'Apple', 'PCS', 0.240, 1, 10, 1),27('SKU002', '小米13 Ultra 16GB+1TB 黑色', 1, '小米', 'PCS', 0.227, 1, 20, 1),28('SKU003', '得力(Deli)签字笔 0.5mm 黑色', 2, '得力', 'BOX', 0.200, 0, 50, 1);2930-- 插入供应商31INSERT INTO supplier (supplier_code, supplier_name, supplier_type, contact_person, contact_phone, province, city, address, credit_level, status) VALUES32('SUP001', '苹果官方供应商', 'VIP', '王五', '13700137000', '北京市', '海淀区', '中关村软件园', 'A', 1),33('SUP002', '小米官方供应商', 'VIP', '赵六', '13600136000', '北京市', '海淀区', '小米科技园', 'A', 1);3435-- 插入客户36INSERT INTO customer (customer_code, customer_name, customer_type, customer_level, contact_person, contact_phone, delivery_city, delivery_address, credit_limit, status) VALUES37('CUS001', '京东商城', 'WHOLESALE', 'VIP', '刘七', '13500135000', '北京市', '北京市朝阳区京东总部', 1000000.00, 1),38('CUS002', '天猫旗舰店', 'WHOLESALE', 'VIP', '陈八', '13400134000', '杭州市', '浙江省杭州市滨江区阿里巴巴', 1000000.00, 1);十三、业务流程补充
13.1 完整入库业务流程
13.2 完整出库业务流程
13.3 盘点业务流程
十四、报表设计
14.1 库存报表
报表名称: 库存汇总表
更新频率: 实时
导出格式: Excel/PDF
报表列:
| 列名 | 说明 | 计算公式 |
|---|---|---|
| 仓库名称 | - | - |
| 商品分类 | - | - |
| SKU编码 | - | - |
| 商品名称 | - | - |
| 总库存 | - | SUM(quantity) |
| 锁定数量 | - | SUM(lock_quantity) |
| 可用库存 | - | 总库存 - 锁定数量 |
| 安全库存 | - | - |
| 库存状态 | - | 预警/正常/充足 |
| 库存金额 | 按成本价计算 | 总库存 × 成本价 |
| 占用库位数 | - | COUNT(DISTINCT location_id) |
| 平均库龄 | 天数 | AVG(DATEDIFF(NOW(), inbound_date)) |
筛选条件:
- 仓库(多选)
- 商品分类(树形)
- 库存状态(正常/预警/零库存)
- 库龄范围
14.2 出入库统计报表
报表名称: 出入库日报/月报
更新频率: 每日凌晨
导出格式: Excel/PDF
报表列:
| 列名 | 说明 | 计算公式 |
|---|---|---|
| 日期 | - | - |
| 仓库名称 | - | - |
| 入库单数 | - | COUNT(入库单) |
| 入库商品种类 | - | COUNT(DISTINCT goods_id) |
| 入库总数量 | - | SUM(actual_quantity) |
| 出库单数 | - | COUNT(出库单) |
| 出库商品种类 | - | COUNT(DISTINCT goods_id) |
| 出库总数量 | - | SUM(actual_quantity) |
| 净变化量 | - | 入库 - 出库 |
| 完成率 | 百分比 | 完成单数 / 总单数 × 100% |
图表展示:
- 出入库趋势折线图
- 出入库类型饼图
- TOP10商品柱状图
14.3 库存周转率分析
报表名称: 库存周转率分析表
统计周期: 月度
计算公式: 周转率 = 出库总额 / 平均库存金额
报表列:
| 列名 | 说明 | 计算公式 |
|---|---|---|
| 商品分类 | - | - |
| SKU编码 | - | - |
| 商品名称 | - | - |
| 期初库存 | 件 | - |
| 本期入库 | 件 | - |
| 本期出库 | 件 | - |
| 期末库存 | 件 | - |
| 平均库存 | 件 | (期初 + 期末) / 2 |
| 周转率 | 次 | 本期出库 / 平均库存 |
| 周转天数 | 天 | 30 / 周转率 |
| ABC分类 | - | 根据周转率分类 |
ABC分类规则:
- A类: 周转率 > 6次/月 (快速周转)
- B类: 周转率 3-6次/月 (正常周转)
- C类: 周转率 < 3次/月 (慢速周转)
十五、技术难点与开发挑战
15.1 核心技术难点
15.1.1 高并发库存扣减一致性问题
问题背景: 在电商大促等高并发场景下,多个用户同时购买同一商品,系统需要保证:
- 不超卖:库存扣减准确无误
- 高性能:秒级响应用户请求
- 高可用:服务稳定不宕机
核心挑战:
- 并发安全:多线程同时修改库存数据
- 性能瓶颈:数据库锁竞争激烈
- 分布式一致性:多服务节点数据同步
解决方案 - 三重保障机制:
1/**2 * 库存扣减 - 分布式锁 + 数据库行锁 + 乐观锁3 */4@Override5@Transactional(rollbackFor = Exception.class)6public boolean deductInventory(Long goodsId, BigDecimal quantity) {7 String lockKey = "inventory:lock:" + goodsId;8 RLock lock = redissonClient.getLock(lockKey);9 10 try {11 // 1. Redis分布式锁(保证集群环境下的互斥)12 boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);13 if (!acquired) {14 throw new BizException("系统繁忙,请稍后重试");15 }16 17 // 2. 数据库行锁(保证单机环境下的互斥)18 Inventory inventory = inventoryMapper.selectForUpdate(goodsId);19 20 // 3. 业务校验21 if (inventory.getAvailableQuantity().compareTo(quantity) < 0) {22 throw new BizException("库存不足");23 }24 25 // 4. 乐观锁更新(兜底机制)26 int updated = inventoryMapper.deductWithVersion(27 goodsId, quantity, inventory.getVersion()28 );29 30 if (updated == 0) {31 throw new BizException("库存更新失败,请重试");32 }33 34 // 5. 异步记录流水35 inventoryLogProducer.sendLog(inventory, quantity);36 37 // 6. 清除缓存38 redisTemplate.delete("inventory:" + goodsId);39 40 return true;41 } finally {42 if (lock.isHeldByCurrentThread()) {43 lock.unlock();44 }45 }46}技术亮点:
- 锁粒度控制:按商品ID加锁,不同商品并行处理
- 超时机制:3秒获锁超时,避免线程堆积
- 性能优化:异步日志记录,立即清除缓存
- 容错设计:多重校验,兜底保障
效果评估:
- 并发处理能力:支持10000+QPS
- 数据准确性:100%防超卖
- 响应时间:P99 < 100ms
15.1.2 拣货路径优化算法
问题背景: 传统仓库拣货效率低下的原因:
- 拣货路径冗余,重复行走
- 拣货顺序随机,缺乏规划
- 人工经验依赖,难以标准化
算法选择对比:
| 算法 | 时间复杂度 | 优化效果 | 适用规模 | 是否采用 |
|---|---|---|---|---|
| 暴力枚举 | O(n!) | 100%最优 | n < 10 | ❌ 不实用 |
| 动态规划 | O(n²·2ⁿ) | 100%最优 | n < 20 | ❌ 性能差 |
| 贪心算法 | O(n²) | 80-90%优化 | n < 1000 | ✅ 采用 |
| 遗传算法 | O(n·g·p) | 85-95%优化 | n > 1000 | ⚪ 备选 |
核心算法实现:
1/**2 * 改进的贪心算法 + 2-opt优化3 */4public List<Location> optimizePath(List<Location> locations) {5 List<Location> optimizedPath = new ArrayList<>();6 Set<Location> unvisited = new HashSet<>(locations);7 8 // 1. 起点:选择距离库区入口最近的库位9 Location entrance = getWarehouseEntrance();10 Location current = findNearest(entrance, unvisited);11 optimizedPath.add(current);12 unvisited.remove(current);13 14 // 2. 贪心选择:每次选择最近的未访问库位15 while (!unvisited.isEmpty()) {16 Location nearest = findNearest(current, unvisited);17 optimizedPath.add(nearest);18 unvisited.remove(nearest);19 current = nearest;20 }21 22 // 3. 2-opt优化:消除交叉路径23 return eliminateCrossings(optimizedPath);24}2526/**27 * 曼哈顿距离计算(适配仓库直角通道)28 */29private double calculateDistance(Location loc1, Location loc2) {30 int rowDiff = Math.abs(loc1.getRowNo() - loc2.getRowNo());31 int colDiff = Math.abs(loc1.getColumnNo() - loc2.getColumnNo());32 int layerDiff = Math.abs(loc1.getLayerNo() - loc2.getLayerNo());33 34 // 层间移动成本更高35 return rowDiff + colDiff + layerDiff * 2.0;36}优化效果对比:
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 平均拣货路径 | 450米 | 280米 | ↓ 38% |
| 平均拣货时间 | 25分钟 | 16分钟 | ↓ 36% |
| 拣货员日产能 | 180单 | 280单 | ↑ 56% |
| 路径交叉次数 | 8次 | 1次 | ↓ 88% |
15.1.3 智能波次生成与任务调度
问题背景: 如何将散乱的订单智能聚合成拣货波次,并合理分配给拣货员?
核心算法 - K-means聚类变种:
1/**2 * 订单聚类 - 按商品位置聚合相似区域订单3 */4private Map<String, List<Order>> clusterOrders(List<Order> orders) {5 Map<String, List<Order>> clusters = new HashMap<>();6 7 for (Order order : orders) {8 // 计算订单的"重心位置"9 Location centerLocation = calculateCenterLocation(order);10 11 // 分配到最近的库区12 String zoneCode = locationService.getNearestZone(centerLocation);13 14 clusters.computeIfAbsent(zoneCode, k -> new ArrayList<>()).add(order);15 }16 17 return clusters;18}1920/**21 * 拣货员评分算法 - 综合考虑位置、负载、技能、效率22 */23private double calculatePickerScore(Picker picker, PickingWave wave) {24 double score = 0;25 26 // 1. 位置得分(40%权重)27 double distance = calculateDistance(picker.getCurrentLocation(), wave.getStartLocation());28 double locationScore = Math.max(0, 100 - distance);29 score += locationScore * 0.4;30 31 // 2. 负载得分(30%权重)32 int currentTasks = picker.getCurrentTaskCount();33 double loadScore = Math.max(0, 100 - currentTasks * 10);34 score += loadScore * 0.3;35 36 // 3. 技能得分(20%权重)37 double skillScore = picker.getSkillLevel() * 20;38 score += skillScore * 0.2;39 40 // 4. 效率得分(10%权重)41 double efficiencyScore = picker.getEfficiencyRate();42 score += efficiencyScore * 0.1;43 44 return score;45}效果提升:
- 拣货效率提升:60%
- 人员利用率:95%
- 订单处理时效:提升35%
15.2 架构设计挑战
15.2.1 微服务拆分策略
拆分原则:
- 业务能力边界:每个服务负责完整的业务域
- 数据独立性:避免跨服务的数据事务
- 团队组织结构:符合康威定律
- 技术栈异构:不同服务可选择最适合的技术
服务划分结果:
1services:2 - name: warehouse-service3 responsibility: 仓库、库区、库位管理4 database: warehouse_db5 6 - name: inventory-service 7 responsibility: 库存管理、库存流水8 database: inventory_db9 10 - name: inbound-service11 responsibility:入库单、收货上架12 database: inbound_db13 14 - name: outbound-service15 responsibility: 出库单、订单处理16 database: outbound_db17 18 - name: picking-service19 responsibility: 拣货波次、任务调度20 database: picking_db21 22 - name: report-service23 responsibility: 报表分析、数据统计24 database: report_db15.2.2 分布式事务处理
场景:出库流程的分布式事务
涉及服务:
- 库存服务:扣减库存
- 订单服务:更新状态
- 物流服务:创建运单
- 账务服务:记录成本
解决方案 - Saga模式:
1/**2 * 出库流程编排3 */4@Component5public class OutboundSaga {6 7 @SagaOrchestrationStart8 public void processOutbound(OutboundOrder order) {9 // 1. 锁定库存10 sagaManager.choreography()11 .step("lockInventory")12 .invokeParticipant("inventory-service")13 .withCompensation("unlockInventory")14 15 // 2. 更新订单状态16 .step("updateOrderStatus") 17 .invokeParticipant("order-service")18 .withCompensation("revertOrderStatus")19 20 // 3. 创建运单21 .step("createWaybill")22 .invokeParticipant("logistics-service") 23 .withCompensation("cancelWaybill")24 25 // 4. 记录成本26 .step("recordCost")27 .invokeParticipant("finance-service")28 .withCompensation("reverseCost")29 30 .execute();31 }32}15.3 性能优化策略
15.3.1 完整缓存架构设计
缓存架构总体设计
L1: 本地缓存设计(Caffeine)
适用数据特征:
- 几乎不变的基础数据(商品信息、仓库信息)
- 访问频率极高的热点数据
- 数据量相对较小(< 1GB)
配置策略:
1@Configuration2public class LocalCacheConfig {3 4 /**5 * 商品信息缓存配置6 */7 @Bean("goodsCache")8 public Cache<Long, Goods> goodsCache() {9 return Caffeine.newBuilder()10 // 最大容量:10万个商品11 .maximumSize(100_000)12 // 写入后过期时间:24小时13 .expireAfterWrite(24, TimeUnit.HOURS)14 // 访问后刷新时间:2小时 15 .refreshAfterWrite(2, TimeUnit.HOURS)16 // 初始容量17 .initialCapacity(1000)18 // 记录统计信息19 .recordStats()20 // 异步加载器21 .buildAsync(new CacheLoader<Long, Goods>() {22 @Override23 public Goods load(Long goodsId) {24 return goodsService.getFromRedis(goodsId);25 }26 });27 }28 29 /**30 * 仓库信息缓存配置31 */32 @Bean("warehouseCache")33 public Cache<Long, Warehouse> warehouseCache() {34 return Caffeine.newBuilder()35 .maximumSize(1000)36 // 仓库信息基本不变,过期时间设长一点37 .expireAfterWrite(7, TimeUnit.DAYS)38 .refreshAfterWrite(1, TimeUnit.DAYS)39 .recordStats()40 .build();41 }42 43 /**44 * 库位信息缓存配置 45 */46 @Bean("locationCache")47 public Cache<String, Location> locationCache() {48 return Caffeine.newBuilder()49 .maximumSize(50000) // 5万个库位50 .expireAfterWrite(12, TimeUnit.HOURS)51 .refreshAfterWrite(1, TimeUnit.HOURS)52 .recordStats()53 .build();54 }55}缓存服务实现:
1@Service2@Slf4j3public class LocalCacheService {4 5 @Autowired6 @Qualifier("goodsCache")7 private Cache<Long, Goods> goodsCache;8 9 @Autowired 10 @Qualifier("warehouseCache")11 private Cache<Long, Warehouse> warehouseCache;12 13 /**14 * 获取商品信息(支持批量获取)15 */16 public Map<Long, Goods> getGoods(Set<Long> goodsIds) {17 Map<Long, CompletableFuture<Goods>> futures = goodsCache.getAll(goodsIds);18 19 Map<Long, Goods> result = new HashMap<>();20 futures.forEach((id, future) -> {21 try {22 result.put(id, future.get(100, TimeUnit.MILLISECONDS));23 } catch (Exception e) {24 log.warn("获取商品信息超时,goodsId: {}", id, e);25 // 降级到L2缓存26 Goods goods = redisService.getGoods(id);27 if (goods != null) {28 result.put(id, goods);29 // 异步更新L1缓存30 goodsCache.put(id, CompletableFuture.completedFuture(goods));31 }32 }33 });34 35 return result;36 }37 38 /**39 * 缓存预热40 */41 @PostConstruct42 public void warmup() {43 log.info("开始缓存预热...");44 45 // 预热热门商品46 List<Long> hotGoodsIds = goodsService.getHotGoodsIds(1000);47 goodsCache.getAll(new HashSet<>(hotGoodsIds));48 49 // 预热所有仓库信息50 List<Warehouse> warehouses = warehouseService.getAllWarehouses();51 warehouses.forEach(w -> warehouseCache.put(w.getId(), w));52 53 log.info("缓存预热完成");54 }55}L2: 分布式缓存设计(Redis Cluster)
集群架构:
1# Redis Cluster 配置2redis:3 cluster:4 nodes:5 - 192.168.1.10:7000 # 主节点16 - 192.168.1.10:7001 # 主节点1从节点7 - 192.168.1.11:7000 # 主节点2 8 - 192.168.1.11:7001 # 主节点2从节点9 - 192.168.1.12:7000 # 主节点310 - 192.168.1.12:7001 # 主节点3从节点11 max-redirects: 312 timeout: 3000ms13 pool:14 max-active: 10015 max-idle: 2016 min-idle: 517 max-wait: 3000ms缓存分层设计:
1@Service2@Slf4j 3public class RedisCacheService {4 5 @Autowired6 private RedisTemplate<String, Object> redisTemplate;7 8 @Autowired9 private StringRedisTemplate stringRedisTemplate;10 11 /**12 * 库存缓存 - 热数据,短TTL13 */14 public void setInventory(Long goodsId, Inventory inventory) {15 String key = CacheKey.INVENTORY + goodsId;16 redisTemplate.opsForValue().set(key, inventory, 10, TimeUnit.MINUTES);17 18 // 设置库存数量的单独缓存,用于快速检查19 String qtyKey = CacheKey.INVENTORY_QTY + goodsId; 20 stringRedisTemplate.opsForValue().set(qtyKey, 21 inventory.getAvailableQuantity().toString(), 5, TimeUnit.MINUTES);22 }23 24 /**25 * 商品信息缓存 - 温数据,长TTL26 */27 public void setGoods(Long goodsId, Goods goods) {28 String key = CacheKey.GOODS + goodsId;29 redisTemplate.opsForValue().set(key, goods, 24, TimeUnit.HOURS);30 }31 32 /**33 * 分布式锁缓存34 */35 public boolean tryLock(String lockKey, String value, long expireTime) {36 Boolean result = stringRedisTemplate.opsForValue()37 .setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);38 return Boolean.TRUE.equals(result);39 }40 41 /**42 * 批量操作 - 使用Pipeline提升性能43 */44 public Map<Long, Inventory> batchGetInventory(Set<Long> goodsIds) {45 List<String> keys = goodsIds.stream()46 .map(id -> CacheKey.INVENTORY + id)47 .collect(Collectors.toList());48 49 // 使用Pipeline批量获取50 List<Object> results = redisTemplate.executePipelined(51 (RedisCallback<Object>) connection -> {52 keys.forEach(key -> connection.get(key.getBytes()));53 return null;54 }55 );56 57 Map<Long, Inventory> inventoryMap = new HashMap<>();58 Iterator<Long> idIter = goodsIds.iterator();59 60 for (Object result : results) {61 Long goodsId = idIter.next();62 if (result != null) {63 inventoryMap.put(goodsId, (Inventory) result);64 }65 }66 67 return inventoryMap;68 }69 70 /**71 * 布隆过滤器 - 防止缓存穿透72 */73 @Autowired74 private RedisTemplate<String, Object> bloomRedisTemplate;75 76 public boolean bloomContains(String key, String value) {77 // 使用多个hash函数78 int hash1 = Math.abs(value.hashCode() % 1000000);79 int hash2 = Math.abs((value.hashCode() * 31) % 1000000);80 int hash3 = Math.abs((value.hashCode() * 37) % 1000000);81 82 return bloomRedisTemplate.opsForValue().getBit(key, hash1) &&83 bloomRedisTemplate.opsForValue().getBit(key, hash2) &&84 bloomRedisTemplate.opsForValue().getBit(key, hash3);85 }86 87 public void bloomAdd(String key, String value) {88 int hash1 = Math.abs(value.hashCode() % 1000000);89 int hash2 = Math.abs((value.hashCode() * 31) % 1000000); 90 int hash3 = Math.abs((value.hashCode() * 37) % 1000000);91 92 bloomRedisTemplate.opsForValue().setBit(key, hash1, true);93 bloomRedisTemplate.opsForValue().setBit(key, hash2, true);94 bloomRedisTemplate.opsForValue().setBit(key, hash3, true);95 }96}缓存Key设计规范:
1public class CacheKey {2 // 业务缓存Key3 public static final String INVENTORY = "wms:inventory:"; // 库存信息 4 public static final String INVENTORY_QTY = "wms:inventory:qty:"; // 库存数量5 public static final String GOODS = "wms:goods:"; // 商品信息6 public static final String WAREHOUSE = "wms:warehouse:"; // 仓库信息7 public static final String LOCATION = "wms:location:"; // 库位信息8 public static final String ORDER = "wms:order:"; // 订单信息9 10 // 锁Key 11 public static final String LOCK_INVENTORY = "wms:lock:inventory:"; // 库存锁12 public static final String LOCK_ORDER = "wms:lock:order:"; // 订单锁13 14 // 布隆过滤器Key15 public static final String BLOOM_GOODS = "wms:bloom:goods"; // 商品存在性16 public static final String BLOOM_ORDER = "wms:bloom:order"; // 订单存在性17 18 // 计数器Key19 public static final String COUNTER_REQUEST = "wms:counter:request:"; // 请求计数20 public static final String COUNTER_ERROR = "wms:counter:error:"; // 错误计数21}缓存更新策略
1. Cache Aside模式(主要使用):
1@Service2public class InventoryCacheService {3 4 /**5 * 查询库存(Cache Aside模式)6 */7 public Inventory getInventory(Long goodsId) {8 // 1. 先查L1缓存9 Inventory inventory = localCacheService.getInventory(goodsId);10 if (inventory != null) {11 return inventory;12 }13 14 // 2. 再查L2缓存 15 inventory = redisCacheService.getInventory(goodsId);16 if (inventory != null) {17 // 异步更新L1缓存18 localCacheService.setInventoryAsync(goodsId, inventory);19 return inventory;20 }21 22 // 3. 查询数据库23 inventory = inventoryMapper.selectByGoodsId(goodsId);24 if (inventory != null) {25 // 异步更新缓存26 CompletableFuture.runAsync(() -> {27 redisCacheService.setInventory(goodsId, inventory);28 localCacheService.setInventory(goodsId, inventory);29 });30 }31 32 return inventory;33 }34 35 /**36 * 更新库存(Cache Aside模式)37 */38 @Transactional(rollbackFor = Exception.class)39 public boolean updateInventory(Long goodsId, BigDecimal quantity) {40 // 1. 先更新数据库41 boolean updated = inventoryMapper.updateQuantity(goodsId, quantity) > 0;42 43 if (updated) {44 // 2. 删除缓存,让下次查询时重新加载45 redisCacheService.deleteInventory(goodsId);46 localCacheService.deleteInventory(goodsId);47 48 // 3. 发送缓存更新事件(可选)49 eventPublisher.publishEvent(new InventoryUpdatedEvent(goodsId));50 }51 52 return updated;53 }54}2. Write Through模式(实时性要求高):
1/**2 * 库存扣减 - Write Through模式3 * 保证缓存和数据库的强一致性4 */5@Transactional(rollbackFor = Exception.class) 6public boolean deductInventory(Long goodsId, BigDecimal quantity) {7 RLock lock = redissonClient.getLock("inventory:lock:" + goodsId);8 9 try {10 if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {11 // 1. 查询最新库存12 Inventory inventory = inventoryMapper.selectForUpdate(goodsId);13 14 // 2. 检查库存充足性15 if (inventory.getAvailableQuantity().compareTo(quantity) < 0) {16 throw new BizException("库存不足");17 }18 19 // 3. 同时更新数据库和缓存20 inventory.setQuantity(inventory.getQuantity().subtract(quantity));21 inventory.setVersion(inventory.getVersion() + 1);22 23 int updated = inventoryMapper.updateWithVersion(inventory);24 if (updated > 0) {25 // 同步更新缓存26 redisCacheService.setInventory(goodsId, inventory);27 localCacheService.setInventory(goodsId, inventory);28 29 return true;30 }31 }32 } finally {33 if (lock.isHeldByCurrentThread()) {34 lock.unlock();35 }36 }37 38 return false;39}3. Write Behind模式(批量更新):
1/**2 * 库存流水异步批量写入3 */4@Component5public class InventoryLogWriteBehind {6 7 private final Queue<InventoryLog> writeQueue = new LinkedBlockingQueue<>(10000);8 9 @Scheduled(fixedDelay = 5000) // 每5秒执行一次10 public void batchWrite() {11 List<InventoryLog> logs = new ArrayList<>();12 13 // 批量取出待写入数据14 while (!writeQueue.isEmpty() && logs.size() < 1000) {15 InventoryLog log = writeQueue.poll();16 if (log != null) {17 logs.add(log);18 }19 }20 21 if (!logs.isEmpty()) {22 try {23 // 批量插入数据库24 inventoryLogMapper.batchInsert(logs);25 log.info("批量写入库存流水 {} 条", logs.size());26 } catch (Exception e) {27 log.error("批量写入库存流水失败", e);28 // 重新放回队列29 logs.forEach(writeQueue::offer);30 }31 }32 }33 34 public void addLog(InventoryLog inventoryLog) {35 if (!writeQueue.offer(inventoryLog)) {36 log.warn("写入队列已满,同步写入数据库");37 inventoryLogMapper.insert(inventoryLog);38 }39 }40}缓存监控与运维
1@Component 2public class CacheMonitor {3 4 @Autowired5 @Qualifier("goodsCache")6 private Cache<Long, Goods> goodsCache;7 8 /**9 * 缓存统计指标10 */11 @Scheduled(fixedRate = 60000) // 每分钟统计一次12 public void collectCacheStats() {13 CacheStats stats = goodsCache.stats();14 15 // 记录关键指标16 log.info("商品缓存统计 - 命中率: {}, 加载次数: {}, 驱逐次数: {}, 平均加载时间: {}ms",17 String.format("%.2f%%", stats.hitRate() * 100),18 stats.loadCount(),19 stats.evictionCount(), 20 String.format("%.2f", stats.averageLoadPenalty() / 1_000_000.0)21 );22 23 // 发送到监控系统24 metricsService.recordCacheHitRate("goods_cache", stats.hitRate());25 metricsService.recordCacheLoadTime("goods_cache", stats.averageLoadPenalty());26 }27 28 /**29 * 缓存健康检查30 */31 @EventListener32 public void onCacheLoadException(CacheLoadExceptionEvent event) {33 log.error("缓存加载异常: key={}, cause={}", event.getKey(), event.getCause());34 35 // 发送告警36 alertService.sendAlert("缓存加载异常", 37 "Key: " + event.getKey() + ", Error: " + event.getCause().getMessage());38 }39}15.3.2 完整分库分表设计
分库分表架构设计
分片策略设计
1. 库存相关表 - 按仓库ID分片
1# ShardingSphere配置2dataSources:3 wms_db_001:4 url: jdbc:mysql://192.168.1.10:3306/wms_db_0015 username: wms_user6 password: wms_password7 8 wms_db_002:9 url: jdbc:mysql://192.168.1.11:3306/wms_db_00210 username: wms_user 11 password: wms_password12 13 wms_db_003:14 url: jdbc:mysql://192.168.1.12:3306/wms_db_00315 username: wms_user16 password: wms_password1718shardingRule:19 tables:20 # 库存表分片规则21 inventory:22 actualDataNodes: wms_db_00$->{1..3}.inventory_00$->{1..9}23 databaseStrategy:24 standard:25 shardingColumn: warehouse_id26 shardingAlgorithmName: warehouse_database_inline27 tableStrategy:28 standard:29 shardingColumn: goods_id 30 shardingAlgorithmName: goods_table_inline31 32 # 库存流水表分片规则 33 inventory_log:34 actualDataNodes: wms_db_00$->{1..3}.inventory_log_$->{202501..202612}35 databaseStrategy:36 standard:37 shardingColumn: warehouse_id38 shardingAlgorithmName: warehouse_database_inline39 tableStrategy:40 standard:41 shardingColumn: create_time42 shardingAlgorithmName: log_table_time4344shardingAlgorithms:45 # 仓库维度分库算法46 warehouse_database_inline:47 type: INLINE48 props:49 algorithm-expression: wms_db_00$->{(warehouse_id % 3) + 1}50 51 # 商品维度分表算法 52 goods_table_inline:53 type: INLINE54 props:55 algorithm-expression: inventory_00$->{(goods_id % 9) + 1}56 57 # 时间维度分表算法58 log_table_time:59 type: INLINE60 props:61 algorithm-expression: inventory_log_$->{create_time.format("yyyyMM")}2. 订单相关表 - 按客户ID分片
1-- 出库单表分片DDL2-- 分库:按customer_code的hash值分布到3个数据库3-- 分表:按订单创建时间按月分表45-- DB1: wms_db_0016CREATE TABLE outbound_order_202501 (7 id BIGINT(20) NOT NULL AUTO_INCREMENT,8 outbound_no VARCHAR(50) NOT NULL,9 warehouse_id BIGINT(20) NOT NULL,10 customer_code VARCHAR(50),11 create_time DATETIME DEFAULT CURRENT_TIMESTAMP,12 -- 其他字段...13 PRIMARY KEY (id),14 UNIQUE KEY uk_outbound_no (outbound_no),15 KEY idx_customer_time (customer_code, create_time),16 KEY idx_warehouse_status (warehouse_id, status)17) ENGINE=InnoDB PARTITION BY HASH(id % 8) PARTITIONS 8;1819CREATE TABLE outbound_order_202502 (20 -- 同上结构21) ENGINE=InnoDB PARTITION BY HASH(id % 8) PARTITIONS 8;2223-- 继续创建其他月份的分表...3. 完整分片表结构
1-- ================================2-- 库存表分片 (按仓库ID分库,按商品ID分表)3-- ================================45-- wms_db_001.inventory_001 (仓库ID=1,4,7... 商品ID末位=1,4,7)6CREATE TABLE inventory_001 (7 id BIGINT(20) NOT NULL AUTO_INCREMENT,8 warehouse_id BIGINT(20) NOT NULL COMMENT '分库字段',9 goods_id BIGINT(20) NOT NULL COMMENT '分表字段', 10 location_id BIGINT(20),11 batch_no VARCHAR(50),12 quantity DECIMAL(10,2) DEFAULT 0,13 lock_quantity DECIMAL(10,2) DEFAULT 0,14 available_quantity DECIMAL(10,2) GENERATED ALWAYS AS (quantity - lock_quantity),15 version INT DEFAULT 1 COMMENT '乐观锁版本号',16 status TINYINT(4) DEFAULT 1,17 create_time DATETIME DEFAULT CURRENT_TIMESTAMP,18 update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,19 PRIMARY KEY (id),20 UNIQUE KEY uk_warehouse_goods_location (warehouse_id, goods_id, location_id),21 KEY idx_goods_warehouse (goods_id, warehouse_id),22 KEY idx_location_status (location_id, status),23 KEY idx_quantity (available_quantity),24 KEY idx_update_time (update_time)25) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表分片001';2627-- wms_db_001.inventory_002 (仓库ID=1,4,7... 商品ID末位=2,5,8) 28CREATE TABLE inventory_002 (29 -- 同上结构30) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表分片002';3132-- 继续创建inventory_003到inventory_009...3334-- ================================35-- 库存流水表分片 (按仓库ID分库,按时间分表) 36-- ================================3738-- wms_db_001.inventory_log_202501 (仓库ID=1,4,7... 2025年1月)39CREATE TABLE inventory_log_202501 (40 id BIGINT(20) NOT NULL AUTO_INCREMENT,41 warehouse_id BIGINT(20) NOT NULL COMMENT '分库字段',42 goods_id BIGINT(20) NOT NULL,43 location_id BIGINT(20),44 batch_no VARCHAR(50),45 operation_type VARCHAR(20) NOT NULL,46 quantity_before DECIMAL(10,2),47 quantity_change DECIMAL(10,2) NOT NULL,48 quantity_after DECIMAL(10,2),49 business_type VARCHAR(50),50 business_no VARCHAR(50),51 operator VARCHAR(50),52 create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '分表字段',53 PRIMARY KEY (id),54 KEY idx_warehouse_goods_time (warehouse_id, goods_id, create_time),55 KEY idx_business (business_type, business_no),56 KEY idx_create_time (create_time)57) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水202501';5859-- ================================ 60-- 出库单表分片 (按客户编码分库,按时间分表)61-- ================================6263-- wms_db_001.outbound_order_202501 (客户编码hash=1,4,7... 2025年1月)64CREATE TABLE outbound_order_202501 (65 id BIGINT(20) NOT NULL AUTO_INCREMENT,66 outbound_no VARCHAR(50) NOT NULL,67 warehouse_id BIGINT(20) NOT NULL,68 customer_code VARCHAR(50) COMMENT '分库字段',69 customer_name VARCHAR(100),70 total_quantity DECIMAL(10,2) DEFAULT 0,71 status TINYINT(4) DEFAULT 1,72 priority TINYINT(4) DEFAULT 1,73 create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '分表字段',74 update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,75 PRIMARY KEY (id),76 UNIQUE KEY uk_outbound_no (outbound_no),77 KEY idx_customer_time (customer_code, create_time),78 KEY idx_warehouse_status (warehouse_id, status),79 KEY idx_status_priority_time (status, priority, create_time)80) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库单202501'81PARTITION BY HASH(id % 4) PARTITIONS 4;分片中间件配置
ShardingSphere-JDBC配置:
1@Configuration2public class ShardingDataSourceConfig {3 4 /**5 * 配置分片数据源6 */7 @Bean8 @Primary9 public DataSource shardingDataSource() throws SQLException {10 11 // 配置真实数据源12 Map<String, DataSource> dataSourceMap = createDataSourceMap();13 14 // 配置分片规则15 ShardingRuleConfiguration shardingRuleConfig = createShardingRuleConfig();16 17 // 配置属性18 Properties properties = new Properties();19 properties.setProperty("sql.show", "true");20 properties.setProperty("executor.size", "20");21 22 return ShardingDataSourceFactory.createDataSource(23 dataSourceMap, shardingRuleConfig, properties24 );25 }26 27 /**28 * 创建数据源映射29 */30 private Map<String, DataSource> createDataSourceMap() {31 Map<String, DataSource> dataSourceMap = new HashMap<>();32 33 // 配置3个分库数据源34 for (int i = 1; i <= 3; i++) {35 HikariDataSource dataSource = new HikariDataSource();36 dataSource.setJdbcUrl("jdbc:mysql://192.168.1." + (9 + i) + ":3306/wms_db_00" + i);37 dataSource.setUsername("wms_user");38 dataSource.setPassword("wms_password");39 dataSource.setMaximumPoolSize(20);40 dataSource.setMinimumIdle(5);41 dataSource.setConnectionTimeout(30000);42 43 dataSourceMap.put("wms_db_00" + i, dataSource);44 }45 46 return dataSourceMap;47 }48 49 /**50 * 创建分片规则配置51 */52 private ShardingRuleConfiguration createShardingRuleConfig() {53 ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();54 55 // 库存表分片规则56 shardingRuleConfig.getTableRuleConfigs().add(createInventoryTableRule());57 58 // 库存流水表分片规则 59 shardingRuleConfig.getTableRuleConfigs().add(createInventoryLogTableRule());60 61 // 出库单表分片规则62 shardingRuleConfig.getTableRuleConfigs().add(createOutboundOrderTableRule());63 64 // 配置分库策略65 shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(66 new InlineShardingStrategyConfiguration("warehouse_id", 67 "wms_db_00$->{(warehouse_id % 3) + 1}")68 );69 70 return shardingRuleConfig;71 }72 73 /**74 * 库存表分片规则75 */76 private TableRuleConfiguration createInventoryTableRule() {77 TableRuleConfiguration config = new TableRuleConfiguration();78 config.setLogicTable("inventory");79 config.setActualDataNodes("wms_db_00$->{1..3}.inventory_00$->{1..9}");80 81 // 分库策略:按仓库ID82 config.setDatabaseShardingStrategyConfig(83 new InlineShardingStrategyConfiguration("warehouse_id",84 "wms_db_00$->{(warehouse_id % 3) + 1}")85 );86 87 // 分表策略:按商品ID 88 config.setTableShardingStrategyConfig(89 new InlineShardingStrategyConfiguration("goods_id",90 "inventory_00$->{(goods_id % 9) + 1}")91 );92 93 return config;94 }95 96 /**97 * 自定义分片算法98 */99 @Component100 public static class CustomShardingAlgorithm implements PreciseShardingAlgorithm<Date> {101 102 @Override103 public String doSharding(Collection<String> availableTargetNames, 104 PreciseShardingValue<Date> shardingValue) {105 // 按月分表106 String monthSuffix = new SimpleDateFormat("yyyyMM").format(shardingValue.getValue());107 108 for (String tableName : availableTargetNames) {109 if (tableName.endsWith(monthSuffix)) {110 return tableName;111 }112 }113 114 throw new IllegalArgumentException("未找到匹配的分表: " + monthSuffix);115 }116 }117}分片查询优化
1. 带分片键查询(最优):
1@Service2public class InventoryShardingService {3 4 /**5 * 单分片查询 - 性能最优6 * SQL只会路由到一个分片执行7 */8 public Inventory getInventory(Long warehouseId, Long goodsId) {9 return inventoryMapper.selectByWarehouseAndGoods(warehouseId, goodsId);10 }11 12 /**13 * 批量查询同一分片数据14 */15 public List<Inventory> getInventoryByWarehouse(Long warehouseId, List<Long> goodsIds) {16 return inventoryMapper.selectByWarehouseAndGoodsList(warehouseId, goodsIds);17 }18 19 /**20 * 范围查询(需要优化)21 */22 public List<Inventory> getInventoryByQuantityRange(Long warehouseId, 23 BigDecimal minQty, BigDecimal maxQty) {24 // 带上分库键,避免跨库查询25 return inventoryMapper.selectByWarehouseAndQuantityRange(warehouseId, minQty, maxQty);26 }27}2. 跨分片查询优化:
1/**2 * 跨分片聚合查询优化3 */4@Service5public class CrossShardQueryService {6 7 @Autowired8 private List<DataSource> shardDataSources;9 10 /**11 * 并行跨分片查询12 */13 public Map<Long, BigDecimal> getTotalInventoryByGoods(Set<Long> goodsIds) {14 15 Map<Long, BigDecimal> result = new ConcurrentHashMap<>();16 17 // 并行查询所有分片18 List<CompletableFuture<Void>> futures = shardDataSources.stream()19 .map(dataSource -> CompletableFuture.runAsync(() -> {20 try (Connection conn = dataSource.getConnection()) {21 // 在每个分片上执行聚合查询22 String sql = "SELECT goods_id, SUM(quantity) as total_qty FROM inventory " +23 "WHERE goods_id IN (" + 24 goodsIds.stream().map(String::valueOf).collect(Collectors.joining(",")) + 25 ") GROUP BY goods_id";26 27 try (PreparedStatement ps = conn.prepareStatement(sql);28 ResultSet rs = ps.executeQuery()) {29 30 while (rs.next()) {31 Long goodsId = rs.getLong("goods_id"); 32 BigDecimal qty = rs.getBigDecimal("total_qty");33 34 result.merge(goodsId, qty, BigDecimal::add);35 }36 }37 } catch (SQLException e) {38 log.error("跨分片查询失败", e);39 }40 }))41 .collect(Collectors.toList());42 43 // 等待所有查询完成44 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();45 46 return result;47 }48}分布式事务处理
1/**2 * 跨分片事务处理3 */4@Service5public class CrossShardTransactionService {6 7 @Autowired8 private DataSourceTransactionManager transactionManager;9 10 /**11 * 手动管理跨分片事务12 */13 public boolean transferInventory(Long fromWarehouseId, Long toWarehouseId, 14 Long goodsId, BigDecimal quantity) {15 16 // 判断是否跨分片17 boolean crossShard = (fromWarehouseId % 3) != (toWarehouseId % 3);18 19 if (!crossShard) {20 // 同分片事务,使用本地事务21 return transferWithinShard(fromWarehouseId, toWarehouseId, goodsId, quantity);22 } else {23 // 跨分片事务,使用分布式事务24 return transferAcrossShards(fromWarehouseId, toWarehouseId, goodsId, quantity);25 }26 }27 28 @Transactional(rollbackFor = Exception.class)29 private boolean transferWithinShard(Long fromWarehouseId, Long toWarehouseId,30 Long goodsId, BigDecimal quantity) {31 // 同一分片内的转移,使用本地事务32 boolean deducted = inventoryService.deductInventory(fromWarehouseId, goodsId, quantity);33 if (deducted) {34 return inventoryService.addInventory(toWarehouseId, goodsId, quantity);35 }36 return false;37 }38 39 private boolean transferAcrossShards(Long fromWarehouseId, Long toWarehouseId,40 Long goodsId, BigDecimal quantity) {41 // 跨分片转移,使用Saga模式42 TransferSaga saga = TransferSaga.builder()43 .fromWarehouseId(fromWarehouseId)44 .toWarehouseId(toWarehouseId)45 .goodsId(goodsId)46 .quantity(quantity)47 .build();48 49 return sagaManager.execute(saga);50 }51}分片运维工具
1/**2 * 分片运维工具3 */4@Component5public class ShardingMaintenanceService {6 7 /**8 * 数据迁移 - 扩容时使用9 */10 public void migrateData(String sourceTable, String targetTable, 11 Date startDate, Date endDate) {12 log.info("开始数据迁移: {} -> {}, 时间范围: {} - {}", 13 sourceTable, targetTable, startDate, endDate);14 15 String sql = "INSERT INTO " + targetTable + 16 " SELECT * FROM " + sourceTable + 17 " WHERE create_time BETWEEN ? AND ?";18 19 try (Connection conn = dataSource.getConnection();20 PreparedStatement ps = conn.prepareStatement(sql)) {21 22 ps.setTimestamp(1, new Timestamp(startDate.getTime()));23 ps.setTimestamp(2, new Timestamp(endDate.getTime()));24 25 int rows = ps.executeUpdate();26 log.info("数据迁移完成,迁移记录数: {}", rows);27 28 } catch (SQLException e) {29 log.error("数据迁移失败", e);30 throw new RuntimeException("数据迁移失败", e);31 }32 }33 34 /**35 * 分片数据统计36 */37 public Map<String, Long> getShardStatistics() {38 Map<String, Long> stats = new HashMap<>();39 40 // 统计每个分片的数据量41 for (String tableName : Arrays.asList("inventory", "inventory_log", "outbound_order")) {42 for (int i = 1; i <= 3; i++) {43 String dbName = "wms_db_00" + i;44 Long count = getTableRowCount(dbName, tableName);45 stats.put(dbName + "." + tableName, count);46 }47 }48 49 return stats;50 }51 52 /**53 * 分片健康检查54 */55 @Scheduled(fixedRate = 300000) // 每5分钟检查一次56 public void healthCheck() {57 for (int i = 1; i <= 3; i++) {58 String dbName = "wms_db_00" + i;59 60 try {61 DataSource ds = getDataSourceByName(dbName);62 try (Connection conn = ds.getConnection()) {63 // 执行简单查询验证连通性64 conn.createStatement().execute("SELECT 1");65 log.debug("分片健康检查通过: {}", dbName);66 }67 } catch (SQLException e) {68 log.error("分片健康检查失败: {}", dbName, e);69 // 发送告警70 alertService.sendAlert("分片异常", "分片 " + dbName + " 连接失败");71 }72 }73 }74}十六、面试准备指南
16.1 完整面试回答实例
16.1.1 高并发库存扣减问题
面试官:"你们项目遇到过什么技术难点?"
完整回答:
"我们在开发WMS仓库管理系统时,遇到了一个典型的电商高并发场景问题。在双11这种大促期间,可能有数千个用户同时下单购买同一个热门商品,如果处理不当就会出现超卖现象,比如库存只有100件,但是卖出了120件。
这个问题的核心挑战在于分布式环境下的数据一致性。主要原因包括:首先是并发安全问题,多个服务实例同时读取库存都是100,然后各自减1,最终库存变成99而不是98;其次是性能瓶颈,传统的数据库锁在高并发时会成为瓶颈;第三是分布式一致性,微服务架构下多个节点之间的数据同步问题。
我们最终采用了三重保障机制来解决:
第一层是Redis分布式锁,使用Redisson实现,按商品ID作为锁的key,比如'inventory🔒商品ID',这样不同商品可以并行处理,相同商品串行处理。我们设置3秒的获锁超时和10秒的锁定时长。
第二层是数据库行锁,使用SELECT FOR UPDATE来锁定库存记录,保证在单个数据库实例内的并发安全。
第三层是乐观锁,在库存表增加version字段,更新时检查版本号是否变化,作为兜底机制。
具体的代码实现是这样的:先尝试获取分布式锁,获取成功后查询库存并加行锁,检查库存是否充足,然后用乐观锁方式更新库存,最后异步记录库存流水,并清除Redis缓存。
这套方案上线后,我们的压测数据显示:并发处理能力达到10000+QPS,完全杜绝了超卖现象,P99响应时间控制在100ms以内。在去年双11期间,系统处理了超过500万笔订单,没有出现任何库存异常。"
16.1.2 拣货路径优化问题
面试官:"还有其他技术难点吗?"
完整回答:
"另一个比较有挑战性的问题是拣货路径优化。传统仓库的拣货员往往是拿着纸质拣货单,按照直觉或经验来规划路径,这样效率很低。我们仓库有3000多个库位,一个拣货任务可能涉及30-50个库位,如何为拣货员规划最短路径是个典型的算法问题。
从算法角度来看,这是旅行商问题TSP的变种。如果用暴力枚举,时间复杂度是O(n!),30个库位就需要计算30的阶乘种可能,完全不现实。动态规划虽然能得到最优解,但时间复杂度是O(n²×2ⁿ),对于实时拣货任务来说太慢了。
我们最终选择了改进的贪心算法。核心思路是:从库区入口开始,每次都选择距离当前位置最近且还没访问过的库位。但仓库不是开阔地带,而是有通道限制的,所以我们用曼哈顿距离而不是欧式距离来计算,公式是横向距离+纵向距离+层间距离×2,层间距离权重更高是因为上下楼梯比水平移动更费时。
为了进一步优化,我们还加入了2-opt算法来消除路径交叉。就是检查路径中是否有交叉点,如果把两条边换一下能让总距离更短,就执行这个交换。
这套算法的时间复杂度是O(n²),对于50个库位的计算时间在10毫秒以内,完全满足实时性要求。
上线后的效果非常明显:平均拣货路径从450米缩短到280米,下降了38%;拣货时间从25分钟减少到16分钟;拣货员的日产能从180单提升到280单,整整提高了56%。我们还做了A/B测试,使用优化算法的拣货员比不使用的效率平均高出一倍以上。"
16.1.3 微服务架构设计问题
面试官:"为什么要用微服务架构,怎么解决服务间的数据一致性?"
完整回答:
"我们选择微服务架构主要基于三个考虑:
首先是业务复杂度。WMS系统涉及入库、出库、拣货、盘点等多个业务域,每个域的逻辑都很复杂,如果做成单体应用,代码耦合严重,维护困难。拆分成微服务后,每个服务专注自己的业务领域,符合单一职责原则。
其次是团队协作。我们有6个开发人员,按业务域分成3个小组,每组负责2个服务。微服务让各组可以独立开发、测试、部署,大大提高了并行开发效率。
第三是技术选型灵活性。比如报表服务需要复杂的数据分析,我们用了ElasticSearch;拣货服务需要地理位置计算,我们集成了专门的算法库。单体应用很难做到这种技术异构。
关于数据一致性,我们采用了分阶段的策略:
对于强一致性要求的场景,比如库存扣减,我们用分布式锁来保证。
对于最终一致性可以接受的场景,比如出库流程,我们用Saga模式。整个流程涉及4个步骤:锁定库存、更新订单状态、创建运单、记录成本。每个步骤都定义了补偿操作,如果某一步失败,会自动执行前面所有步骤的补偿操作来回滚。
比如锁定库存成功了,但创建运单失败,系统会自动执行解锁库存和恢复订单状态的补偿操作。
我们还大量使用了消息队列来实现异步解耦。比如库存变更后,会发MQ消息通知报表服务更新统计数据,通知预警服务检查安全库存等。这样既保证了数据最终一致,又避免了同步调用的性能问题。
整个架构上线一年多,系统可用性达到99.9%以上,单个服务的故障不会影响整体业务,扩容也非常方便。"
16.2 重点准备话题
16.2.1 性能优化问题
面试官:"系统的性能怎么样?你们是怎么做性能优化的?"
完整回答:
"我们的WMS系统承载着每天几十万笔业务操作,性能要求很高。我主要从四个方面做了优化:
缓存优化:我们设计了三级缓存架构。L1是本地缓存,用Caffeine实现,主要缓存商品基础信息这种几乎不变的数据;L2是Redis分布式缓存,缓存库存数据这种会变但不是实时变化的数据;L3是数据库缓存。这样设计的好处是热点数据可以就近获取,大大减少网络开销。
数据库优化:首先是分库分表,库存表按仓库ID分片,库存流水表按时间分片。其次是索引优化,比如库存查询经常按商品ID和库位ID查,我们建了联合索引。还有就是读写分离,报表查询走从库,业务操作走主库。
异步处理:像库存流水记录、报表统计更新这些不影响主流程的操作,我们都改成了异步处理。使用RocketMQ消息队列,既提高了响应速度,也提高了系统的解耦性。
连接池优化:数据库连接池、Redis连接池都做了精细化配置。数据库连接池我们设置了20个核心连接,最大50个连接,根据业务高峰期的并发量计算出来的。
优化后的效果很明显:库存查询接口的P99响应时间从300ms降到50ms,库存扣减接口支持10000+QPS,整体系统吞吐量提升了3倍。在去年双11压测中,我们模拟了100万并发用户,系统表现非常稳定。"
16.2.2 异常处理与监控
面试官:"系统出现异常怎么处理?你们的监控是怎么做的?"
完整回答:
"异常处理我们分为业务异常和技术异常两类:
业务异常处理:比如拣货时发现库位空缺,我们设计了自动恢复机制。系统会先查找同批次的其他库位,如果找到就自动切换过去;如果找不到就转人工处理,同时触发紧急盘点任务。这样既保证了业务连续性,也及时发现了数据问题。
技术异常处理:我们用了熔断降级机制。比如报表服务如果响应超时,会自动熔断,返回缓存数据或者默认数据,不会影响核心业务。用的是Sentinel组件,可以设置不同的熔断策略。
监控体系:我们建立了三层监控。
第一层是基础监控,用Prometheus收集CPU、内存、网络等指标,Grafana做可视化展示。
第二层是应用监控,用SkyWalking做APM,可以看到每个接口的调用链路、响应时间、错误率。特别是分布式环境下,一个请求可能经过多个服务,SkyWalking能完整还原整个调用路径。
第三层是业务监控,我们自定义了一些业务指标,比如库存准确率、拣货效率、订单处理时效等,这些指标直接反映系统的业务健康度。
告警机制:设置了多级告警,P0级别是影响核心业务的,会立即电话通知;P1级别是性能问题,会发短信;P2级别是一般异常,会发钉钉消息。我们还建立了值班制度,确保24小时有人响应。
去年一年,我们的系统可用性达到99.95%,平均故障恢复时间MTTR控制在15分钟以内。"
16.2.3 数据一致性保障
面试官:"分布式系统怎么保证数据一致性?"
完整回答:
"数据一致性是分布式系统的核心挑战,我们根据不同场景采用了不同策略:
强一致性场景:比如库存扣减,我们用分布式锁+数据库事务来保证。具体流程是先获取Redis分布式锁,然后在数据库事务内完成库存检查和扣减,最后释放锁。这样可以保证任何时刻库存数据都是准确的。
最终一致性场景:比如出库流程,涉及库存服务、订单服务、物流服务、财务服务四个系统。我们用Saga模式来处理,定义了四个步骤和对应的补偿操作。如果任何一步失败,会自动执行补偿操作回滚前面的步骤。
异步一致性场景:比如报表统计,不要求实时准确,我们用消息队列来实现。库存发生变化时发送MQ消息,报表服务消费消息后更新统计数据。即使消息延迟或重复,最终数据都会是一致的。
幂等性保障:所有的写操作都设计成幂等的,比如库存扣减操作,我们会记录操作流水号,同一个流水号的操作只会执行一次。这样即使网络重试也不会造成数据错误。
数据对账机制:我们每天凌晨会做全量数据对账,比较系统库存和实际盘点数据,发现差异会自动报警。还会定期做业务数据的交叉验证,比如库存变更总量应该等于入库总量减去出库总量。
通过这套机制,我们的数据准确率达到99.9%以上,即使在网络异常或服务故障的情况下,也能保证数据不会出现不一致的问题。"
16.3 项目核心价值与成果
面试官:"这个项目最终带来了什么价值?"
完整回答:
"我们的WMS系统上线后,给公司带来了显著的业务价值:
运营效率提升:
- 拣货效率提高56%,原来拣货员一天处理180单,现在能处理280单
- 库存准确率达到99.9%,基本消除了人工盘点的工作量
- 订单处理时效提升35%,客户满意度明显改善
成本节约:
- 人员成本降低30%,原来需要20个拣货员,现在12个就够了
- 库存周转率提升40%,减少了资金占用
- 错误率下降90%,大幅减少了退货和赔偿成本
业务支撑能力:
- 系统支持10000+QPS并发,为业务快速增长提供了技术保障
- 微服务架构让新功能开发速度提升50%
- 完善的监控体系让系统可用性达到99.95%
技术沉淀:
- 积累了分布式高并发的实战经验
- 形成了一套完整的WMS解决方案,可以复制到其他仓库
- 培养了团队的技术能力,为后续项目奠定了基础"
16.4 深度技术追问应对
Q: "为什么选择贪心算法而不是更精确的算法?"
完整回答:
"这是一个工程实践中的经典权衡问题。理论上,动态规划或者遗传算法确实能找到更优解,但在实际业务场景下:
首先是实时性要求,拣货员扫描波次二维码后,需要在1秒内看到拣货路径,用户体验不能妥协。动态规划的时间复杂度是O(n²×2ⁿ),50个库位需要计算几十秒,完全不可接受。
其次是优化效果的边际递减。我们做过测试,贪心算法能获得理论最优解的80-90%效果,而从90%提升到95%需要付出10倍的计算代价,从95%提升到100%可能需要100倍的计算代价,这在商业上是不划算的。
第三是算法的可维护性。贪心算法逻辑清晰,团队成员都能理解和维护。复杂算法虽然效果好,但如果只有我一个人懂,风险太大。
最后,我们还加了2-opt优化作为补充,这样既保证了实时性,又在可接受的计算成本下进一步提升了效果。实际运行数据表明,这个选择是正确的。"
Q: "微服务的数据库设计有什么坑?"
完整回答:
"确实踩过几个坑,分享一下经验:
第一个坑是外键关联。刚开始我们想保持数据一致性,在不同服务的表之间还保留了外键约束,结果发现部署和维护极其困难。后来彻底去掉了跨服务的外键,改用业务层面的关联检查。
第二个坑是分布式事务滥用。一开始什么操作都想用分布式事务保证一致性,结果系统性能很差,还经常出现死锁。后来我们重新梳理了业务场景,大部分改成了最终一致性,只在核心场景保留强一致性。
第三个坑是数据冗余设计不当。比如订单表里冗余了商品名称,但商品改名后订单里还是老名字,造成了数据混乱。后来我们建立了明确的冗余数据管理规范,哪些字段可以冗余,什么时候同步更新,都有明确定义。
第四个坑是ID生成策略。不同服务用不同的ID生成器,有的用自增ID,有的用UUID,有的用雪花算法,结果在做数据关联时各种问题。后来统一用雪花算法,既保证了全局唯一,又保持了趋势递增。
这些坑让我们深刻理解了'微服务不是银弹'这句话,技术选型一定要结合具体业务场景。"
Q: "如果让你重新设计这个系统,你会怎么改进?"
完整回答:
"经过一年多的运行,我确实发现了一些可以改进的地方:
架构层面:我会考虑引入事件驱动架构。现在我们的服务间调用还是比较紧耦合,如果用事件驱动,各个服务只需要关注自己的业务领域事件,系统会更加松耦合。
数据层面:我会引入CQRS模式,读写分离做得更彻底。现在报表查询还是会对业务数据库造成一定压力,如果有专门的查询数据库,性能会更好。
算法层面:我会尝试引入机器学习。比如库存预测,现在还是基于简单的统计规则,如果用机器学习分析历史数据,预测会更准确。拣货路径也可以用强化学习来优化。
运维层面:我会加强自动化运维。现在还有一些人工操作,比如容量扩展、故障恢复等,如果能完全自动化,系统的可靠性会更高。
业务层面:我会考虑加入更多的智能化功能,比如智能补货建议、异常预警、自动化盘点等,让系统从单纯的管理工具变成智能决策助手。
当然,这些改进都需要考虑投入产出比,不是所有的技术都适合现在就上。技术服务于业务,这是我在这个项目中最大的收获。"
文档完成 ✅
本文档全面介绍了WMS仓库管理系统的设计方案、技术难点和面试准备策略,为开发和求职提供完整的参考指南。
评论