Skip to main content

WMS 仓库管理系统设计(基于RuoYi-Pro-Mini)

📌 重要说明:基于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 技术架构

架构设计思想

为什么采用微服务架构?

  1. 业务解耦:仓储业务复杂度高,入库、出库、拣货等模块独立性强,微服务化便于团队协作和独立演进
  2. 弹性扩展:库存查询、拣货任务等高并发场景可独立扩展,避免资源浪费
  3. 故障隔离:某个服务故障不影响整体系统,提高可用性
  4. 技术异构:报表服务可使用ElasticSearch,日志服务可用MongoDB,各取所长

核心组件选型说明

组件类型技术选型选型理由替代方案
服务注册Nacos国产化、配置中心集成、社区活跃Consul, Eureka
网关Spring Cloud Gateway异步非阻塞、性能优秀、Spring生态Zuul 2.0, Kong
负载均衡Spring Cloud LoadBalancer轻量级、可定制Ribbon(已停更)
服务调用OpenFeign + OkHttp声明式、可读性强、支持HTTP/2Dubbo, gRPC
熔断限流Sentinel实时监控、规则丰富、国产化Hystrix(停更), Resilience4j
链路追踪SkyWalkingAPM全栈、国产、无侵入Zipkin, Jaeger
消息队列RocketMQ顺序消息、事务消息、高吞吐Kafka, RabbitMQ
数据库MySQL 8.0ACID保证、成熟稳定、生态完善PostgreSQL, TiDB
缓存Redis 7.0高性能、数据结构丰富、分布式锁Memcached, Hazelcast
搜索引擎ElasticSearch全文检索、日志分析、实时聚合Solr, OpenSearch
对象存储MinIO兼容S3、部署简单、私有化FastDFS, OSS
定时任务XXL-Job分布式、可视化、失败重试Quartz, Elastic-Job

1.5 技术栈

后端技术栈

xml
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>

前端技术栈

json
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的理由:

  1. 提高库存利用率:订单支付前库存仍可销售(设置锁定时效)
  2. 防止超卖:通过Redis分布式锁保证原子性
  3. 支持灵活策略:可配置锁定时长、自动释放规则

2.2.4 并发库存扣减方案

核心挑战: 高并发场景下如何保证库存扣减的准确性和性能?

java
1/**
2 * 库存扣减 - 三重保障机制
3 * 1. Redis分布式锁:保证同一商品同一时刻只有一个线程操作
4 * 2. 数据库行锁(FOR UPDATE):保证数据库层面的并发安全
5 * 3. 乐观锁(版本号):作为兜底机制
6 */
7@Override
8@Transactional(rollbackFor = Exception.class)
9public boolean deductInventory(Long goodsId, BigDecimal quantity) {
10 // 分布式锁Key
11 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%⚠️ 警告扩容/清理
java
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 库存快照与对账

为什么需要库存快照?

  • 数据追溯:出现库存差异时快速定位问题时间点
  • 报表统计:月末/年末库存报表生成
  • 审计合规:满足财务审计要求
java
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}
27
28/**
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%

我们采用"波次拣货+批次拣货"组合策略的原因:

  1. 订单聚合:将时间窗口内的订单聚合成波次,一次性处理
  2. 路径优化:波次内统一规划拣货路径,减少重复行走
  3. 人员均衡:根据拣货员位置和工作量智能分配任务
  4. 灵活调整:支持紧急订单插入、优先级调整

2.5.2 波次生成算法

核心目标: 在满足时效要求的前提下,最大化拣货效率

java
1/**
2 * 波次生成策略
3 * 考虑因素:订单优先级、商品位置、拣货员状态、截单时间
4 */
5@Service
6public 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(Comparator
18 .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%的优化效果已能显著提升效率
  • 实现简单:便于维护和调整
java
1/**
2 * 拣货路径优化 - 改进的贪心算法
3 * 从库区入口开始,每次选择距离当前位置最近且未访问的库位
4 */
5@Service
6public 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 拣货任务分配策略

目标: 实现拣货员工作负载均衡,提高整体效率

java
1/**
2 * 拣货任务分配 - 综合评分算法
3 * 考虑因素:拣货员当前位置、工作负载、技能等级、任务优先级
4 */
5@Service
6public 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-100
60 score += efficiencyScore * 0.1;
61
62 return score;
63 }
64}

2.5.5 拣货异常处理

常见异常场景及处理方案:

异常类型触发条件处理方案是否需要人工介入
库存不足实际库存 < 待拣数量自动减单或转采购⚠️ 需确认
商品破损质检发现问题标记残次、寻找替代库位✅ 需介入
库位空缺扫描库位无货触发盘点任务、查找其他批次✅ 需介入
拣货超时执行时间 > 预估时间×1.5发送提醒、触发协助请求⚠️ 视情况
拣错商品复核发现SKU不符回退重拣、记录错误率❌ 自动处理
java
1/**
2 * 拣货异常处理服务
3 */
4@Service
5public 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 = inventoryService
26 .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 完整建表语句

sql
1-- ================================
2-- WMS 仓库管理系统数据库
3-- ================================
4
5CREATE DATABASE IF NOT EXISTS `wms` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
6USE `wms`;
7
8-- ================================
9-- 1. 仓库基础表
10-- ================================
11
12-- 仓库表
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='仓库表';
32
33-- 库区表
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='库区表';
50
51-- 库位表
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='库位表';
72
73-- ================================
74-- 2. 商品管理表
75-- ================================
76
77-- 商品分类表
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='商品分类表';
90
91-- 商品信息表
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='商品信息表';
120
121-- ================================
122-- 3. 库存管理表
123-- ================================
124
125-- 库存表
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='库存表';
151
152-- 库存流水表
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='库存流水表';
173
174-- ================================
175-- 4. 入库管理表
176-- ================================
177
178-- 入库单表
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='入库单表';
204
205-- 入库单明细表
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='入库单明细表';
228
229-- ================================
230-- 5. 出库管理表
231-- ================================
232
233-- 出库单表
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='出库单表';
274
275-- 出库单明细表
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='出库单明细表';
298
299-- ================================
300-- 6. 拣货管理表
301-- ================================
302
303-- 拣货波次表
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='拣货波次表';
330
331-- 拣货波次订单关联表
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='拣货波次订单关联表';
344
345-- 拣货任务表
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='拣货任务表';
380
381-- ================================
382-- 7. 盘点管理表
383-- ================================
384
385-- 盘点计划表
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='盘点计划表';
414
415-- 盘点单表
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='盘点单表';
447
448-- ================================
449-- 8. 库位调整表
450-- ================================
451
452-- 移库单表
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='移库单表';
482
483-- ================================
484-- 9. 供应商/客户管理表
485-- ================================
486
487-- 供应商表
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='供应商表';
511
512-- 客户表
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='客户表';
539
540-- 承运商表
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='承运商表';
563
564-- ================================
565-- 10. 系统配置表
566-- ================================
567
568-- 数据字典表
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='数据字典表';
588
589-- 用户表
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='用户表';
614
615-- 角色表
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='角色表';
630
631-- 菜单表
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='菜单表';
652
653-- 用户角色关联表
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='用户角色关联表';
664
665-- 角色菜单关联表
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='角色菜单关联表';
676
677-- 操作日志表
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='操作日志表';
703
704---
705
706## 四、核心业务实现
707
708### 4.1 库存服务实现
709
710#### 实体类定义
711
712```java
713package com.wms.inventory.entity;
714
715import com.baomidou.mybatisplus.annotation.*;
716import lombok.Data;
717import java.math.BigDecimal;
718import java.time.LocalDate;
719import java.time.LocalDateTime;
720
721/**
722 * 库存实体
723 */
724@Data
725@TableName("inventory")
726public class Inventory {
727
728 @TableId(type = IdType.AUTO)
729 private Long id;
730
731 /**
732 * 仓库ID
733 */
734 private Long warehouseId;
735
736 /**
737 * 库位ID
738 */
739 private Long locationId;
740
741 /**
742 * 商品ID
743 */
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 * 供应商ID
788 */
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}

库存服务接口

java
1package com.wms.inventory.service;
2
3import 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;
8
9/**
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}

库存服务实现(核心逻辑)

java
1package com.wms.inventory.service.impl;
2
3import 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;
14
15import java.math.BigDecimal;
16import java.time.LocalDateTime;
17import java.util.concurrent.TimeUnit;
18
19/**
20 * 库存服务实现
21 */
22@Slf4j
23@Service
24@RequiredArgsConstructor
25public 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 @Override
38 @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 @Override
96 @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 @Override
144 @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 出库服务实现

出库订单实体

java
1package com.wms.outbound.entity;
2
3import com.baomidou.mybatisplus.annotation.*;
4import lombok.Data;
5import java.math.BigDecimal;
6import java.time.LocalDateTime;
7
8@Data
9@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}

出库服务实现

java
1package com.wms.outbound.service.impl;
2
3import 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;
11
12/**
13 * 出库服务实现
14 */
15@Slf4j
16@Service
17@RequiredArgsConstructor
18public 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 拣货服务实现

拣货路径优化算法

java
1package com.wms.picking.service;
2
3import lombok.Data;
4import java.util.*;
5
6/**
7 * 拣货路径优化服务
8 */
9@Service
10public 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}
68
69@Data
70class 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 库存管理页面

vue
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-option
15 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>
36
37 <!-- 库存预警提示 -->
38 <el-alert
39 v-if="alertCount > 0"
40 title="库存预警"
41 type="warning"
42 :description="`当前有 ${alertCount} 个商品库存不足,请及时补货`"
43 show-icon
44 :closable="false"
45 :style="{ margin: '20px 0' }"
46 />
47
48 <!-- 数据表格 -->
49 <el-card class="table-card">
50 <vxe-table
51 ref="tableRef"
52 :data="tableData"
53 :loading="loading"
54 border
55 stripe
56 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>
96
97 <!-- 分页 -->
98 <el-pagination
99 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>
110
111<script setup>
112import { ref, reactive, onMounted } from 'vue'
113import { ElMessage, ElMessageBox } from 'element-plus'
114import { getInventoryList, freezeInventory } from '@/api/inventory'
115
116// 搜索表单
117const searchForm = reactive({
118 goodsName: '',
119 skuCode: '',
120 warehouseId: null,
121 status: null
122})
123
124// 表格数据
125const tableData = ref([])
126const loading = ref(false)
127const alertCount = ref(0)
128
129// 分页
130const pagination = reactive({
131 page: 1,
132 size: 20,
133 total: 0
134})
135
136// 查询数据
137const handleSearch = async () => {
138 loading.value = true
139 try {
140 const params = {
141 ...searchForm,
142 page: pagination.page,
143 size: pagination.size
144 }
145 const { data } = await getInventoryList(params)
146 tableData.value = data.records
147 pagination.total = data.total
148 alertCount.value = data.alertCount || 0
149 } catch (error) {
150 ElMessage.error('查询失败')
151 } finally {
152 loading.value = false
153 }
154}
155
156// 重置
157const handleReset = () => {
158 Object.assign(searchForm, {
159 goodsName: '',
160 skuCode: '',
161 warehouseId: null,
162 status: null
163 })
164 handleSearch()
165}
166
167// 库存标签类型
168const getStockTagType = (row) => {
169 if (row.availableQuantity <= 0) return 'danger'
170 if (row.availableQuantity < row.safetyStock) return 'warning'
171 return 'success'
172}
173
174// 状态类型
175const getStatusType = (status) => {
176 const map = { 1: 'success', 2: 'danger', 3: 'warning', 4: 'info' }
177 return map[status] || ''
178}
179
180const getStatusText = (status) => {
181 const map = { 1: '正常', 2: '冻结', 3: '待检', 4: '损坏' }
182 return map[status] || ''
183}
184
185// 冻结库存
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}
200
201onMounted(() => {
202 handleSearch()
203})
204</script>
205
206<style scoped>
207.inventory-container {
208 padding: 20px;
209}
210
211.search-card {
212 margin-bottom: 20px;
213}
214
215.text-danger {
216 color: #f56c6c;
217 font-weight: bold;
218}
219</style>

5.2 入库管理页面(流程图)

5.3 仓库大屏监控

vue
1<template>
2 <div class="warehouse-screen">
3 <div class="screen-header">
4 <h1>🏢 智能仓库监控大屏</h1>
5 <div class="datetime">{{ currentTime }}</div>
6 </div>
7
8 <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>
21
22 <!-- 图表区域 -->
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>
37
38 <!-- 实时任务 -->
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>
58
59<script setup>
60import { ref, onMounted, onUnmounted } from 'vue'
61import * as echarts from 'echarts'
62import dayjs from 'dayjs'
63
64const currentTime = ref(dayjs().format('YYYY-MM-DD HH:mm:ss'))
65
66// 统计数据
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])
73
74// ECharts 图表初始化
75const trendChart = ref(null)
76const pieChart = ref(null)
77const barChart = ref(null)
78
79const 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 })
92
93 // 饼图
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 })
108
109 // 柱状图
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}
122
123onMounted(() => {
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>
133
134<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}
143
144.screen-header {
145 display: flex;
146 justify-content: space-between;
147 align-items: center;
148 margin-bottom: 30px;
149}
150
151.stats-row {
152 display: grid;
153 grid-template-columns: repeat(4, 1fr);
154 gap: 20px;
155 margin-bottom: 30px;
156}
157
158.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}
166
167.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}
177
178.stat-value {
179 font-size: 32px;
180 font-weight: bold;
181}
182
183.charts-row {
184 display: grid;
185 grid-template-columns: repeat(3, 1fr);
186 gap: 20px;
187 margin-bottom: 30px;
188}
189
190.chart-container {
191 background: rgba(255, 255, 255, 0.1);
192 backdrop-filter: blur(10px);
193 border-radius: 10px;
194 padding: 20px;
195}
196
197.chart {
198 height: 300px;
199}
200</style>

六、部署方案

6.1 微服务架构部署

yaml
1# docker-compose.yml
2version: '3.8'
3
4services:
5 # Nacos 注册中心
6 nacos:
7 image: nacos/nacos-server:v2.2.3
8 container_name: wms-nacos
9 environment:
10 - MODE=standalone
11 - SPRING_DATASOURCE_PLATFORM=mysql
12 - MYSQL_SERVICE_HOST=mysql
13 - MYSQL_SERVICE_DB_NAME=nacos
14 - MYSQL_SERVICE_USER=root
15 - MYSQL_SERVICE_PASSWORD=123456
16 ports:
17 - "8848:8848"
18 - "9848:9848"
19 networks:
20 - wms-network
21 depends_on:
22 - mysql
23
24 # MySQL 数据库
25 mysql:
26 image: mysql:8.0
27 container_name: wms-mysql
28 environment:
29 - MYSQL_ROOT_PASSWORD=123456
30 - MYSQL_DATABASE=wms
31 ports:
32 - "3306:3306"
33 volumes:
34 - mysql-data:/var/lib/mysql
35 - ./init-sql:/docker-entrypoint-initdb.d
36 networks:
37 - wms-network
38
39 # Redis 缓存
40 redis:
41 image: redis:7.0-alpine
42 container_name: wms-redis
43 command: redis-server --requirepass 123456
44 ports:
45 - "6379:6379"
46 networks:
47 - wms-network
48
49 # RocketMQ NameServer
50 rocketmq-nameserver:
51 image: apache/rocketmq:5.1.0
52 container_name: wms-rocketmq-nameserver
53 command: sh mqnamesrv
54 ports:
55 - "9876:9876"
56 networks:
57 - wms-network
58
59 # RocketMQ Broker
60 rocketmq-broker:
61 image: apache/rocketmq:5.1.0
62 container_name: wms-rocketmq-broker
63 command: sh mqbroker -n rocketmq-nameserver:9876
64 ports:
65 - "10909:10909"
66 - "10911:10911"
67 environment:
68 - NAMESRV_ADDR=rocketmq-nameserver:9876
69 depends_on:
70 - rocketmq-nameserver
71 networks:
72 - wms-network
73
74 # API 网关
75 gateway:
76 image: wms/gateway:latest
77 container_name: wms-gateway
78 ports:
79 - "8080:8080"
80 environment:
81 - NACOS_SERVER=nacos:8848
82 - REDIS_HOST=redis
83 depends_on:
84 - nacos
85 - redis
86 networks:
87 - wms-network
88
89 # 库存服务
90 inventory-service:
91 image: wms/inventory-service:latest
92 container_name: wms-inventory-service
93 environment:
94 - NACOS_SERVER=nacos:8848
95 - MYSQL_HOST=mysql
96 - REDIS_HOST=redis
97 depends_on:
98 - nacos
99 - mysql
100 - redis
101 networks:
102 - wms-network
103 deploy:
104 replicas: 2
105
106 # 入库服务
107 inbound-service:
108 image: wms/inbound-service:latest
109 container_name: wms-inbound-service
110 environment:
111 - NACOS_SERVER=nacos:8848
112 - MYSQL_HOST=mysql
113 - ROCKETMQ_NAMESRV=rocketmq-nameserver:9876
114 depends_on:
115 - nacos
116 - mysql
117 - rocketmq-nameserver
118 networks:
119 - wms-network
120
121 # 出库服务
122 outbound-service:
123 image: wms/outbound-service:latest
124 container_name: wms-outbound-service
125 environment:
126 - NACOS_SERVER=nacos:8848
127 - MYSQL_HOST=mysql
128 - ROCKETMQ_NAMESRV=rocketmq-nameserver:9876
129 depends_on:
130 - nacos
131 - mysql
132 - rocketmq-nameserver
133 networks:
134 - wms-network
135
136networks:
137 wms-network:
138 driver: bridge
139
140volumes:
141 mysql-data:

6.2 Kubernetes 部署配置

yaml
1# k8s-deployment.yaml
2---
3apiVersion: apps/v1
4kind: Deployment
5metadata:
6 name: inventory-service
7 namespace: wms
8spec:
9 replicas: 3
10 selector:
11 matchLabels:
12 app: inventory-service
13 template:
14 metadata:
15 labels:
16 app: inventory-service
17 spec:
18 containers:
19 - name: inventory-service
20 image: wms/inventory-service:v1.0.0
21 ports:
22 - containerPort: 8081
23 env:
24 - name: NACOS_SERVER
25 value: "nacos.wms.svc.cluster.local:8848"
26 - name: MYSQL_HOST
27 value: "mysql.wms.svc.cluster.local"
28 - name: REDIS_HOST
29 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/health
40 port: 8081
41 initialDelaySeconds: 60
42 periodSeconds: 10
43 readinessProbe:
44 httpGet:
45 path: /actuator/health/readiness
46 port: 8081
47 initialDelaySeconds: 30
48 periodSeconds: 5
49---
50apiVersion: v1
51kind: Service
52metadata:
53 name: inventory-service
54 namespace: wms
55spec:
56 selector:
57 app: inventory-service
58 ports:
59 - protocol: TCP
60 port: 8081
61 targetPort: 8081
62 type: ClusterIP
63---
64apiVersion: autoscaling/v2
65kind: HorizontalPodAutoscaler
66metadata:
67 name: inventory-service-hpa
68 namespace: wms
69spec:
70 scaleTargetRef:
71 apiVersion: apps/v1
72 kind: Deployment
73 name: inventory-service
74 minReplicas: 2
75 maxReplicas: 10
76 metrics:
77 - type: Resource
78 resource:
79 name: cpu
80 target:
81 type: Utilization
82 averageUtilization: 70
83 - type: Resource
84 resource:
85 name: memory
86 target:
87 type: Utilization
88 averageUtilization: 80

6.3 监控配置(Prometheus + Grafana)

yaml
1# prometheus.yml
2global:
3 scrape_interval: 15s
4 evaluation_interval: 15s
5
6scrape_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: instance
19 regex: '([^:]+)(:[0-9]+)?'
20 replacement: '${1}'
21
22 # MySQL 监控
23 - job_name: 'mysql'
24 static_configs:
25 - targets: ['mysql-exporter:9104']
26
27 # Redis 监控
28 - job_name: 'redis'
29 static_configs:
30 - targets: ['redis-exporter:9121']

七、性能优化

7.1 数据库优化

7.1.1 分库分表策略

为什么需要分库分表?

问题数据量阈值影响解决方案
单表数据过大> 500万行查询慢、索引失效分表
并发写入瓶颈> 5000 TPS锁等待、性能下降分库
历史数据冗余增长无限制磁盘占用、备份慢归档

我们的分表策略:

java
1/**
2 * 库存流水表分表策略
3 * 按月份分表: inventory_log_202401, inventory_log_202402 ...
4 *
5 * 选择月份分表的原因:
6 * 1. 流水数据按时间查询居多,月份分表查询效率高
7 * 2. 每月数据量可控(约100-200万条)
8 * 3. 便于历史数据归档(超过12个月自动归档到冷存储)
9 */
10@Component
11public class InventoryLogShardingAlgorithm implements StandardShardingAlgorithm<LocalDateTime> {
12
13 @Override
14 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 @Override
22 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}

配置示例:

yaml
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_time
13 sharding-algorithm-name: inventory-log-sharding
14 sharding-algorithms:
15 inventory-log-sharding:
16 type: CLASS_BASED
17 props:
18 strategy: STANDARD
19 algorithm-class-name: com.wms.config.InventoryLogShardingAlgorithm

7.1.2 索引优化

索引设计原则:

  1. 覆盖索引优先:查询字段全部在索引中,避免回表
  2. 最左前缀匹配:组合索引按查询频率排列
  3. 避免过度索引:每个索引都有维护成本
  4. 定期分析索引:删除未使用的索引
sql
1-- ========================================
2-- 核心索引设计
3-- ========================================
4
5-- 1. 库存表组合索引(覆盖常用查询)
6-- 查询场景:按仓库、商品、批次查询库存
7CREATE INDEX idx_warehouse_goods_batch
8ON inventory(warehouse_id, goods_id, batch_no, quantity, lock_quantity);
9
10-- 2. 库存表状态索引
11-- 查询场景:查询异常库存、冻结库存
12CREATE INDEX idx_status_goods
13ON inventory(status, goods_id) WHERE status != 1;
14
15-- 3. 出库单状态 + 创建时间索引
16-- 查询场景:查询待处理订单、今日订单
17CREATE INDEX idx_status_create_time
18ON outbound_order(status, create_time DESC)
19WHERE status IN (1, 2, 3); -- 只索引未完成状态
20
21-- 4. 拣货任务分配索引
22-- 查询场景:查找可分配任务
23CREATE INDEX idx_warehouse_status_priority
24ON picking_task(warehouse_id, status, priority DESC, create_time)
25WHERE status = 1; -- 只索引待分配状态
26
27-- 5. 库存流水时间范围查询
28-- 查询场景:统计某段时间的出入库记录
29CREATE INDEX idx_create_time_operation
30ON inventory_log(create_time, operation_type, quantity_change);
31
32-- 6. 商品名称全文索引(用于搜索)
33CREATE FULLTEXT INDEX idx_goods_name_fulltext
34ON goods(goods_name, brand, model);
35
36-- ========================================
37-- 索引监控与优化
38-- ========================================
39
40-- 查看未使用的索引
41SELECT
42 t.table_schema,
43 t.table_name,
44 s.index_name,
45 s.rows_read,
46 s.rows_inserted
47FROM information_schema.tables t
48LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage s
49 ON t.table_name = s.object_name
50WHERE t.table_schema = 'wms'
51 AND s.index_name IS NOT NULL
52 AND s.index_name != 'PRIMARY'
53 AND s.rows_read = 0
54ORDER BY t.table_name;
55
56-- 查看索引使用频率
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_ms
63FROM performance_schema.table_io_waits_summary_by_index_usage
64WHERE object_schema = 'wms'
65ORDER BY sum_timer_wait DESC
66LIMIT 20;

索引优化效果:

优化项优化前优化后提升
库存查询响应时间450ms35ms↓ 92%
订单列表查询1.2s180ms↓ 85%
拣货任务分配680ms45ms↓ 93%
索引数量28个18个↓ 36%

7.1.3 SQL优化实践

慢查询优化案例:

sql
1-- ❌ 优化前:查询某商品的可用库存总量
2-- 问题:全表扫描,未使用索引
3SELECT SUM(quantity - lock_quantity) AS available_qty
4FROM inventory
5WHERE goods_id = 12345
6 AND status = 1;
7
8-- 执行时间:850ms(扫描50万行)
9
10-- ✅ 优化后:添加索引 + 使用覆盖索引
11CREATE INDEX idx_goods_status_qty
12ON inventory(goods_id, status, quantity, lock_quantity);
13
14-- 执行时间:15ms(只扫描相关行)
15
16
17-- ❌ 优化前:分页查询订单列表(偏移量大时性能差)
18SELECT * FROM outbound_order
19WHERE status = 2
20ORDER BY create_time DESC
21LIMIT 10000, 20;
22
23-- 执行时间:2.3s(需要跳过10000条记录)
24
25-- ✅ 优化后:使用延迟关联 + 子查询优化
26SELECT o.* FROM outbound_order o
27INNER JOIN (
28 SELECT id FROM outbound_order
29 WHERE status = 2
30 ORDER BY create_time DESC
31 LIMIT 10000, 20
32) AS t ON o.id = t.id;
33
34-- 执行时间:180ms(子查询只查ID,减少数据传输)
35
36
37-- ❌ 优化前:统计每个商品的库存分布(N+1查询)
38-- Java代码中循环查询,产生大量SQL
39for (Goods goods : goodsList) {
40 List<Inventory> invs = inventoryMapper.selectByGoodsId(goods.getId());
41 // ... 处理
42}
43
44-- 执行时间:1000 * 50ms = 50秒
45
46-- ✅ 优化后:批量查询 + JOIN
47SELECT g.id, g.goods_name, i.warehouse_id, SUM(i.quantity) AS total_qty
48FROM goods g
49LEFT JOIN inventory i ON g.id = i.goods_id
50WHERE g.id IN (1, 2, 3, ..., 1000)
51 AND i.status = 1
52GROUP BY g.id, i.warehouse_id;
53
54-- 执行时间:350ms

7.2 缓存策略

java
1/**
2 * 多级缓存策略
3 */
4@Service
5public class InventoryCacheService {
6
7 @Autowired
8 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. 查询 Redis
28 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 消息队列异步处理

java
1/**
2 * 出库消息生产者
3 */
4@Service
5public class OutboundMessageProducer {
6
7 @Autowired
8 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 @Override
21 public void onSuccess(SendResult sendResult) {
22 log.info("出库消息发送成功: orderId={}", orderId);
23 }
24
25 @Override
26 public void onException(Throwable throwable) {
27 log.error("出库消息发送失败: orderId={}", orderId, throwable);
28 // 重试或记录补偿任务
29 }
30 });
31 }
32}
33
34/**
35 * 库存扣减消费者
36 */
37@Service
38@RocketMQMessageListener(
39 topic = "OUTBOUND_COMPLETED_TOPIC",
40 consumerGroup = "inventory-consumer-group"
41)
42public class InventoryDeductConsumer implements RocketMQListener<OutboundCompletedEvent> {
43
44 @Autowired
45 private InventoryService inventoryService;
46
47 @Override
48 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普通default24小时内
2紧急warning8小时内
3特急danger2小时内

计量单位 (unit)

单位代码单位名称英文
PCS个/件Piece
BOXBox
CTN纸箱Carton
PLT托盘Pallet
KG千克Kilogram
METERMeter
LITERLiter

异常类型 (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普通供应商一般合作供应商
VIPVIP供应商重要合作伙伴
STRATEGIC战略供应商战略合作伙伴

客户类型 (customer_type)

类型值类型名称说明
RETAIL零售客户零售终端客户
WHOLESALE批发客户批发商客户
ENTERPRISE企业客户企业级客户

客户等级 (customer_level)

等级值等级名称说明
VIPVIP客户最高级客户
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点击查看详情
波次类型100pxTag标签
订单数量80px-
商品种类80px-
总数量100px-
优先级80pxTag标签
拣货员100px-
预计耗时100px分钟
实际耗时100px分钟,超时红色
拣货进度120px进度条
状态90pxTag标签
开始时间150px-
完成时间150px-
操作180px查看/分配/开始/取消

智能生成波次配置:

  • 波次大小: 30-50单/波次
  • 时间窗口: 30分钟内订单聚合
  • 聚类算法: 按库区和商品位置聚类
  • 优先级规则: 特急订单优先处理

波次详情页包含:

  1. 波次基本信息
  2. 包含的出库单列表
  3. 拣货路径可视化(库位地图)
  4. 拣货任务明细
  5. 拣货进度实时监控

10.2.5 盘点计划管理页面

页面路径: /stock-taking/plan/list

功能说明: 创建和管理盘点计划

检索条件:

字段名字段类型是否必填说明
计划编号文本输入-
计划名称文本输入模糊查询
仓库下拉选择-
盘点类型下拉选择全盘/循环盘/抽盘/动态盘
状态下拉选择待审核/待执行/执行中/已完成/已取消
计划时间日期区间-

新增盘点计划表单:

字段名字段类型是否必填说明
计划名称文本输入如:"2025年第一季度全盘"
仓库下拉选择-
盘点类型单选全盘/循环盘/抽盘/动态盘
盘点范围动态表单根据盘点类型显示不同配置
计划开始时间日期时间选择器-
计划结束时间日期时间选择器必须晚于开始时间
盘点人员多选选择参与盘点的人员
备注多行文本-

盘点范围配置:

  • 全盘: 自动包含仓库内所有商品
  • 循环盘: 选择商品分类或ABC分类
  • 抽盘: 输入抽样比例(如10%)和随机种子
  • 动态盘: 选择零库存或指定商品

盘点执行流程:

  1. 审核通过后生成盘点单
  2. 盘点人员PDA扫描盘点
  3. 提交盘点结果
  4. 复核差异数据
  5. 生成盘盈盘亏单
  6. 调整库存

10.3 移动端(PDA)功能设计

10.3.1 收货上架

流程步骤:

  1. 扫描入库单号 → 显示商品明细
  2. 逐个扫描商品条码 → 输入实收数量
  3. 扫描目标库位 → 确认上架
  4. 打印库位标签
  5. 完成入库

页面元素:

  • 大号输入框(支持扫码枪)
  • 商品图片显示
  • 数量调整按钮(+/-)
  • 异常上报按钮

10.3.2 拣货出库

流程步骤:

  1. 登录 → 查看待拣货任务列表
  2. 选择任务 → 显示优化后的拣货路径
  3. 按顺序到达库位 → 扫描库位码
  4. 扫描商品条码 → 输入拣货数量
  5. 扫描集货容器 → 完成拣货
  6. 提交复核

特色功能:

  • 路径导航(AR箭头指引)
  • 语音播报库位和数量
  • 拣货进度实时同步
  • 异常一键上报

10.3.3 库存盘点

流程步骤:

  1. 扫描盘点单号 → 显示待盘点库位列表
  2. 到达库位 → 扫描库位码
  3. 逐个扫描商品条码 → 输入实盘数量
  4. 对比账面数量 → 标记差异
  5. 拍照上传(异常情况)
  6. 提交盘点结果

智能提示:

  • 账面数量实时对比
  • 差异自动标红
  • 遗漏提示
  • 盘点进度显示

十一、数据库设计补充(关联关系图)

11.1 完整ER图

11.2 核心表关联关系说明

主表从表关联字段关系类型约束
warehousewarehouse_areawarehouse_id1:NCASCADE
warehouse_areawarehouse_locationarea_id1:NCASCADE
warehouse_locationinventorylocation_id1:NRESTRICT
goodsinventorygoods_id1:NRESTRICT
goods_categorygoodscategory_id1:NSET NULL
supplierinbound_ordersupplier_id1:NSET NULL
customeroutbound_ordercustomer_code1:NSET NULL
inbound_orderinbound_detailinbound_id1:NCASCADE
outbound_orderoutbound_detailoutbound_id1:NCASCADE
picking_wavepicking_wave_orderwave_id1:NCASCADE
picking_wavepicking_taskwave_id1:NCASCADE
stock_taking_planstock_takingplan_id1:NCASCADE

约束说明:

  • CASCADE: 主表记录删除时,从表记录同步删除
  • RESTRICT: 存在从表记录时,主表记录不可删除
  • SET NULL: 主表记录删除时,从表外键字段设为NULL

十二、初始化数据SQL

12.1 数据字典初始化

sql
1-- 清空字典表
2TRUNCATE TABLE sys_dict;
3
4-- 入库类型字典
5INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
6('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);
12
13-- 出库类型字典
14INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
15('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);
21
22-- 入库单状态字典
23INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
24('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);
29
30-- 出库单状态字典
31INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
32('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);
39
40-- 库存状态字典
41INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
42('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);
46
47-- 优先级字典
48INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
49('priority', '优先级', '1', '普通', 1, 'default', 1),
50('priority', '优先级', '2', '紧急', 2, 'warning', 1),
51('priority', '优先级', '3', '特急', 3, 'danger', 1);
52
53-- 计量单位字典
54INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
55('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);
62
63-- 仓库类型字典
64INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
65('warehouse_type', '仓库类型', '1', '成品仓', 1, 1),
66('warehouse_type', '仓库类型', '2', '原料仓', 2, 1),
67('warehouse_type', '仓库类型', '3', '半成品仓', 3, 1);
68
69-- 库区类型字典
70INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
71('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);
76
77-- 库位类型字典
78INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
79('location_type', '库位类型', 'NORMAL', '普通库位', 1, 1),
80('location_type', '库位类型', 'TEMP', '临时库位', 2, 1),
81('location_type', '库位类型', 'DEFECT', '残次品库位', 3, 1),
82('location_type', '库位类型', 'FROZEN', '冷冻库位', 4, 1);
83
84-- 库位状态字典
85INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
86('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);
90
91-- 波次类型字典
92INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
93('wave_type', '波次类型', 'BATCH', '批次拣货', 1, 1),
94('wave_type', '波次类型', 'ZONE', '分区拣货', 2, 1),
95('wave_type', '波次类型', 'SINGLE', '单品拣货', 3, 1);
96
97-- 波次状态字典
98INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
99('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);
104
105-- 盘点计划状态字典
106INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
107('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);
112
113-- 盘点类型字典
114INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
115('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);
119
120-- 盘点单状态字典
121INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
122('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);
126
127-- 移库类型字典
128INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
129('stock_move_type', '移库类型', 'LOCATION', '库位调整', 1, 1),
130('stock_move_type', '移库类型', 'AREA', '库区调整', 2, 1),
131('stock_move_type', '移库类型', 'WAREHOUSE', '仓库调拨', 3, 1);
132
133-- 移库单状态字典
134INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
135('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);
139
140-- 供应商类型字典
141INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
142('supplier_type', '供应商类型', 'NORMAL', '普通供应商', 1, 1),
143('supplier_type', '供应商类型', 'VIP', 'VIP供应商', 2, 1),
144('supplier_type', '供应商类型', 'STRATEGIC', '战略供应商', 3, 1);
145
146-- 客户类型字典
147INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
148('customer_type', '客户类型', 'RETAIL', '零售客户', 1, 1),
149('customer_type', '客户类型', 'WHOLESALE', '批发客户', 2, 1),
150('customer_type', '客户类型', 'ENTERPRISE', '企业客户', 3, 1);
151
152-- 客户等级字典
153INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
154('customer_level', '客户等级', 'VIP', 'VIP客户', 1, 1),
155('customer_level', '客户等级', 'GOLD', '金牌客户', 2, 1),
156('customer_level', '客户等级', 'SILVER', '银牌客户', 3, 1),
157('customer_level', '客户等级', 'NORMAL', '普通客户', 4, 1);
158
159-- 承运商类型字典
160INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
161('carrier_type', '承运商类型', 'EXPRESS', '快递', 1, 1),
162('carrier_type', '承运商类型', 'LOGISTICS', '物流', 2, 1),
163('carrier_type', '承运商类型', 'SPECIAL', '专线', 3, 1);
164
165-- 入库单明细状态字典
166INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
167('inbound_detail_status', '入库单明细状态', '1', '待收货', 1, 'info', 1),
168('inbound_detail_status', '入库单明细状态', '2', '已收货', 2, 'warning', 1),
169('inbound_detail_status', '入库单明细状态', '3', '已上架', 3, 'success', 1);
170
171-- 出库单明细状态字典
172INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, css_class, status) VALUES
173('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);
178
179-- 异常类型字典
180INSERT INTO sys_dict (dict_code, dict_name, dict_value, dict_label, sort_order, status) VALUES
181('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 示例数据初始化

sql
1-- 插入测试仓库
2INSERT INTO warehouse (warehouse_code, warehouse_name, warehouse_type, province, city, district, address, contact_person, contact_phone, total_area, status) VALUES
3('WH001', '北京总仓', 1, '北京市', '朝阳区', '望京街道', '望京SOHO T1座', '张三', '13800138000', 50000.00, 1),
4('WH002', '上海分仓', 1, '上海市', '浦东新区', '张江', '张江高科技园区', '李四', '13900139000', 30000.00, 1);
5
6-- 插入库区
7INSERT INTO warehouse_area (warehouse_id, area_code, area_name, area_type, floor, area_size, status) VALUES
8(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);
11
12-- 插入示例库位
13INSERT INTO warehouse_location (warehouse_id, area_id, location_code, location_type, row_no, column_no, layer_no, capacity, max_weight, status) VALUES
14(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);
17
18-- 插入商品分类
19INSERT INTO goods_category (category_code, category_name, parent_id, level, sort_order) VALUES
20('ELEC', '电子产品', 0, 1, 1),
21('DAILY', '日用品', 0, 1, 2),
22('FOOD', '食品', 0, 1, 3);
23
24-- 插入示例商品
25INSERT INTO goods (sku_code, goods_name, category_id, brand, unit, weight, need_batch, safety_stock, status) VALUES
26('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);
29
30-- 插入供应商
31INSERT INTO supplier (supplier_code, supplier_name, supplier_type, contact_person, contact_phone, province, city, address, credit_level, status) VALUES
32('SUP001', '苹果官方供应商', 'VIP', '王五', '13700137000', '北京市', '海淀区', '中关村软件园', 'A', 1),
33('SUP002', '小米官方供应商', 'VIP', '赵六', '13600136000', '北京市', '海淀区', '小米科技园', 'A', 1);
34
35-- 插入客户
36INSERT INTO customer (customer_code, customer_name, customer_type, customer_level, contact_person, contact_phone, delivery_city, delivery_address, credit_limit, status) VALUES
37('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 高并发库存扣减一致性问题

问题背景: 在电商大促等高并发场景下,多个用户同时购买同一商品,系统需要保证:

  • 不超卖:库存扣减准确无误
  • 高性能:秒级响应用户请求
  • 高可用:服务稳定不宕机

核心挑战

  • 并发安全:多线程同时修改库存数据
  • 性能瓶颈:数据库锁竞争激烈
  • 分布式一致性:多服务节点数据同步

解决方案 - 三重保障机制

java
1/**
2 * 库存扣减 - 分布式锁 + 数据库行锁 + 乐观锁
3 */
4@Override
5@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⚪ 备选

核心算法实现

java
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}
25
26/**
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聚类变种

java
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}
19
20/**
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 微服务拆分策略

拆分原则

  1. 业务能力边界:每个服务负责完整的业务域
  2. 数据独立性:避免跨服务的数据事务
  3. 团队组织结构:符合康威定律
  4. 技术栈异构:不同服务可选择最适合的技术

服务划分结果

yaml
1services:
2 - name: warehouse-service
3 responsibility: 仓库、库区、库位管理
4 database: warehouse_db
5
6 - name: inventory-service
7 responsibility: 库存管理、库存流水
8 database: inventory_db
9
10 - name: inbound-service
11 responsibility:入库单、收货上架
12 database: inbound_db
13
14 - name: outbound-service
15 responsibility: 出库单、订单处理
16 database: outbound_db
17
18 - name: picking-service
19 responsibility: 拣货波次、任务调度
20 database: picking_db
21
22 - name: report-service
23 responsibility: 报表分析、数据统计
24 database: report_db

15.2.2 分布式事务处理

场景:出库流程的分布式事务

涉及服务:

  • 库存服务:扣减库存
  • 订单服务:更新状态
  • 物流服务:创建运单
  • 账务服务:记录成本

解决方案 - Saga模式

java
1/**
2 * 出库流程编排
3 */
4@Component
5public class OutboundSaga {
6
7 @SagaOrchestrationStart
8 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)

配置策略

java
1@Configuration
2public 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 @Override
23 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}

缓存服务实现

java
1@Service
2@Slf4j
3public class LocalCacheService {
4
5 @Autowired
6 @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 @PostConstruct
42 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)

集群架构

yaml
1# Redis Cluster 配置
2redis:
3 cluster:
4 nodes:
5 - 192.168.1.10:7000 # 主节点1
6 - 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 # 主节点3
10 - 192.168.1.12:7001 # 主节点3从节点
11 max-redirects: 3
12 timeout: 3000ms
13 pool:
14 max-active: 100
15 max-idle: 20
16 min-idle: 5
17 max-wait: 3000ms

缓存分层设计

java
1@Service
2@Slf4j
3public class RedisCacheService {
4
5 @Autowired
6 private RedisTemplate<String, Object> redisTemplate;
7
8 @Autowired
9 private StringRedisTemplate stringRedisTemplate;
10
11 /**
12 * 库存缓存 - 热数据,短TTL
13 */
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 * 商品信息缓存 - 温数据,长TTL
26 */
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 @Autowired
74 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设计规范

java
1public class CacheKey {
2 // 业务缓存Key
3 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 // 布隆过滤器Key
15 public static final String BLOOM_GOODS = "wms:bloom:goods"; // 商品存在性
16 public static final String BLOOM_ORDER = "wms:bloom:order"; // 订单存在性
17
18 // 计数器Key
19 public static final String COUNTER_REQUEST = "wms:counter:request:"; // 请求计数
20 public static final String COUNTER_ERROR = "wms:counter:error:"; // 错误计数
21}
缓存更新策略

1. Cache Aside模式(主要使用)

java
1@Service
2public 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模式(实时性要求高)

java
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模式(批量更新)

java
1/**
2 * 库存流水异步批量写入
3 */
4@Component
5public 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}
缓存监控与运维
java
1@Component
2public class CacheMonitor {
3
4 @Autowired
5 @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 @EventListener
32 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分片

yaml
1# ShardingSphere配置
2dataSources:
3 wms_db_001:
4 url: jdbc:mysql://192.168.1.10:3306/wms_db_001
5 username: wms_user
6 password: wms_password
7
8 wms_db_002:
9 url: jdbc:mysql://192.168.1.11:3306/wms_db_002
10 username: wms_user
11 password: wms_password
12
13 wms_db_003:
14 url: jdbc:mysql://192.168.1.12:3306/wms_db_003
15 username: wms_user
16 password: wms_password
17
18shardingRule:
19 tables:
20 # 库存表分片规则
21 inventory:
22 actualDataNodes: wms_db_00$->{1..3}.inventory_00$->{1..9}
23 databaseStrategy:
24 standard:
25 shardingColumn: warehouse_id
26 shardingAlgorithmName: warehouse_database_inline
27 tableStrategy:
28 standard:
29 shardingColumn: goods_id
30 shardingAlgorithmName: goods_table_inline
31
32 # 库存流水表分片规则
33 inventory_log:
34 actualDataNodes: wms_db_00$->{1..3}.inventory_log_$->{202501..202612}
35 databaseStrategy:
36 standard:
37 shardingColumn: warehouse_id
38 shardingAlgorithmName: warehouse_database_inline
39 tableStrategy:
40 standard:
41 shardingColumn: create_time
42 shardingAlgorithmName: log_table_time
43
44shardingAlgorithms:
45 # 仓库维度分库算法
46 warehouse_database_inline:
47 type: INLINE
48 props:
49 algorithm-expression: wms_db_00$->{(warehouse_id % 3) + 1}
50
51 # 商品维度分表算法
52 goods_table_inline:
53 type: INLINE
54 props:
55 algorithm-expression: inventory_00$->{(goods_id % 9) + 1}
56
57 # 时间维度分表算法
58 log_table_time:
59 type: INLINE
60 props:
61 algorithm-expression: inventory_log_$->{create_time.format("yyyyMM")}

2. 订单相关表 - 按客户ID分片

sql
1-- 出库单表分片DDL
2-- 分库:按customer_code的hash值分布到3个数据库
3-- 分表:按订单创建时间按月分表
4
5-- DB1: wms_db_001
6CREATE 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;
18
19CREATE TABLE outbound_order_202502 (
20 -- 同上结构
21) ENGINE=InnoDB PARTITION BY HASH(id % 8) PARTITIONS 8;
22
23-- 继续创建其他月份的分表...

3. 完整分片表结构

sql
1-- ================================
2-- 库存表分片 (按仓库ID分库,按商品ID分表)
3-- ================================
4
5-- 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';
26
27-- 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';
31
32-- 继续创建inventory_003到inventory_009...
33
34-- ================================
35-- 库存流水表分片 (按仓库ID分库,按时间分表)
36-- ================================
37
38-- 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';
58
59-- ================================
60-- 出库单表分片 (按客户编码分库,按时间分表)
61-- ================================
62
63-- 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配置

java
1@Configuration
2public class ShardingDataSourceConfig {
3
4 /**
5 * 配置分片数据源
6 */
7 @Bean
8 @Primary
9 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, properties
24 );
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 // 分库策略:按仓库ID
82 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 @Component
100 public static class CustomShardingAlgorithm implements PreciseShardingAlgorithm<Date> {
101
102 @Override
103 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. 带分片键查询(最优)

java
1@Service
2public 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. 跨分片查询优化

java
1/**
2 * 跨分片聚合查询优化
3 */
4@Service
5public class CrossShardQueryService {
6
7 @Autowired
8 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}
分布式事务处理
java
1/**
2 * 跨分片事务处理
3 */
4@Service
5public class CrossShardTransactionService {
6
7 @Autowired
8 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}
分片运维工具
java
1/**
2 * 分片运维工具
3 */
4@Component
5public 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仓库管理系统的设计方案、技术难点和面试准备策略,为开发和求职提供完整的参考指南。

参与讨论