Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
M
mini-wms
概览
Overview
Details
Activity
Cycle Analytics
版本库
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
问题
0
Issues
0
列表
Board
标记
里程碑
合并请求
0
Merge Requests
0
CI / CD
CI / CD
流水线
作业
日程表
图表
维基
Wiki
代码片段
Snippets
成员
Members
Collapse sidebar
Close sidebar
活动
图像
聊天
创建新问题
作业
提交
Issue Boards
Open sidebar
周海峰
mini-wms
Commits
d04916a9
Commit
d04916a9
authored
Dec 17, 2025
by
wangchunyang
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of
https://code.palacesun.com/zhouhaifeng/mini-wms
parents
13acd4f6
887e590d
隐藏空白字符变更
内嵌
并排
正在显示
13 个修改的文件
包含
412 行增加
和
317 行删除
+412
-317
ruoyi-admin-vue/src/views/inventory/inbound/index.vue
+1
-1
ruoyi-admin-vue/src/views/inventory/inbound_items/index.vue
+1
-1
ruoyi-admin-vue/src/views/inventory/locations/index.vue
+8
-8
ruoyi-admin-vue/src/views/inventory/materials/index.vue
+1
-1
ruoyi-admin-vue/src/views/inventory/orders/index.vue
+1
-1
ruoyi-admin-vue/src/views/inventory/owners/index.vue
+10
-9
ruoyi-admin-vue/src/views/system/role/index.vue
+10
-11
ruoyi-admin-vue/src/views/system/user/index.vue
+1
-1
ruoyi-inventory/src/main/java/com/ruoyi/inventory/domain/OutboundOrders.java
+3
-1
ruoyi-inventory/src/main/java/com/ruoyi/inventory/service/impl/OutboundOrdersServiceImpl.java
+318
-275
ruoyi-inventory/src/main/java/com/ruoyi/inventory/utils/InventoryCache.java
+37
-0
ruoyi-inventory/src/main/resources/mapper/inventory/InventoryMapper.xml
+3
-1
ruoyi-inventory/src/main/resources/mapper/inventory/OutboundOrdersMapper.xml
+18
-7
没有找到文件。
ruoyi-admin-vue/src/views/inventory/inbound/index.vue
View file @
d04916a9
...
...
@@ -33,7 +33,7 @@
>
删除
</el-button>
<el-button
type=
"
warning
"
type=
"
success
"
plain
icon=
"el-icon-upload"
size=
"medium"
...
...
ruoyi-admin-vue/src/views/inventory/inbound_items/index.vue
View file @
d04916a9
...
...
@@ -84,7 +84,7 @@
</el-col>
<!-- <el-col :span="1.5" v-if="isEditable">
<el-button
type="
warning
"
type="
success
"
plain
icon="el-icon-download"
size="mini"
...
...
ruoyi-admin-vue/src/views/inventory/locations/index.vue
View file @
d04916a9
...
...
@@ -28,6 +28,14 @@
@
click=
"handleDelete"
v-hasPermi=
"['inventory:locations:remove']"
>
删除
</el-button>
<el-button
type=
"success"
plain
icon=
"el-icon-upload"
size=
"medium"
@
click=
"handleImport"
v-hasPermi=
"['inventory:locations:add']"
>
导入
</el-button>
<el-button
type=
"warning"
plain
...
...
@@ -36,14 +44,6 @@
@
click=
"handleExport"
v-hasPermi=
"['inventory:locations:export']"
>
导出
</el-button>
<el-button
type=
"info"
plain
icon=
"el-icon-upload2"
size=
"medium"
@
click=
"handleImport"
v-hasPermi=
"['inventory:locations:add']"
>
导入
</el-button>
</
template
>
</PageTitle>
...
...
ruoyi-admin-vue/src/views/inventory/materials/index.vue
View file @
d04916a9
...
...
@@ -34,7 +34,7 @@
<el-button
type=
"success"
plain
icon=
"el-icon-upload
2
"
icon=
"el-icon-upload"
size=
"medium"
@
click=
"handleImport"
v-hasPermi=
"['inventory:materials:import']"
...
...
ruoyi-admin-vue/src/views/inventory/orders/index.vue
View file @
d04916a9
...
...
@@ -33,7 +33,7 @@
>
删除
</el-button>
<el-button
type=
"
warning
"
type=
"
success
"
plain
icon=
"el-icon-upload"
size=
"medium"
...
...
ruoyi-admin-vue/src/views/inventory/owners/index.vue
View file @
d04916a9
...
...
@@ -34,6 +34,15 @@
>
删除
</el-button>
<el-button
type=
"success"
plain
icon=
"el-icon-upload"
size=
"medium"
@
click=
"handleImport"
v-hasPermi=
"['inventory:owners:add']"
>
导入
</el-button>
<el-button
type=
"warning"
plain
icon=
"el-icon-download"
...
...
@@ -42,15 +51,7 @@
v-hasPermi=
"['inventory:owners:export']"
>
导出
</el-button>
<el-button
type=
"info"
plain
icon=
"el-icon-upload2"
size=
"medium"
@
click=
"handleImport"
v-hasPermi=
"['inventory:owners:add']"
>
导入
</el-button>
</
template
>
</PageTitle>
...
...
ruoyi-admin-vue/src/views/system/role/index.vue
View file @
d04916a9
...
...
@@ -36,7 +36,15 @@
v-hasPermi=
"['system:role:remove']"
>
删除
</el-button>
<el-button
type=
"success"
plain
icon=
"el-icon-upload"
size=
"medium"
@
click=
"handleImport"
v-hasPermi=
"['system:role:import']"
>
导入
</el-button>
<el-button
type=
"warning"
plain
...
...
@@ -45,16 +53,7 @@
@
click=
"handleExport"
v-hasPermi=
"['system:role:export']"
>
导出
</el-button>
<el-button
type=
"info"
plain
icon=
"el-icon-upload2"
size=
"medium"
@
click=
"handleImport"
v-hasPermi=
"['system:role:import']"
>
导入
</el-button>
</
template
>
</PageTitle>
...
...
ruoyi-admin-vue/src/views/system/user/index.vue
View file @
d04916a9
...
...
@@ -9,7 +9,7 @@
<el-button
type=
"danger"
plain
icon=
"el-icon-delete"
size=
"medium"
:disabled=
"multiple"
@
click=
"handleDelete"
v-hasPermi=
"['system:user:remove']"
>
删除
</el-button>
<el-button
type=
"
info"
plain
icon=
"el-icon-upload2
"
size=
"medium"
@
click=
"handleImport"
v-hasPermi=
"['system:user:import']"
>
导入
</el-button>
<el-button
type=
"
success"
plain
icon=
"el-icon-upload
"
size=
"medium"
@
click=
"handleImport"
v-hasPermi=
"['system:user:import']"
>
导入
</el-button>
<el-button
type=
"warning"
plain
icon=
"el-icon-download"
size=
"medium"
@
click=
"handleExport"
v-hasPermi=
"['system:user:export']"
>
导出
</el-button>
</
template
>
...
...
ruoyi-inventory/src/main/java/com/ruoyi/inventory/domain/OutboundOrders.java
View file @
d04916a9
...
...
@@ -109,5 +109,6 @@ public class OutboundOrders extends BaseEntity
/** 出库单明细信息 */
private
List
<
OutboundOrderItems
>
outboundOrderItemsList
;
@Excel
(
name
=
"导入标识"
)
private
Long
isImport
;
// 对应数据库字段is_import,驼峰命名转换
}
\ No newline at end of file
ruoyi-inventory/src/main/java/com/ruoyi/inventory/service/impl/OutboundOrdersServiceImpl.java
View file @
d04916a9
package
com
.
ruoyi
.
inventory
.
service
.
impl
;
import
java.util.*
;
import
java.util.stream.Collectors
;
import
com.ruoyi.common.annotation.SerialExecution
;
import
com.ruoyi.common.config.WarehouseConfig
;
import
com.ruoyi.common.core.domain.entity.Materials
;
import
com.ruoyi.common.exception.ServiceException
;
import
com.ruoyi.common.utils.DateUtils
;
import
com.ruoyi.common.utils.SecurityUtils
;
import
com.ruoyi.common.utils.StringUtils
;
import
com.ruoyi.inventory.utils.InventoryCache
;
import
com.ruoyi.inventory.domain.*
;
import
com.ruoyi.inventory.domain.vo.OutboundTemplateVO
;
import
com.ruoyi.inventory.mapper.InventoryMapper
;
import
com.ruoyi.inventory.mapper.OutboundOrderItemsMapper
;
import
com.ruoyi.inventory.mapper.OutboundOrderLogMapper
;
import
com.ruoyi.inventory.mapper.OutboundOrdersMapper
;
import
com.ruoyi.inventory.service.IOutboundOrdersService
;
import
org.apache.commons.lang3.SystemUtils
;
import
org.mybatis.spring.SqlSessionTemplate
;
import
org.springframework.beans.BeanUtils
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Service
;
import
com.ruoyi.common.utils.StringUtils
;
import
org.springframework.transaction.annotation.Transactional
;
import
com.ruoyi.inventory.mapper.OutboundOrdersMapper
;
import
com.ruoyi.inventory.service.IOutboundOrdersService
;
import
org.springframework.transaction.support.TransactionSynchronization
;
import
org.springframework.transaction.support.TransactionSynchronizationManager
;
import
org.springframework.util.CollectionUtils
;
import
java.util.*
;
import
java.util.concurrent.CompletableFuture
;
import
java.util.stream.Collectors
;
/**
* 出库单主Service业务层处理
* 核心修正:
* 1. 库存匹配Key统一为 物料ID_库位ID_库存类型(移除仓库维度)
* 2. 确保inventoryType完整参与匹配
* 3. 库存扣减后≤0时强制设置inventory_status=0
* 4. 无库位库存不足时,自动扣减同物料同库存类型的有库位库存
* 5. 导入场景下仅保留新增逻辑,移除更新相关处理
* 6. 移除所有仓库相关逻辑,仅保留物料+库位维度匹配
* 最终修复版:
* 1. 合并维度调整为「物料ID+库存类型+库位ID」,确保库位信息准确
* 2. 扣减时按该维度分组,统一收集记录后合并
* 3. 修复跨场景(无库位→有库位)扣减同一物料+库存类型的合并逻辑
* 4. 新增相同库存维度的明细合并逻辑,插入前合并为一条记录
*
* @author ruoyi
* @date 2025-12-03
*/
@Service
public
class
OutboundOrdersServiceImpl
implements
IOutboundOrdersService
{
public
class
OutboundOrdersServiceImpl
implements
IOutboundOrdersService
{
@Autowired
private
OutboundOrdersMapper
outboundOrdersMapper
;
...
...
@@ -64,6 +63,9 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
@Autowired
private
InventoryMapper
inventoryMapper
;
@Autowired
private
SqlSessionTemplate
sqlSessionTemplate
;
/**
* 查询出库单主
*
...
...
@@ -71,8 +73,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
* @return 出库单主
*/
@Override
public
OutboundOrders
selectOutboundOrdersById
(
String
id
)
{
public
OutboundOrders
selectOutboundOrdersById
(
String
id
)
{
return
outboundOrdersMapper
.
selectOutboundOrdersById
(
id
);
}
...
...
@@ -83,8 +84,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
* @return 出库单主
*/
@Override
public
List
<
OutboundOrders
>
selectOutboundOrdersList
(
OutboundOrders
outboundOrders
)
{
public
List
<
OutboundOrders
>
selectOutboundOrdersList
(
OutboundOrders
outboundOrders
)
{
List
<
OutboundOrders
>
outboundOrders1
=
outboundOrdersMapper
.
selectOutboundOrdersList
(
outboundOrders
);
return
outboundOrders1
;
}
...
...
@@ -97,11 +97,11 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
*/
@Transactional
@Override
public
int
insertOutboundOrders
(
OutboundOrders
outboundOrders
)
{
public
int
insertOutboundOrders
(
OutboundOrders
outboundOrders
)
{
outboundOrders
.
setCreateTime
(
DateUtils
.
getNowDate
());
outboundOrders
.
setCreateUserCode
(
SystemUtils
.
getUserName
());
outboundOrders
.
setId
(
UUID
.
randomUUID
().
toString
());
outboundOrders
.
setIsImport
(
0
l
);
int
rows
=
outboundOrdersMapper
.
insertOutboundOrders
(
outboundOrders
);
insertOutboundOrderItems
(
outboundOrders
);
...
...
@@ -116,8 +116,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
*/
@Transactional
@Override
public
int
updateOutboundOrders
(
OutboundOrders
outboundOrders
)
{
public
int
updateOutboundOrders
(
OutboundOrders
outboundOrders
)
{
throw
new
ServiceException
(
"当前系统仅支持新增出库单,不支持修改操作"
);
}
...
...
@@ -129,8 +128,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
*/
@Transactional
@Override
public
int
deleteOutboundOrdersByIds
(
String
[]
ids
)
{
public
int
deleteOutboundOrdersByIds
(
String
[]
ids
)
{
outboundOrdersMapper
.
deleteOutboundOrderItemsByOrderIds
(
ids
);
outboundOrderLogMapper
.
deleteOutboundOrderLogByOrdersIds
(
ids
);
return
outboundOrdersMapper
.
deleteOutboundOrdersByIds
(
ids
);
...
...
@@ -144,8 +142,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
*/
@Transactional
@Override
public
int
deleteOutboundOrdersById
(
String
id
)
{
public
int
deleteOutboundOrdersById
(
String
id
)
{
outboundOrdersMapper
.
deleteOutboundOrderItemsByOrderId
(
id
);
return
outboundOrdersMapper
.
deleteOutboundOrdersById
(
id
);
}
...
...
@@ -186,16 +183,10 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
}
/**
* 核心库存扣减逻辑
* 1. 有库位:直接扣减指定库位库存
* 2. 无库位:先扣无库位库存,不足则扣同物料同库存类型的有库位库存
* 3. 扣减后数量≤0时设置inventory_status=0
* 4. 无库位扣减有库位时,记录扣减的库位和数量(用于生成明细)
* 5. 移除仓库维度,仅按物料+库位+库存类型匹配
* @param outboundOrderItems 出库明细
* @param updateUser 操作人
* @param updateTime 操作时间
* @return 无库位明细扣减的有库位库存记录 Map<无库位明细ID, List<扣减的库位库存信息Map>>
* 核心库存扣减逻辑(最终修复版)
* 1. 扣减维度:物料ID+库存类型+库位ID(统一维度,跨场景合并)
* 2. 扣减时先按该维度分组,再累计扣减数量
* 3. 收集记录时按该维度统一收集,合并器仅做最终校验
*/
private
Map
<
String
,
List
<
Map
<
String
,
Object
>>>
deductInventory
(
List
<
OutboundOrderItems
>
outboundOrderItems
,
String
updateUser
,
Date
updateTime
)
{
if
(
CollectionUtils
.
isEmpty
(
outboundOrderItems
))
{
...
...
@@ -203,10 +194,12 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
}
Map
<
String
,
List
<
Map
<
String
,
Object
>>>
deductRecordMap
=
new
HashMap
<>();
Map
<
String
,
List
<
Inventory
>>
inventoryGroupMap
=
this
.
loadInventoryGroupMap
();
// 预加载库存:按「物料ID+库存类型+库位ID」分组(核心调整)
Map
<
String
,
Inventory
>
inventoryFullMap
=
this
.
loadInventoryFullMap
();
Map
<
String
,
Long
>
deductQtyMap
=
this
.
buildDeductQtyMap
(
outboundOrderItems
);
List
<
Inventory
>
needUpdateList
=
new
ArrayList
<>();
// 库存更新Map(最终去重,保留最终状态)
Map
<
String
,
Inventory
>
toUpdateInventoryMap
=
new
LinkedHashMap
<>();
for
(
Map
.
Entry
<
String
,
Long
>
entry
:
deductQtyMap
.
entrySet
())
{
String
key
=
entry
.
getKey
();
...
...
@@ -217,121 +210,83 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
String
materialId
=
keyParts
.
length
>
0
?
keyParts
[
0
]
:
""
;
String
locationId
=
keyParts
.
length
>
1
?
keyParts
[
1
]
:
""
;
String
inventoryType
=
keyParts
.
length
>
2
?
keyParts
[
2
]
:
""
;
String
groupKey
=
String
.
join
(
"_"
,
materialId
,
inventoryType
);
List
<
Inventory
>
inventoryList
=
inventoryGroupMap
.
get
(
groupKey
);
if
(
CollectionUtils
.
isEmpty
(
inventoryList
))
{
throw
new
ServiceException
(
String
.
format
(
"物料[%s]库存类型[%s]无可用库存,无法扣减"
,
materialId
,
inventoryType
));
}
Long
remainDeductQty
=
totalDeductQty
;
List
<
Map
<
String
,
Object
>>
deductRecords
=
new
ArrayList
<>();
String
itemId
=
outboundOrderItems
.
stream
()
.
filter
(
item
->
key
.
equals
(
buildDeductKey
(
item
)))
.
map
(
OutboundOrderItems:
:
getId
)
.
findFirst
()
.
orElse
(
null
);
// 区分无库位/有库位逻辑
if
(
StringUtils
.
isBlank
(
locationId
))
{
// 无库位:先扣无库位库存,再扣有库位库存
// 第一步:扣无库位库存
List
<
Inventory
>
noLocationInvList
=
inventoryList
.
stream
()
.
filter
(
inv
->
StringUtils
.
isBlank
(
inv
.
getLocationId
()))
.
collect
(
Collectors
.
toList
());
for
(
Inventory
inv
:
noLocationInvList
)
{
if
(
remainDeductQty
<=
0
)
break
;
Long
currentQty
=
Optional
.
ofNullable
(
inv
.
getQuantity
()).
orElse
(
0L
);
Long
deductQty
=
Math
.
min
(
remainDeductQty
,
currentQty
);
// 不扣负数,只扣现有库存
inv
.
setQuantity
(
currentQty
-
deductQty
);
if
(
inv
.
getQuantity
()
<=
0
)
{
inv
.
setInventoryStatus
(
0L
);
}
inv
.
setUpdateBy
(
updateUser
);
inv
.
setUpdateTime
(
updateTime
);
needUpdateList
.
add
(
inv
);
Long
remainDeductQty
=
totalDeductQty
;
// 核心:按「物料ID+库存类型+库位ID」分组扣减(先处理无库位,再处理有库位)
List
<
Map
<
String
,
Object
>>
tempDeductRecords
=
new
ArrayList
<>();
// 记录无库位扣减
deductRecords
.
add
(
buildDeductRecord
(
inv
,
inventoryType
,
deductQty
));
remainDeductQty
-=
deductQty
;
}
// 步骤1:先扣指定维度的库存(无库位/有库位)
if
(
StringUtils
.
isBlank
(
locationId
))
{
// 无库位:先扣「物料+库存类型+空库位」的库存
String
noLocKey
=
buildInventoryKey
(
materialId
,
""
,
inventoryType
);
remainDeductQty
=
deductByInventoryKey
(
noLocKey
,
remainDeductQty
,
updateUser
,
updateTime
,
inventoryFullMap
,
toUpdateInventoryMap
,
tempDeductRecords
);
//
第二步:无库位不足,扣有库位
库存
//
无库位不足,扣「物料+库存类型+任意有库位」的
库存
if
(
remainDeductQty
>
0
)
{
List
<
Inventory
>
hasLocationInvList
=
inventoryList
.
stream
()
.
filter
(
inv
->
StringUtils
.
isNotBlank
(
inv
.
getLocationId
()))
// 筛选该物料+库存类型的所有有库位库存
List
<
String
>
hasLocKeys
=
inventoryFullMap
.
keySet
().
stream
()
.
filter
(
k
->
{
String
[]
parts
=
k
.
split
(
"_"
);
return
parts
.
length
>=
3
&&
parts
[
0
].
equals
(
materialId
)
&&
parts
[
2
].
equals
(
inventoryType
)
&&
StringUtils
.
isNotBlank
(
parts
[
1
]);
})
.
collect
(
Collectors
.
toList
());
for
(
Inventory
inv
:
hasLocationInvList
)
{
if
(
remainDeductQty
<=
0
)
break
;
Long
currentQty
=
Optional
.
ofNullable
(
inv
.
getQuantity
()).
orElse
(
0L
);
Long
deductQty
=
Math
.
min
(
remainDeductQty
,
currentQty
);
// 不扣负数
inv
.
setQuantity
(
currentQty
-
deductQty
);
if
(
inv
.
getQuantity
()
<=
0
)
{
inv
.
setInventoryStatus
(
0L
);
}
inv
.
setUpdateBy
(
updateUser
);
inv
.
setUpdateTime
(
updateTime
);
needUpdateList
.
add
(
inv
);
// 记录有库位扣减
deductRecords
.
add
(
buildDeductRecord
(
inv
,
inventoryType
,
deductQty
))
;
remainDeductQty
-=
deductQty
;
for
(
String
hasLocKey
:
hasLocKeys
)
{
if
(
remainDeductQty
<=
0
)
break
;
remainDeductQty
=
deductByInventoryKey
(
hasLocKey
,
remainDeductQty
,
updateUser
,
updateTime
,
inventoryFullMap
,
toUpdateInventoryMap
,
tempDeductRecords
)
;
}
}
// 最后仍有剩余(所有库存耗尽),允许扣最后一个库存为负数
if
(
remainDeductQty
>
0
&&
!
inventoryList
.
isEmpty
())
{
Inventory
lastInv
=
inventoryList
.
get
(
inventoryList
.
size
()
-
1
);
Long
deductQty
=
remainDeductQty
;
lastInv
.
setQuantity
(
Optional
.
ofNullable
(
lastInv
.
getQuantity
()).
orElse
(
0L
)
-
deductQty
);
lastInv
.
setInventoryStatus
(
0L
);
lastInv
.
setUpdateBy
(
updateUser
);
lastInv
.
setUpdateTime
(
updateTime
);
needUpdateList
.
add
(
lastInv
);
deductRecords
.
add
(
buildDeductRecord
(
lastInv
,
inventoryType
,
deductQty
));
remainDeductQty
=
0L
;
}
}
else
{
// 有库位:直接扣指定库位库存
Inventory
targetInv
=
inventoryList
.
stream
()
.
filter
(
inv
->
locationId
.
equals
(
inv
.
getLocationId
()))
.
findFirst
()
.
orElseThrow
(()
->
new
ServiceException
(
String
.
format
(
"物料[%s]库位[%s]无库存"
,
materialId
,
locationId
)));
Long
currentQty
=
Optional
.
ofNullable
(
targetInv
.
getQuantity
()).
orElse
(
0L
);
Long
deductQty
=
Math
.
min
(
remainDeductQty
,
currentQty
);
targetInv
.
setQuantity
(
currentQty
-
deductQty
);
if
(
targetInv
.
getQuantity
()
<=
0
)
{
targetInv
.
setInventoryStatus
(
0L
);
}
targetInv
.
setUpdateBy
(
updateUser
);
targetInv
.
setUpdateTime
(
updateTime
);
needUpdateList
.
add
(
targetInv
);
deductRecords
.
add
(
buildDeductRecord
(
targetInv
,
inventoryType
,
deductQty
));
remainDeductQty
-=
deductQty
;
// 有库位:扣指定「物料+库存类型+库位ID」的库存
String
targetKey
=
buildInventoryKey
(
materialId
,
locationId
,
inventoryType
);
remainDeductQty
=
deductByInventoryKey
(
targetKey
,
remainDeductQty
,
updateUser
,
updateTime
,
inventoryFullMap
,
toUpdateInventoryMap
,
tempDeductRecords
);
}
// 剩余部分扣为负数
if
(
remainDeductQty
>
0
)
{
Long
finalDeduct
=
remainDeductQty
;
targetInv
.
setQuantity
(
targetInv
.
getQuantity
()
-
finalDeduct
);
targetInv
.
setInventoryStatus
(
0L
);
deductRecords
.
add
(
buildDeductRecord
(
targetInv
,
inventoryType
,
finalDeduct
));
remainDeductQty
=
0L
;
// 步骤2:剩余部分扣负数(最后一个库存)
if
(
remainDeductQty
>
0
&&
!
tempDeductRecords
.
isEmpty
()
)
{
Map
<
String
,
Object
>
lastRecord
=
tempDeductRecords
.
get
(
tempDeductRecords
.
size
()
-
1
)
;
String
lastInvId
=
(
String
)
lastRecord
.
get
(
"inventoryId"
);
Inventory
lastInv
=
toUpdateInventoryMap
.
get
(
lastInvId
);
if
(
lastInv
==
null
)
{
throw
new
ServiceException
(
String
.
format
(
"物料[%s]库存类型[%s]扣减负数时未找到目标库存"
,
materialId
,
inventoryType
))
;
}
// 累计扣减负数数量
Long
finalDeduct
=
remainDeductQty
;
lastInv
.
setQuantity
(
lastInv
.
getQuantity
()
-
finalDeduct
);
lastInv
.
setInventoryStatus
(
0L
);
lastInv
.
setUpdateBy
(
updateUser
);
lastInv
.
setUpdateTime
(
updateTime
);
toUpdateInventoryMap
.
put
(
lastInvId
,
lastInv
);
// 合并到最后一条记录
lastRecord
.
put
(
"deductQty"
,
(
Long
)
lastRecord
.
get
(
"deductQty"
)
+
finalDeduct
);
remainDeductQty
=
0L
;
}
// 关联明细与扣减记录
if
(
itemId
!=
null
&&
!
deductRecords
.
isEmpty
())
{
deductRecordMap
.
put
(
itemId
,
deductRecords
);
// 步骤3:统一合并(按物料+库存类型+库位ID)
if
(
itemId
!=
null
&&
!
tempDeductRecords
.
isEmpty
())
{
List
<
Map
<
String
,
Object
>>
mergedRecords
=
mergeDeductRecords
(
tempDeductRecords
);
deductRecordMap
.
put
(
itemId
,
mergedRecords
);
}
// 校验是否扣减完成
if
(
remainDeductQty
>
0
)
{
throw
new
ServiceException
(
String
.
format
(
"物料[%s]库存类型[%s]扣减失败,剩余%d数量未扣减"
,
materialId
,
inventoryType
,
remainDeductQty
));
}
}
// 批量更新库存
if
(!
needUpdateList
.
isEmpty
())
{
// 批量更新库存(最终去重)
if
(!
toUpdateInventoryMap
.
isEmpty
())
{
List
<
Inventory
>
needUpdateList
=
new
ArrayList
<>(
toUpdateInventoryMap
.
values
());
inventoryMapper
.
batchUpdateInventory
(
needUpdateList
);
inventoryService
.
RefreshInventory
(
needUpdateList
.
stream
().
map
(
Inventory:
:
getId
).
distinct
().
collect
(
Collectors
.
toList
()));
}
...
...
@@ -339,82 +294,126 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
return
deductRecordMap
;
}
private
Map
<
String
,
Object
>
buildDeductRecord
(
Inventory
inv
,
String
inventoryType
,
Long
deductQty
)
{
Map
<
String
,
Object
>
record
=
new
HashMap
<>();
record
.
put
(
"inventoryId"
,
inv
.
getId
());
record
.
put
(
"locationId"
,
inv
.
getLocationId
());
record
.
put
(
"materialId"
,
inv
.
getMaterialId
());
record
.
put
(
"inventoryType"
,
inventoryType
);
record
.
put
(
"deductQty"
,
deductQty
);
return
record
;
}
/**
*
扣减单个库存的数量
* @param inventory
库存对象
*
按「物料ID+库位ID+库存类型」扣减指定库存
* @param inventory
Key 库存Key(物料ID_库位ID_库存类型)
* @param deductQty 待扣减数量
* @param updateUser 操作人
* @param updateTime 操作时间
* @return 剩余未扣减数量
*/
private
Long
deductSingleInventory
(
Inventory
inventory
,
Long
deductQty
,
String
updateUser
,
Date
updateTime
)
{
Long
currentQty
=
Optional
.
ofNullable
(
inventory
.
getQuantity
()).
orElse
(
0L
);
// 关键修改:不再限制扣减数量,直接扣减(允许库存变为负数)
Long
canDeductQty
=
deductQty
;
// 原逻辑:Math.min(deductQty, currentQty)
// 扣减数量(允许负数)
Long
newQty
=
currentQty
-
canDeductQty
;
inventory
.
setQuantity
(
newQty
);
// 扣减后≤0,设置状态为0(即使是负数也设置)
if
(
newQty
<=
0
)
{
inventory
.
setInventoryStatus
(
0L
);
private
Long
deductByInventoryKey
(
String
inventoryKey
,
Long
deductQty
,
String
updateUser
,
Date
updateTime
,
Map
<
String
,
Inventory
>
inventoryFullMap
,
Map
<
String
,
Inventory
>
toUpdateInventoryMap
,
List
<
Map
<
String
,
Object
>>
tempDeductRecords
)
{
Inventory
inv
=
inventoryFullMap
.
get
(
inventoryKey
);
if
(
inv
==
null
)
{
return
deductQty
;
}
// 补充审计字段
inventory
.
setUpdateBy
(
updateUser
);
inventory
.
setUpdateTime
(
updateTime
);
Long
currentQty
=
Optional
.
ofNullable
(
inv
.
getQuantity
()).
orElse
(
0L
);
Long
canDeduct
=
Math
.
min
(
deductQty
,
currentQty
);
// 更新库存状态
inv
.
setQuantity
(
currentQty
-
canDeduct
);
inv
.
setInventoryStatus
(
inv
.
getQuantity
()
<=
0
?
0L
:
1L
);
inv
.
setUpdateBy
(
updateUser
);
inv
.
setUpdateTime
(
updateTime
);
toUpdateInventoryMap
.
put
(
inv
.
getId
(),
inv
);
// 返回剩余未扣减数量(扣减全部,剩余为0)
return
0L
;
// 原逻辑:deductQty - canDeductQty
// 收集扣减记录(按统一维度)
Map
<
String
,
Object
>
record
=
buildDeductRecord
(
inv
,
inv
.
getInventoryType
().
toString
(),
canDeduct
);
tempDeductRecords
.
add
(
record
);
// 返回剩余未扣减数量
return
deductQty
-
canDeduct
;
}
/**
* 预加载库存分组Map
* Key=物料ID_库存类型 Value=该维度下所有库位的库存列表(含无库位)
* 移除仓库维度
* 合并同一「物料ID+库存类型+库位ID」的扣减记录(最终版)
* 核心:按「物料ID_库位ID_库存类型」合并,保留库位信息准确性
*/
private
Map
<
String
,
List
<
Inventory
>>
loadInventoryGroupMap
()
{
Inventory
query
=
new
Inventory
();
query
.
setInventoryStatus
(
1L
);
query
.
setIsUsed
(
1L
);
List
<
Inventory
>
inventoryList
=
inventoryService
.
selectInventoryList
(
query
);
private
List
<
Map
<
String
,
Object
>>
mergeDeductRecords
(
List
<
Map
<
String
,
Object
>>
deductRecords
)
{
if
(
CollectionUtils
.
isEmpty
(
deductRecords
))
{
return
Collections
.
emptyList
();
}
if
(
CollectionUtils
.
isEmpty
(
inventoryList
))
{
// 合并Key:物料ID_库位ID_库存类型
Map
<
String
,
Map
<
String
,
Object
>>
mergeMap
=
new
LinkedHashMap
<>();
for
(
Map
<
String
,
Object
>
record
:
deductRecords
)
{
String
materialId
=
(
String
)
record
.
get
(
"materialId"
);
String
locationId
=
(
String
)
record
.
get
(
"locationId"
);
String
inventoryType
=
(
String
)
record
.
get
(
"inventoryType"
);
Long
deductQty
=
(
Long
)
record
.
get
(
"deductQty"
);
if
(
StringUtils
.
isBlank
(
materialId
)
||
StringUtils
.
isBlank
(
inventoryType
)
||
deductQty
<=
0
)
{
continue
;
}
String
mergeKey
=
buildInventoryKey
(
materialId
,
locationId
,
inventoryType
);
if
(
mergeMap
.
containsKey
(
mergeKey
))
{
Map
<
String
,
Object
>
existRecord
=
mergeMap
.
get
(
mergeKey
);
existRecord
.
put
(
"deductQty"
,
(
Long
)
existRecord
.
get
(
"deductQty"
)
+
deductQty
);
}
else
{
Map
<
String
,
Object
>
newRecord
=
new
HashMap
<>(
record
);
mergeMap
.
put
(
mergeKey
,
newRecord
);
}
}
return
new
ArrayList
<>(
mergeMap
.
values
());
}
/**
* 构建库存Key:物料ID_库位ID_库存类型
*/
private
String
buildInventoryKey
(
String
materialId
,
String
locationId
,
String
inventoryType
)
{
return
String
.
join
(
"_"
,
Optional
.
ofNullable
(
materialId
).
orElse
(
""
),
Optional
.
ofNullable
(
locationId
).
orElse
(
""
),
Optional
.
ofNullable
(
inventoryType
).
orElse
(
""
)
);
}
/**
* 构建扣减记录
*/
private
Map
<
String
,
Object
>
buildDeductRecord
(
Inventory
inv
,
String
inventoryType
,
Long
deductQty
)
{
Map
<
String
,
Object
>
record
=
new
HashMap
<>();
record
.
put
(
"inventoryId"
,
inv
.
getId
());
record
.
put
(
"locationId"
,
inv
.
getLocationId
());
record
.
put
(
"materialId"
,
inv
.
getMaterialId
());
record
.
put
(
"inventoryType"
,
inventoryType
);
record
.
put
(
"deductQty"
,
deductQty
);
return
record
;
}
/**
* 预加载库存全量Map(按「物料ID_库位ID_库存类型」为Key)
*/
private
Map
<
String
,
Inventory
>
loadInventoryFullMap
()
{
Collection
<
Inventory
>
allInventory
=
InventoryCache
.
getAll
().
values
();
if
(
CollectionUtils
.
isEmpty
(
allInventory
))
{
return
Collections
.
emptyMap
();
}
// 按「物料ID_库存类型」分组(移除仓库维度)
return
inventoryList
.
stream
()
.
collect
(
Collectors
.
groupingB
y
(
inv
->
String
.
join
(
"_"
,
Optional
.
ofNullable
(
inv
.
getMaterialId
()).
orElse
(
""
),
return
allInventory
.
stream
()
.
collect
(
Collectors
.
toMap
(
inv
->
buildInventoryKe
y
(
inv
.
getMaterialId
()
,
inv
.
getLocationId
(
),
Optional
.
ofNullable
(
inv
.
getInventoryType
()).
map
(
String:
:
valueOf
).
orElse
(
""
)
),
HashMap:
:
new
,
Collectors
.
toList
()
inv
->
inv
,
(
k1
,
k2
)
->
k1
,
// 重复Key保留第一个
LinkedHashMap:
:
new
));
}
/**
* 构建扣减数量Map
* Key规则:物料ID_库位ID_库存类型
(移除仓库维度)
* Key规则:物料ID_库位ID_库存类型
*/
private
Map
<
String
,
Long
>
buildDeductQtyMap
(
List
<
OutboundOrderItems
>
items
)
{
Map
<
String
,
Long
>
deductQtyMap
=
new
HashMap
<>();
for
(
OutboundOrderItems
item
:
items
)
{
String
key
=
buildDeductKey
(
item
);
// 累加扣减数量
Long
qty
=
Optional
.
ofNullable
(
item
.
getActualQuantity
()).
orElse
(
0L
);
deductQtyMap
.
put
(
key
,
deductQtyMap
.
getOrDefault
(
key
,
0L
)
+
qty
);
}
...
...
@@ -423,18 +422,17 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
/**
* 构建明细的扣减Key
* 移除仓库维度,Key规则:物料ID_库位ID_库存类型
*/
private
String
buildDeductKey
(
OutboundOrderItems
item
)
{
return
String
.
join
(
"_"
,
Optional
.
ofNullable
(
item
.
getMaterialId
()).
orElse
(
""
),
Optional
.
ofNullable
(
item
.
getLocationId
()).
orElse
(
""
),
return
buildInventoryKey
(
item
.
getMaterialId
(
),
item
.
getLocationId
(
),
Optional
.
ofNullable
(
item
.
getInventoryType
()).
map
(
String:
:
valueOf
).
orElse
(
""
)
);
}
@Override
public
List
<
Map
<
String
,
String
>>
outboundOrdersTopTenByQuantity
()
{
public
List
<
Map
<
String
,
String
>>
outboundOrdersTopTenByQuantity
()
{
return
outboundOrdersMapper
.
SelectOutboundOrdersMaterialsTopTenByQuantity
();
}
...
...
@@ -450,8 +448,6 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
/**
* 新增出库单明细信息
*
* @param outboundOrders 出库单主对象
*/
public
void
insertOutboundOrderItems
(
OutboundOrders
outboundOrders
)
{
List
<
OutboundOrderItems
>
outboundOrderItemsList
=
outboundOrders
.
getOutboundOrderItemsList
();
...
...
@@ -466,20 +462,23 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
throw
new
RuntimeException
(
"库存被修改请重新确认"
);
}
// 合并相同库存维度的明细记录(简化版)
List
<
OutboundOrderItems
>
mergedItemsList
=
mergeSameInventoryItems
(
outboundOrderItemsList
);
// 为明细设置订单ID和主键ID
for
(
OutboundOrderItems
items
:
outboundOrder
ItemsList
)
{
for
(
OutboundOrderItems
items
:
merged
ItemsList
)
{
items
.
setOutboundOrderId
(
id
);
items
.
setOrderId
(
outboundOrders
.
getOrderId
());
items
.
setId
(
UUID
.
randomUUID
().
toString
().
replace
(
"-"
,
""
));
}
// 批量插入出库单明细
outboundOrdersMapper
.
batchOutboundOrderItems
(
outboundOrder
ItemsList
);
outboundOrdersMapper
.
batchOutboundOrderItems
(
merged
ItemsList
);
// 拷贝明细到日志列表
List
<
String
>
inventoryIds
=
new
ArrayList
<>();
List
<
OutboundOrderLog
>
outboundOrderLogs
=
new
ArrayList
<>();
for
(
OutboundOrderItems
items
:
outboundOrder
ItemsList
)
{
for
(
OutboundOrderItems
items
:
merged
ItemsList
)
{
OutboundOrderLog
log
=
new
OutboundOrderLog
();
BeanUtils
.
copyProperties
(
items
,
log
);
log
.
setOrderId
(
items
.
getOutboundOrderId
());
...
...
@@ -496,6 +495,40 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
}
}
/**
* 简化版:合并相同库存维度的明细记录
* 按「物料ID+库存类型+库位ID」维度合并,仅累加实际数量
*/
private
List
<
OutboundOrderItems
>
mergeSameInventoryItems
(
List
<
OutboundOrderItems
>
itemsList
)
{
if
(
CollectionUtils
.
isEmpty
(
itemsList
))
{
return
Collections
.
emptyList
();
}
// 按「物料ID+库存类型+库位ID」分组合并
Map
<
String
,
OutboundOrderItems
>
mergeMap
=
new
LinkedHashMap
<>();
for
(
OutboundOrderItems
item
:
itemsList
)
{
String
mergeKey
=
buildInventoryKey
(
item
.
getMaterialId
(),
item
.
getLocationId
(),
Optional
.
ofNullable
(
item
.
getInventoryType
()).
map
(
String:
:
valueOf
).
orElse
(
""
)
);
if
(
mergeMap
.
containsKey
(
mergeKey
))
{
// 只合并实际数量
OutboundOrderItems
existItem
=
mergeMap
.
get
(
mergeKey
);
Long
newActualQty
=
Optional
.
ofNullable
(
existItem
.
getActualQuantity
()).
orElse
(
0L
)
+
Optional
.
ofNullable
(
item
.
getActualQuantity
()).
orElse
(
0L
);
existItem
.
setActualQuantity
(
newActualQty
);
}
else
{
// 新记录直接放入
mergeMap
.
put
(
mergeKey
,
item
);
}
}
return
new
ArrayList
<>(
mergeMap
.
values
());
}
@Transactional
(
rollbackFor
=
Exception
.
class
)
@Override
public
String
importOutboundOrders
(
List
<
OutboundTemplateVO
>
inboundOrdersList
,
Boolean
isUpdateSupport
,
String
operName
,
Integer
orderType
)
{
...
...
@@ -519,7 +552,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
Map
<
String
,
List
<
OutboundOrderItems
>>
validItemMap
=
new
HashMap
<>();
boolean
hasValidateError
=
false
;
// 3. 预加载映射缓存
(移除仓库相关映射)
// 3. 预加载映射缓存
Map
<
String
,
String
>
sapToMaterialIdMap
=
loadSapToMaterialIdMap
();
Map
<
String
,
String
>
locationNameToIdMap
=
loadLocationNameToIdMap
();
Map
<
String
,
String
>
ownerNameToIdMap
=
loadOwnerNameToIdMap
();
...
...
@@ -530,7 +563,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
.
filter
(
vo
->
StringUtils
.
isNotBlank
(
vo
.
getOrderId
()))
.
collect
(
Collectors
.
groupingBy
(
OutboundTemplateVO:
:
getOrderId
));
// 5. 数据验证(仅
保留新增逻辑,检测到已存在则抛异常
)
// 5. 数据验证(仅
新增逻辑
)
for
(
Map
.
Entry
<
String
,
List
<
OutboundTemplateVO
>>
entry
:
orderGroupMap
.
entrySet
())
{
String
orderId
=
entry
.
getKey
();
List
<
OutboundTemplateVO
>
voList
=
entry
.
getValue
();
...
...
@@ -539,7 +572,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
try
{
OutboundTemplateVO
firstVO
=
voList
.
get
(
0
);
// 检查出库单是否已存在
(仅新增,存在则抛异常)
// 检查出库单是否已存在
OutboundOrders
outboundOrdersQuery
=
new
OutboundOrders
();
outboundOrdersQuery
.
setOrderId
(
orderId
);
List
<
OutboundOrders
>
existMains
=
outboundOrdersMapper
.
selectOutboundOrdersList
(
outboundOrdersQuery
);
...
...
@@ -548,7 +581,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
throw
new
ServiceException
(
String
.
format
(
"入库单号【%s】已存在,当前系统仅支持新增,不支持更新"
,
orderId
));
}
//
仅新增逻辑:
构建新出库单主数据
// 构建新出库单主数据
mainDO
=
new
OutboundOrders
();
BeanUtils
.
copyProperties
(
firstVO
,
mainDO
,
"sapNo"
,
"materialName"
,
"plannedQuantity"
,
"actualQuantity"
,
...
...
@@ -591,6 +624,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
mainDO
.
setUpdateTime
(
now
);
mainDO
.
setUpdateUserCode
(
operId
);
mainDO
.
setSortNo
(
Optional
.
ofNullable
(
mainDO
.
getSortNo
()).
orElse
(
0L
));
mainDO
.
setIsImport
(
0L
);
// 明细校验
for
(
int
i
=
0
;
i
<
voList
.
size
();
i
++)
{
...
...
@@ -645,22 +679,45 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
// 库存校验(包含inventoryType)- 有库位才校验,无库位直接跳过
if
(
StringUtils
.
isNotBlank
(
locationName
))
{
String
inventoryTypeStr
=
Optional
.
ofNullable
(
orderType
).
map
(
String:
:
valueOf
).
orElse
(
""
);
String
inventoryMatchKey
=
String
.
join
(
"_"
,
materialId
,
locationId
,
inventoryTypeStr
);
String
inventoryMatchKey
=
buildInventoryKey
(
materialId
,
locationId
,
inventoryTypeStr
);
AbstractMap
.
SimpleEntry
<
String
,
Long
>
inventoryEntry
=
inventoryTOIdMap
.
get
(
inventoryMatchKey
);
String
inventoryId
=
""
;
if
(
inventoryEntry
==
null
)
{
throw
new
ServiceException
(
String
.
format
(
"入库单号【%s】第%d条明细:物料【%s】+库位【%s】+库存类型【%s】组合的库存记录不存在"
,
orderId
,
lineNo
,
vo
.
getMaterialName
(),
vo
.
getLocationName
(),
inventoryTypeStr
));
Inventory
inventory
=
new
Inventory
();
BeanUtils
.
copyProperties
(
itemDO
,
inventory
);
inventoryId
=
UUID
.
randomUUID
().
toString
();
inventory
.
setInventoryType
(
Long
.
valueOf
(
orderType
));
inventory
.
setBatchId
(
itemDO
.
getBatchCode
());
inventory
.
setWarehousesId
(
"local"
);
inventory
.
setQuantity
(
0L
);
inventory
.
setInventoryStatus
(
1L
);
inventory
.
setIsUsed
(
1L
);
inventory
.
setId
(
inventoryId
);
int
insertCount
=
inventoryMapper
.
insertInventory
(
inventory
);
if
(
insertCount
!=
1
)
{
throw
new
ServiceException
(
String
.
format
(
"入库单号【%s】第%d条明细新增库存失败,插入行数为0"
,
orderId
,
lineNo
));
}
// 插入后直接添加到全局缓存
InventoryCache
.
addInventory
(
inventoryMatchKey
,
inventory
);
sqlSessionTemplate
.
clearCache
();
sqlSessionTemplate
.
flushStatements
();
}
else
{
System
.
out
.
println
(
"库存已存在,使用已有库存ID:"
+
inventoryEntry
.
getKey
());
inventoryId
=
inventoryEntry
.
getKey
();
}
itemDO
.
setInventoryId
(
inventory
Entry
.
getKey
()
);
itemDO
.
setInventoryId
(
inventory
Id
);
}
// 无库位时不校验库存,也不设置inventoryId
itemDOList
.
add
(
itemDO
);
}
// 合并相同库存维度的明细
List
<
OutboundOrderItems
>
mergedItemList
=
mergeSameInventoryItems
(
itemDOList
);
validMainMap
.
put
(
orderId
,
mainDO
);
validItemMap
.
put
(
orderId
,
itemDO
List
);
validItemMap
.
put
(
orderId
,
mergedItem
List
);
}
catch
(
Exception
e
)
{
hasValidateError
=
true
;
...
...
@@ -675,27 +732,26 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
throw
new
ServiceException
(
String
.
format
(
"验证失败,导入终止!失败详情:%s"
,
failureMsg
.
toString
()));
}
// 7. 执行新增操作
(仅新增,无更新)
// 7. 执行新增操作
Map
<
String
,
List
<
OutboundOrderItems
>>
allItemListMap
=
new
HashMap
<>();
for
(
Map
.
Entry
<
String
,
OutboundOrders
>
entry
:
validMainMap
.
entrySet
())
{
String
orderId
=
entry
.
getKey
();
OutboundOrders
mainDO
=
entry
.
getValue
();
List
<
OutboundOrderItems
>
itemDOList
=
validItemMap
.
get
(
orderId
);
//
仅
新增主单
// 新增主单
outboundOrdersMapper
.
insertOutboundOrders
(
mainDO
);
totalMainSuccess
++;
successMsg
.
append
(
String
.
format
(
"入库单号【%s】已新增;\n"
,
orderId
));
// 插入明细
(仅新增,无删除操作)
// 插入明细
if
(!
CollectionUtils
.
isEmpty
(
itemDOList
))
{
// 批量插入新明细
int
itemSuccess
=
outboundOrderItemsMapper
.
batchInsertOutboundOrderItems
(
itemDOList
);
totalItemSuccess
+=
itemSuccess
;
int
itemFail
=
itemDOList
.
size
()
-
itemSuccess
;
totalItemFailure
+=
itemFail
;
successMsg
.
append
(
String
.
format
(
"入库单号【%s】成功导入%d条物料明细;\n"
,
orderId
,
itemSuccess
));
successMsg
.
append
(
String
.
format
(
"入库单号【%s】成功导入%d条物料明细
(已合并相同库存维度)
;\n"
,
orderId
,
itemSuccess
));
if
(
itemFail
>
0
)
{
failureMsg
.
append
(
String
.
format
(
"入库单号【%s】有%d条物料明细导入失败;\n"
,
orderId
,
itemFail
));
}
...
...
@@ -704,28 +760,36 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
}
}
// 8. 执行库存扣减并处理无库位明细拆分
for
(
Map
.
Entry
<
String
,
List
<
OutboundOrderItems
>>
entry
:
allItemListMap
.
entrySet
())
{
List
<
OutboundOrderItems
>
itemList
=
entry
.
getValue
();
// 执行库存扣减,获取无库位明细的扣减记录
Map
<
String
,
List
<
Map
<
String
,
Object
>>>
deductRecordMap
=
deductInventory
(
itemList
,
operId
,
now
);
// 处理无库位明细拆分:整理有效明细后统一插入
if
(!
deductRecordMap
.
isEmpty
())
{
handleNoLocationItemSplit
(
itemList
,
deductRecordMap
,
operId
,
now
);
// 8. 异步执行库存扣减和无库位明细拆分
CompletableFuture
.
runAsync
(()
->
{
try
{
for
(
Map
.
Entry
<
String
,
List
<
OutboundOrderItems
>>
entry
:
allItemListMap
.
entrySet
())
{
List
<
OutboundOrderItems
>
itemList
=
entry
.
getValue
();
// 执行库存扣减
Map
<
String
,
List
<
Map
<
String
,
Object
>>>
deductRecordMap
=
deductInventory
(
itemList
,
operId
,
now
);
// 处理无库位明细拆分
boolean
hasNoLocationItem
=
itemList
.
stream
()
.
anyMatch
(
item
->
StringUtils
.
isBlank
(
item
.
getLocationId
()));
if
(
hasNoLocationItem
&&
!
deductRecordMap
.
isEmpty
())
{
handleNoLocationItemSplit
(
itemList
,
deductRecordMap
,
operId
,
now
);
}
}
}
catch
(
Exception
e
)
{
e
.
printStackTrace
();
}
}
}
);
// 9. 结果汇总
if
(
totalMainFailure
>
0
||
totalItemFailure
>
0
)
{
String
finalFailureMsg
=
String
.
format
(
"导入结果:成功新增%d个入库单,失败%d个;成功导入%d条明细,失败%d条。失败详情:%s"
,
"导入结果:成功新增%d个入库单,失败%d个;成功导入%d条明细
(已合并相同库存维度)
,失败%d条。失败详情:%s"
,
totalMainSuccess
,
totalMainFailure
,
totalItemSuccess
,
totalItemFailure
,
failureMsg
.
toString
()
);
throw
new
ServiceException
(
finalFailureMsg
);
}
else
{
String
finalSuccessMsg
=
String
.
format
(
"恭喜您,数据已全部导入成功!共新增%d个入库单,成功导入%d条物料明细。详情:%s"
,
"恭喜您,数据已全部导入成功!共新增%d个入库单,成功导入%d条物料明细
(已合并相同库存维度)
。详情:%s"
,
totalMainSuccess
,
totalItemSuccess
,
successMsg
.
toString
()
);
return
finalSuccessMsg
;
...
...
@@ -733,10 +797,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
}
/**
* 处理无库位明细拆分:
* 1. 过滤无效扣减(≤0),按库位合并有效扣减数量
* 2. 仅新增场景:删除临时明细,插入整理后的有效明细
* 3. 严格保留原逻辑,仅适配新增场景
* 处理无库位明细拆分(适配新的合并维度)
*/
private
void
handleNoLocationItemSplit
(
List
<
OutboundOrderItems
>
itemList
,
Map
<
String
,
List
<
Map
<
String
,
Object
>>>
deductRecordMap
,
...
...
@@ -744,39 +805,27 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
List
<
OutboundOrderItems
>
newValidItemList
=
new
ArrayList
<>();
Set
<
String
>
orderIdSet
=
new
HashSet
<>();
// 第一步:遍历扣减记录,整理有效明细(过滤≤0,合并同库位)
for
(
OutboundOrderItems
item
:
itemList
)
{
String
itemId
=
item
.
getId
();
List
<
Map
<
String
,
Object
>>
deductRecords
=
deductRecordMap
.
get
(
itemId
);
if
(
CollectionUtils
.
isEmpty
(
deductRecords
))
continue
;
// 收集订单ID(用于删除临时明细)
orderIdSet
.
add
(
item
.
getOutboundOrderId
());
// 按库位合并扣减数量,过滤≤0的无效记录
Map
<
String
,
Long
>
locationQtyMap
=
new
HashMap
<>();
// 直接遍历合并后的记录(已按物料+库存类型+库位ID合并)
for
(
Map
<
String
,
Object
>
rec
:
deductRecords
)
{
String
inventoryId
=
(
String
)
rec
.
get
(
"inventoryId"
);
String
locId
=
(
String
)
rec
.
get
(
"locationId"
);
Long
deductQty
=
(
Long
)
rec
.
get
(
"deductQty"
);
if
(
deductQty
<=
0
)
continue
;
// 过滤无效扣减
locationQtyMap
.
put
(
locId
,
locationQtyMap
.
getOrDefault
(
locId
,
0L
)
+
deductQty
);
}
Long
validQty
=
(
Long
)
rec
.
get
(
"deductQty"
);
// 生成有效明细
for
(
Map
.
Entry
<
String
,
Long
>
entry
:
locationQtyMap
.
entrySet
())
{
String
locId
=
entry
.
getKey
();
Long
validQty
=
entry
.
getValue
();
if
(
validQty
<=
0
||
StringUtils
.
isBlank
(
inventoryId
))
continue
;
OutboundOrderItems
newItem
=
new
OutboundOrderItems
();
BeanUtils
.
copyProperties
(
item
,
newItem
);
newItem
.
setId
(
UUID
.
randomUUID
().
toString
().
replace
(
"-"
,
""
));
newItem
.
setLocationId
(
locId
);
newItem
.
setActualQuantity
(
validQty
);
newItem
.
setInventoryId
(
deductRecords
.
stream
()
.
filter
(
r
->
locId
.
equals
(
r
.
get
(
"locationId"
)))
.
findFirst
()
.
map
(
r
->
(
String
)
r
.
get
(
"inventoryId"
))
.
orElse
(
""
));
newItem
.
setInventoryId
(
inventoryId
);
newItem
.
setCreateBy
(
operId
);
newItem
.
setCreateTime
(
now
);
newItem
.
setUpdateBy
(
operId
);
...
...
@@ -785,18 +834,19 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
}
}
//
第二步:删除临时明细(导入时插入的原始明细)
//
删除临时明细
for
(
String
orderId
:
orderIdSet
)
{
outboundOrderItemsMapper
.
deleteOutboundOrderItemsByOrderId
(
orderId
);
}
// 第三步:批量插入整理后的有效明细
if
(!
newValidItemList
.
isEmpty
())
{
outboundOrderItemsMapper
.
batchInsertOutboundOrderItems
(
newValidItemList
);
// 批量插入有效明细(插入前再次合并)
List
<
OutboundOrderItems
>
mergedNewItems
=
mergeSameInventoryItems
(
newValidItemList
);
if
(!
mergedNewItems
.
isEmpty
())
{
outboundOrderItemsMapper
.
batchInsertOutboundOrderItems
(
mergedNewItems
);
//
同步生成日志(保留原日志逻辑)
//
生成日志
List
<
OutboundOrderLog
>
logList
=
new
ArrayList
<>();
for
(
OutboundOrderItems
newItem
:
newValidItemList
)
{
for
(
OutboundOrderItems
newItem
:
mergedNewItems
)
{
OutboundOrderLog
log
=
new
OutboundOrderLog
();
BeanUtils
.
copyProperties
(
newItem
,
log
);
log
.
setOrderId
(
newItem
.
getOutboundOrderId
());
...
...
@@ -807,7 +857,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
}
}
// ========== 预加载映射辅助方法
(移除仓库相关)
==========
// ========== 预加载映射辅助方法 ==========
private
Map
<
String
,
String
>
loadSapToMaterialIdMap
()
{
List
<
Materials
>
materialsList
=
materialsService
.
selectMaterialsList
(
new
Materials
());
if
(
CollectionUtils
.
isEmpty
(
materialsList
))
{
...
...
@@ -853,8 +903,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
}
/**
* 加载库存映射Map(移除仓库维度)
* Key=物料ID_库位ID_库存类型
* 加载库存映射Map(按新维度)+ 同步填充全局缓存
*/
private
Map
<
String
,
AbstractMap
.
SimpleEntry
<
String
,
Long
>>
loadInventoryTOIdMap
()
{
Inventory
inventory
=
new
Inventory
();
...
...
@@ -862,40 +911,35 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
inventory
.
setIsUsed
(
1L
);
List
<
Inventory
>
inventoryList
=
inventoryService
.
selectInventoryList
(
inventory
);
// 清空全局缓存
InventoryCache
.
clear
();
if
(
CollectionUtils
.
isEmpty
(
inventoryList
))
{
return
Collections
.
emptyMap
();
}
Map
<
String
,
AbstractMap
.
SimpleEntry
<
String
,
Long
>>
emptyLocationMap
=
inventoryList
.
stream
()
return
inventoryList
.
stream
()
.
filter
(
inv
->
StringUtils
.
isNotBlank
(
inv
.
getMaterialId
())
&&
inv
.
getInventoryType
()
!=
null
&&
StringUtils
.
isNotBlank
(
inv
.
getId
())
&&
StringUtils
.
isBlank
(
inv
.
getLocationId
()))
.
collect
(
Collectors
.
toMap
(
inv
->
String
.
join
(
"_"
,
inv
.
getMaterialId
().
trim
(),
""
,
inv
.
getInventoryType
().
toString
()),
inv
->
new
AbstractMap
.
SimpleEntry
<>(
inv
.
getId
().
trim
(),
Optional
.
ofNullable
(
inv
.
getQuantity
()).
orElse
(
0L
)),
(
k1
,
k2
)
->
k1
,
HashMap:
:
new
));
Map
<
String
,
AbstractMap
.
SimpleEntry
<
String
,
Long
>>
nonEmptyLocationMap
=
inventoryList
.
stream
()
.
filter
(
inv
->
StringUtils
.
isNotBlank
(
inv
.
getMaterialId
())
&&
StringUtils
.
isNotBlank
(
inv
.
getLocationId
())
&&
StringUtils
.
isNotBlank
(
inv
.
getId
())
&&
inv
.
getInventoryType
()
!=
null
)
&&
StringUtils
.
isNotBlank
(
inv
.
getId
()))
.
peek
(
inv
->
{
// 同步到全局缓存(按新维度)
String
key
=
buildInventoryKey
(
inv
.
getMaterialId
(),
inv
.
getLocationId
(),
inv
.
getInventoryType
().
toString
()
);
InventoryCache
.
addInventory
(
key
,
inv
);
})
.
collect
(
Collectors
.
toMap
(
inv
->
String
.
join
(
"_"
,
inv
.
getMaterialId
().
trim
(),
inv
.
getLocationId
().
trim
(),
inv
.
getInventoryType
().
toString
()),
inv
->
buildInventoryKey
(
inv
.
getMaterialId
(),
inv
.
getLocationId
(),
inv
.
getInventoryType
().
toString
()
),
inv
->
new
AbstractMap
.
SimpleEntry
<>(
inv
.
getId
().
trim
(),
Optional
.
ofNullable
(
inv
.
getQuantity
()).
orElse
(
0L
)),
(
k1
,
k2
)
->
k1
,
HashMap:
:
new
));
emptyLocationMap
.
putAll
(
nonEmptyLocationMap
);
return
emptyLocationMap
;
}
}
\ No newline at end of file
ruoyi-inventory/src/main/java/com/ruoyi/inventory/utils/InventoryCache.java
0 → 100644
View file @
d04916a9
package
com
.
ruoyi
.
inventory
.
utils
;
import
com.ruoyi.inventory.domain.Inventory
;
import
java.util.AbstractMap
;
import
java.util.Map
;
import
java.util.concurrent.ConcurrentHashMap
;
/**
* 库存映射全局缓存(解决导入新增库存即时可见问题)
*/
public
class
InventoryCache
{
// 并发安全Map,Key=物料ID_库位ID_库存类型,Value=库存对象
private
static
final
Map
<
String
,
Inventory
>
INVENTORY_MAP
=
new
ConcurrentHashMap
<>();
// 添加库存(直接存对象,避免参数不匹配)
public
static
void
addInventory
(
String
key
,
Inventory
inventory
)
{
INVENTORY_MAP
.
put
(
key
,
inventory
);
}
// 获取库存
public
static
Inventory
getInventory
(
String
key
)
{
return
INVENTORY_MAP
.
get
(
key
);
}
// 清空缓存
public
static
void
clear
()
{
INVENTORY_MAP
.
clear
();
}
// 获取全部缓存(核心:供loadInventoryGroupMap直接读取)
public
static
Map
<
String
,
Inventory
>
getAll
()
{
return
INVENTORY_MAP
;
}
}
\ No newline at end of file
ruoyi-inventory/src/main/resources/mapper/inventory/InventoryMapper.xml
View file @
d04916a9
...
...
@@ -435,7 +435,7 @@
and inventory_status = '1'
]]>
</select>
<insert
id=
"insertInventory"
parameterType=
"Inventory"
>
<insert
id=
"insertInventory"
parameterType=
"Inventory"
flushCache=
"true"
>
insert into inventory
<trim
prefix=
"("
suffix=
")"
suffixOverrides=
","
>
<if
test=
"id != null"
>
id,
</if>
...
...
@@ -563,6 +563,7 @@ and inventory_status = '1'
left join inventory i on i.material_id = m.id
and i.is_used = 1
and i.unit_price > 0
and i.inventory_status=1
and i.last_inbound_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
and i.last_inbound_time
<
DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')
where
...
...
@@ -580,6 +581,7 @@ and inventory_status = '1'
from materials m
left join inventory i on i.material_id = m.id
and i.is_used = 1
and i.inventory_status=1
and i.last_inbound_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
and i.last_inbound_time
<
DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')
where
...
...
ruoyi-inventory/src/main/resources/mapper/inventory/OutboundOrdersMapper.xml
View file @
d04916a9
...
...
@@ -16,7 +16,6 @@
<result
property=
"ownerId"
column=
"owner_id"
/>
<result
property=
"ownerName"
column=
"owner_name"
/>
<result
property=
"orderStatus"
column=
"order_status"
/>
<!-- 保留原字段映射,新增startDate/endDate用于查询 -->
<result
property=
"inboundDate"
column=
"inbound_date"
/>
<result
property=
"destination"
column=
"destination"
/>
<result
property=
"totalPlannedQuantity"
column=
"total_planned_quantity"
/>
...
...
@@ -29,6 +28,8 @@
<result
property=
"createUserCode"
column=
"create_user_code"
/>
<result
property=
"updateTime"
column=
"update_time"
/>
<result
property=
"updateUserCode"
column=
"update_user_code"
/>
<!-- 新增 isImport 字段映射 -->
<result
property=
"isImport"
column=
"is_import"
/>
</resultMap>
<resultMap
id=
"OutboundOrdersOutboundOrderItemsResult"
type=
"com.ruoyi.inventory.domain.OutboundOrders"
extends=
"OutboundOrdersResult"
>
...
...
@@ -94,7 +95,8 @@
oo.create_time,
oo.create_user_code,
oo.update_time,
oo.update_user_code
oo.update_user_code,
oo.is_import
from outbound_orders oo
left join owners o on oo.owner_id = o.id and o.is_used = 1
left join warehouses w on oo.warehouse_id = w.id and w.is_used = 1 and w.is_enabled = 1
...
...
@@ -111,7 +113,8 @@
<if
test=
"warehouseId != null and warehouseId != ''"
>
and oo.warehouse_id = #{warehouseId}
</if>
<if
test=
"ownerId != null and ownerId != ''"
>
and oo.owner_id = #{ownerId}
</if>
<if
test=
"orderStatus != null "
>
and oo.order_status = #{orderStatus}
</if>
<!-- 替换为时间段查询:startDate和endDate -->
<!-- 新增 isImport 查询条件 -->
<if
test=
"isImport != null "
>
and oo.is_import = #{isImport}
</if>
<if
test=
"startDate != null"
>
and oo.inbound_date
>
= #{startDate}
</if>
<if
test=
"endDate != null"
>
and oo.inbound_date
<
= #{endDate}
</if>
<if
test=
"destination != null and destination != ''"
>
and oo.destination = #{destination}
</if>
...
...
@@ -147,7 +150,8 @@
oo.create_time,
oo.create_user_code,
oo.update_time,
oo.update_user_code
oo.update_user_code,
oo.is_import
from outbound_orders oo
left join owners o on oo.owner_id = o.id
left join warehouses w on oo.warehouse_id = w.id
...
...
@@ -202,7 +206,6 @@
<if
test=
"warehouseId != null"
>
warehouse_id,
</if>
<if
test=
"ownerId != null"
>
owner_id,
</if>
<if
test=
"orderStatus != null"
>
order_status,
</if>
<!-- 保留inboundDate字段的插入(业务字段仍需存储) -->
<if
test=
"inboundDate != null"
>
inbound_date,
</if>
<if
test=
"destination != null"
>
destination,
</if>
<if
test=
"totalPlannedQuantity != null"
>
total_planned_quantity,
</if>
...
...
@@ -215,6 +218,8 @@
<if
test=
"createUserCode != null"
>
create_user_code,
</if>
<if
test=
"updateTime != null"
>
update_time,
</if>
<if
test=
"updateUserCode != null"
>
update_user_code,
</if>
<!-- 新增 is_import 字段插入 -->
<if
test=
"isImport != null"
>
is_import,
</if>
</trim>
<trim
prefix=
"values ("
suffix=
")"
suffixOverrides=
","
>
<if
test=
"id != null"
>
#{id},
</if>
...
...
@@ -238,6 +243,8 @@
<if
test=
"createUserCode != null"
>
#{createUserCode},
</if>
<if
test=
"updateTime != null"
>
#{updateTime},
</if>
<if
test=
"updateUserCode != null"
>
#{updateUserCode},
</if>
<!-- 新增 is_import 字段值 -->
<if
test=
"isImport != null"
>
#{isImport},
</if>
</trim>
</insert>
...
...
@@ -264,6 +271,8 @@
<if
test=
"createUserCode != null"
>
create_user_code = #{createUserCode},
</if>
<if
test=
"updateTime != null"
>
update_time = #{updateTime},
</if>
<if
test=
"updateUserCode != null"
>
update_user_code = #{updateUserCode},
</if>
<!-- 新增 is_import 字段更新 -->
<if
test=
"isImport != null"
>
is_import = #{isImport},
</if>
</trim>
where id = #{id}
</update>
...
...
@@ -316,6 +325,7 @@
left join outbound_order_items ooi
on ooi.material_id = m.id
and ooi.is_used = 1
and ooi.item_status=3
and ooi.shipped_at >= DATE_FORMAT(CURDATE(), '%Y-%m-01 00:00:00')
and ooi.shipped_at
<
DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01 00:00:00')
where
...
...
@@ -334,6 +344,7 @@
left join outbound_order_items ooi
on ooi.material_id = m.id
and ooi.is_used = 1
and ooi.item_status=3
and ooi.shipped_at >= DATE_FORMAT(CURDATE(), '%Y-%m-01 00:00:00')
and ooi.shipped_at
<
DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01 00:00:00')
where
...
...
@@ -346,8 +357,8 @@
<select
id=
"outboundOrdersCount"
resultType=
"String"
>
select count(*) from outbound_orders where is_used = 1 and order_status=2
and inbound_date
>
= DATE_FORMAT(CURDATE(), '%Y-%m-01')
and inbound_date
<
DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')
and inbound_date
>
= DATE_FORMAT(CURDATE(), '%Y-%m-01')
and inbound_date
<
DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')
</select>
<update
id=
"batchUpdateInventory"
>
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论