@@ -2,40 +2,35 @@ package top.wms.admin.material.service.impl;
import cn.hutool.core.bean.BeanUtil ;
import cn.hutool.core.collection.CollUtil ;
import cn.hutool.core.date.DateUnit ;
import cn.hutool.core.date.DateUtil ;
import cn.hutool.core.io.resource.ResourceUtil ;
import cn.hutool.core.lang.UUID ;
import cn.hutool.core.math.MathUtil ;
import cn.hutool.core.util.* ;
import cn.hutool.extra.validation.ValidationUtil ;
import cn.hutool.http.ContentType ;
import cn.hutool.json.JSONUtil ;
import com.alibaba.excel.EasyExcel ;
import com.baomidou.mybatisplus.core.conditions.Wrapper ;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper ;
import com.baomidou.mybatisplus.core.toolkit.Wrappers ;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction ;
import jakarta.servlet.http.HttpServletResponse ;
import cn.hutool.core.util.StrUtil ;
import lombok.RequiredArgsConstructor ;
import me.ahoo.cosid.IdGenerator ;
import me.ahoo.cosid.provider.DefaultIdGeneratorProvider ;
import lombok.extern.slf4j.Slf4j ;
import net.dreamlu.mica.core.result.R ;
import org.dromara.x.file.storage.core.FileInfo ;
import org.springframework.mock.web.MockMultipartFile ;
import org.springframework.stereotype.Service ;
import org.springframework.transaction.annotation.Transactional ;
import org.springframework.web.multipart.MultipartFile ;
import top.continew.starter.cache.redisson.util.RedisUtils ;
import top.continew.starter.core.exception.BusinessException ;
import top.continew.starter.core.util.SpringUtils ;
import top.continew.starter.core.validation.CheckUtils ;
import top.continew.starter.extension.crud.service.BaseServiceImpl ;
import top.continew.starter.web.util.FileUploadUtils ;
import top.wms.admin.common.constant.CacheConstants ;
import top.wms.admin.common.context.UserContextHolder ;
import top.wms.admin.common.enums.GenderEnum ;
import top.wms.admin.common.util.SecureUtils ;
import top.wms.admin.material.mapper.MaterialInfoMapper ;
import top.wms.admin.material.model.entity.MaterialInfoDO ;
@@ -47,23 +42,19 @@ import top.wms.admin.material.model.resp.MaterialImportParseResp;
import top.wms.admin.material.model.resp.MaterialInfoImportResp ;
import top.wms.admin.material.model.resp.MaterialInfoResp ;
import top.wms.admin.material.service.MaterialInfoService ;
import top.wms.admin.system.model.entity.DeptDO ;
import top.wms.admin.system.model.entity.RoleDO ;
import top.wms.admin.system.model.entity.UserDO ;
import top.wms.admin.system.model.entity.UserRoleDO ;
import top.wms.admin.system.model.req.user.UserImportReq ;
import top.wms.admin.system.model.req.user.UserImportRowReq ;
import top.wms.admin.system.model.resp.user.UserImportParseResp ;
import top.wms.admin.system.model.resp.user.UserImportResp ;
import top.wms.admin.system.service.FileService ;
import javax.imageio.ImageIO ;
import java.awt.image.BufferedImage ;
import java.io.ByteArrayOutputStream ;
import java.io.IOException ;
import java.math.BigDecimal ;
import java.sql.Time ;
import java.time.Duration ;
import java.time.LocalDateTime ;
import java.util.* ;
import java.util.function.Function ;
import java.util.stream.Collectors ;
import java.util.zip.ZipEntry ;
import java.util.zip.ZipInputStream ;
import static top.wms.admin.system.enums.ImportPolicyEnum.* ;
import static top.wms.admin.system.enums.ImportPolicyEnum.SKIP ;
@@ -76,13 +67,18 @@ import static top.wms.admin.system.enums.ImportPolicyEnum.SKIP;
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class MaterialInfoServiceImpl extends BaseServiceImpl < MaterialInfoMapper , MaterialInfoDO , MaterialInfoResp , MaterialInfoResp , MaterialInfoQuery , MaterialInfoReq > implements MaterialInfoService {
private static final Set < String > IMAGE_EXTENSIONS = Set . of ( " jpg " , " jpeg " , " png " , " gif " , " bmp " ) ;
private final FileService fileService ;
@Override
public MaterialInfoDO getMaterialInfoByCode ( String code ) {
if ( StrUtil . isNotBlank ( code ) ) {
if ( StrUtil . isNotBlank ( code ) ) {
return baseMapper . lambdaQuery ( ) . eq ( MaterialInfoDO : : getEncoding , code ) . one ( ) ;
} else {
} else {
return null ;
}
}
@@ -90,7 +86,8 @@ public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper,
@Override
public void downloadImportTemplate ( HttpServletResponse response ) throws IOException {
try {
FileUploadUtils . download ( response , ResourceUtil . getStream ( " templates/import/materialInfo.xlsx " ) , " 物料信息导入模板.xlsx " ) ;
FileUploadUtils . download ( response , ResourceUtil
. getStream ( " templates/import/materialInfo.xlsx " ) , " 物料信息导入模板.xlsx " ) ;
} catch ( Exception e ) {
log . error ( " 下载用户导入模板失败:{} " , e ) ;
response . setCharacterEncoding ( CharsetUtil . UTF_8 ) ;
@@ -106,10 +103,10 @@ public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper,
// 读取表格数据
try {
importRowList = EasyExcel . read ( file . getInputStream ( ) )
. head ( MaterialImportRowReq . class )
. sheet ( )
. headRowNumber ( 1 )
. doReadSync ( ) ;
. head ( MaterialImportRowReq . class )
. sheet ( )
. headRowNumber ( 1 )
. doReadSync ( ) ;
} catch ( Exception e ) {
log . error ( " 物料信息导入数据文件解析异常:{} " , e ) ;
throw new BusinessException ( " 数据文件解析异常 " ) ;
@@ -125,25 +122,24 @@ public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper,
// 检测表格内数据是否合法
Set < String > seenCode = new HashSet < > ( ) ;
boolean hasDuplicateEncoding = validRowList . stream ( )
. map ( MaterialImportRowReq : : getEncoding )
. anyMatch ( encoding - > encoding ! = null & & ! seenCode . add ( encoding ) ) ;
. map ( MaterialImportRowReq : : getEncoding )
. anyMatch ( encoding - > encoding ! = null & & ! seenCode . add ( encoding ) ) ;
CheckUtils . throwIf ( hasDuplicateEncoding , " 存在重复物料编码,请检测数据 " ) ;
// 查询重复用户
materialImportResp
. setDuplicateNameRows ( countExistByField ( validRowList , MaterialImportRowReq : : getMaterialName , MaterialInfoDO : : getMaterialName , false ) ) ;
. setDuplicateNameRows ( countExistByField ( validRowList , MaterialImportRowReq : : getMaterialName , MaterialInfoDO : : getMaterialName , false ) ) ;
// 查询重复邮箱
materialImportResp
. setDuplicateCodeRows ( countExistByField ( validRowList , MaterialImportRowReq : : getEncoding , MaterialInfoDO : : getEncoding , false ) ) ;
. setDuplicateCodeRows ( countExistByField ( validRowList , MaterialImportRowReq : : getEncoding , MaterialInfoDO : : getEncoding , false ) ) ;
// 设置导入会话并缓存数据, 有效期10分钟
String importKey = UUID . fastUUID ( ) . toString ( true ) ;
RedisUtils . set ( CacheConstants . DATA_IMPORT_KEY + importKey , JSONUtil . toJsonStr ( validRowList ) , Duration
. ofMinutes ( 10 ) ) ;
. ofMinutes ( 10 ) ) ;
materialImportResp . setImportKey ( importKey ) ;
return materialImportResp ;
}
@Override
@Transactional ( rollbackFor = Exception . class )
public MaterialInfoImportResp importMaterial ( MaterialInfoImportReq req ) {
@@ -160,22 +156,21 @@ public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper,
// 已存在数据查询
List < String > existName = listExistByField ( importMaterialList , MaterialImportRowReq : : getMaterialName , MaterialInfoDO : : getMaterialName ) ;
List < String > existCode = listExistByField ( importMaterialList , MaterialImportRowReq : : getEncoding , MaterialInfoDO : : getEncoding ) ;
CheckUtils
. throwIf ( isExitImportMaterial ( req , importMaterialList , existName , existCode ) , " 数据不符合导入策略,已退出导入 " ) ;
CheckUtils . throwIf ( isExitImportMaterial ( req , importMaterialList , existName , existCode ) , " 数据不符合导入策略,已退出导入 " ) ;
// 批量操作数据库集合
List < MaterialInfoDO > insertList = new ArrayList < > ( ) ;
List < MaterialInfoDO > updateByNameList = new ArrayList < > ( ) ;
List < MaterialInfoDO > updateByCodeList = new ArrayList < > ( ) ;
// ID生成器
// IdGenerator idGenerator = DefaultIdGeneratorProvider.INSTANCE.getShare();
// IdGenerator idGenerator = DefaultIdGeneratorProvider.INSTANCE.getShare();
for ( MaterialImportRowReq row : importMaterialList ) {
if ( isSkipMaterialImport ( req , row , existName , existCode ) ) {
// 按规则跳过该行
continue ;
}
MaterialInfoDO materialDO = BeanUtil . toBeanIgnoreError ( row , MaterialInfoDO . class ) ;
materialDO . setUnitWeight ( NumberUtil . isValidNumber ( row . getUnitWeight ( ) ) ? row . getUnitWeight ( ) : null ) ;
materialDO . setMaterialSpec ( StrUtil . isNotBlank ( row . getMaterialSpec ( ) ) ? row . getMaterialSpec ( ) : null ) ;
materialDO . setUnitWeight ( NumberUtil . isValidNumber ( row . getUnitWeight ( ) ) ? row . getUnitWeight ( ) : null ) ;
materialDO . setMaterialSpec ( StrUtil . isNotBlank ( row . getMaterialSpec ( ) ) ? row . getMaterialSpec ( ) : null ) ;
// 修改 or 新增
if ( UPDATE . validate ( req . getDuplicateName ( ) , row . getMaterialName ( ) , existName ) ) {
materialDO . setMaterialName ( row . getMaterialName ( ) ) ;
@@ -188,7 +183,7 @@ public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper,
materialDO . setUpdateUser ( UserContextHolder . getUserId ( ) ) ;
updateByCodeList . add ( materialDO ) ;
} else {
// materialDO.setId(idGenerator.generate());
// materialDO.setId(idGenerator.generate());
insertList . add ( materialDO ) ;
}
}
@@ -199,23 +194,18 @@ public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper,
int updateByCodeCount = updateByCodeList . size ( ) ;
int totalUpdateCount = updateByNameCount + updateByCodeCount ;
int totalHandleCount = insertCount + totalUpdateCount ;
return new MaterialInfoImportResp (
totalHandle Count , // 总处理 数
insert Count , // 新增 数
totalUpdateCount // 更新总数
return new MaterialInfoImportResp ( totalHandleCount , // 总处理数
insert Count , // 新增 数
totalUpdate Count // 更新总 数
) ;
}
/**
* 按指定数据集获取数据库已存在的数量
*
* @param materialRowList 导入的数据源
* @param rowField 导入数据的字段
* @param dbField 对比数据库的字段
* @param rowField 导入数据的字段
* @param dbField 对比数据库的字段
* @return 存在的数量
*/
private int countExistByField ( List < MaterialImportRowReq > materialRowList ,
@@ -227,10 +217,9 @@ public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper,
return 0 ;
}
return ( int ) this . count ( Wrappers . < MaterialInfoDO > lambdaQuery ( )
. in ( dbField , fieldEncrypt ? SecureUtils . encryptFieldByAes ( fieldValues ) : fieldValues ) ) ;
. in ( dbField , fieldEncrypt ? SecureUtils . encryptFieldByAes ( fieldValues ) : fieldValues ) ) ;
}
/**
* 过滤无效的导入用户数据(批量导入不严格校验数据)
*
@@ -239,22 +228,23 @@ public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper,
private List < MaterialImportRowReq > filterImportData ( List < MaterialImportRowReq > importRowList ) {
// 校验过滤
List < MaterialImportRowReq > list = importRowList . stream ( )
. filter ( row - > ValidationUtil . validate ( row ) . isEmpty ( ) )
. toList ( ) ;
. filter ( row - > ValidationUtil . validate ( row ) . isEmpty ( ) )
. toList ( ) ;
// 物料名去重
return list . stream ( )
. collect ( Collectors . toMap ( MaterialImportRowReq : : getMaterialName , row - > row , ( existing , replacement ) - > existing ) )
. values ( )
. stream ( )
. toList ( ) ;
. collect ( Collectors . toMap ( MaterialImportRowReq : : getMaterialName , row - > row , ( existing ,
replacement ) - > existing ) )
. values ( )
. stream ( )
. toList ( ) ;
}
/**
* 按指定数据集获取数据库已存在内容
*
* @param materialRowList 导入的数据源
* @param rowField 导入数据的字段
* @param dbField 对比数据库的字段
* @param rowField 导入数据的字段
* @param dbField 对比数据库的字段
* @return 存在的内容
*/
private List < String > listExistByField ( List < MaterialImportRowReq > materialRowList ,
@@ -265,54 +255,56 @@ public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper,
return Collections . emptyList ( ) ;
}
List < MaterialInfoDO > materialDOList = baseMapper . selectList ( Wrappers . < MaterialInfoDO > lambdaQuery ( )
. in ( dbField , fieldValues )
. select ( dbField ) ) ;
. in ( dbField , fieldValues )
. select ( dbField ) ) ;
return materialDOList . stream ( ) . map ( dbField ) . filter ( Objects : : nonNull ) . toList ( ) ;
}
/**
* 判断是否退出导入
*
* @param req 导入参数
* @param list 导入数据
* @param existName 导入数据中已存在的物料名
* @param existCode 导入数据中已存在的物料编号
* @param req 导入参数
* @param list 导入数据
* @param existName 导入数据中已存在的物料名
* @param existCode 导入数据中已存在的物料编号
* @return 是否退出
*/
private boolean isExitImportMaterial ( MaterialInfoImportReq req ,
List < MaterialImportRowReq > list ,
List < String > existName ,
List < String > existCode ) {
List < MaterialImportRowReq > list ,
List < String > existName ,
List < String > existCode ) {
return list . stream ( )
. anyMatch ( row - > EXIT . validate ( req . getDuplicateName ( ) , row . getMaterialName ( ) , existName ) | | EXIT . validate ( req
. getDuplicateCode ( ) , row . getEncoding ( ) , existCode ) ) ;
. anyMatch ( row - > EXIT . validate ( req . getDuplicateName ( ) , row . getMaterialName ( ) , existName ) | | EXIT
. validate ( req . getDuplicateCode ( ) , row . getEncoding ( ) , existCode ) ) ;
}
/**
* 判断是否跳过导入
*
* @param req 导入参数
* @param row 导入数据
* @param existName 导入数据中已存在的物料名称
* @param existCode 导入数据中已存在的物料编号
* @param req 导入参数
* @param row 导入数据
* @param existName 导入数据中已存在的物料名称
* @param existCode 导入数据中已存在的物料编号
* @return 是否跳过
*/
private boolean isSkipMaterialImport ( MaterialInfoImportReq req ,
MaterialImportRowReq row ,
List < String > existName ,
List < String > existCode ) {
MaterialImportRowReq row ,
List < String > existName ,
List < String > existCode ) {
return SKIP . validate ( req . getDuplicateName ( ) , row . getMaterialName ( ) , existName ) | | SKIP . validate ( req
. getDuplicateCode ( ) , row . getEncoding ( ) , existCode ) ;
. getDuplicateCode ( ) , row . getEncoding ( ) , existCode ) ;
}
/**
* 导入用户
*
* @param insertList 新增用户
* @param updateByNameList 修改用户
* @param updateByCodeList 用户角色关联
* @param insertList 新增用户
* @param updateByNameList 修改用户
* @param updateByCodeList 用户角色关联
*/
private void doImportMaterial ( List < MaterialInfoDO > insertList , List < MaterialInfoDO > updateByNameList , List < MaterialInfoDO > updateByCodeList ) {
private void doImportMaterial ( List < MaterialInfoDO > insertList ,
List < MaterialInfoDO > updateByNameList ,
List < MaterialInfoDO > updateByCodeList ) {
if ( CollUtil . isNotEmpty ( insertList ) ) {
baseMapper . insertBatch ( insertList ) ;
}
@@ -323,4 +315,102 @@ public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper,
baseMapper . updateByCode ( updateByCodeList ) ;
}
}
@Override
public void uploadMaterialPhotos ( MultipartFile zipFile ) {
// 1. 初始化物料编码-照片地址Map
Map < String , String > codeUrlMap = new HashMap < > ( ) ;
// 物料照片存储路径(自定义,比如按日期分目录)
String photoStoragePath = " / " + DateUtil . today ( ) + " / " ;
try ( ZipInputStream zipInputStream = new ZipInputStream ( zipFile . getInputStream ( ) ) ) {
ZipEntry zipEntry ;
while ( ( zipEntry = zipInputStream . getNextEntry ( ) ) ! = null ) {
// 跳过目录、非图片文件
if ( zipEntry . isDirectory ( ) | | ! isImageFile ( zipEntry . getName ( ) ) ) {
zipInputStream . closeEntry ( ) ;
continue ;
}
// 2. 提取物料编码(照片名 = 物料编码,去掉后缀)
String fileName = zipEntry . getName ( ) ;
log . info ( " 正在处理的照片: " + fileName ) ;
//去除windows或linux环境下 可能存在的多层级目录
if ( fileName . contains ( " / " ) ) {
fileName = fileName . substring ( fileName . lastIndexOf ( " / " ) + 1 ) ;
}
if ( fileName . contains ( " \\ " ) ) {
fileName = fileName . substring ( fileName . lastIndexOf ( " \\ " ) + 1 ) ;
}
String materialCode = fileName . substring ( 0 , fileName . lastIndexOf ( " . " ) ) ;
// 3. 读取ZIP中的图片为BufferedImage
BufferedImage image = ImageIO . read ( zipInputStream ) ;
if ( ObjectUtil . isEmpty ( image ) ) {
log . warn ( " 无法读取图片: {} " , fileName ) ;
zipInputStream . closeEntry ( ) ;
continue ;
}
// 4. 将BufferedImage转为字节流
ByteArrayOutputStream baos = new ByteArrayOutputStream ( ) ;
String imageExt = getImageExtension ( fileName ) ;
ImageIO . write ( image , imageExt , baos ) ;
byte [ ] imageBytes = baos . toByteArray ( ) ;
// 5. 转换为MultipartFile( 使用MockMultipartFile)
MultipartFile singleImageFile = new MockMultipartFile ( " file " , fileName , " image/ " + ( imageExt
. equals ( " jpg " ) ? " jpeg " : imageExt ) , imageBytes ) ;
// 6. 调用upload方法上传图片
FileInfo fileInfo = fileService . upload ( singleImageFile , photoStoragePath , null , true , true ) ;
// 7. 将物料编码和图片URL存入Map
codeUrlMap . put ( materialCode , fileInfo . getUrl ( ) ) ;
zipInputStream . closeEntry ( ) ;
}
setPhotosByCode ( codeUrlMap ) ;
} catch ( Exception e ) {
throw new BusinessException ( " 照片批量导入失败: " + e . getMessage ( ) ) ;
}
}
private boolean isImageFile ( String fileName ) {
if ( fileName = = null | | ! fileName . contains ( " . " ) )
return false ;
String ext = fileName . substring ( fileName . lastIndexOf ( " . " ) + 1 ) . toLowerCase ( ) ;
return IMAGE_EXTENSIONS . contains ( ext ) ;
}
private String getImageExtension ( String fileName ) {
String ext = fileName . substring ( fileName . lastIndexOf ( " . " ) + 1 ) . toLowerCase ( ) ;
return " jpeg " . equals ( ext ) ? " jpg " : ext ;
}
private void setPhotosByCode ( Map < String , String > codeUrlMap ) {
CheckUtils . throwIfEmpty ( codeUrlMap , " 照片为空,请重新上传 " ) ;
List < MaterialInfoDO > existList = baseMapper . selectList ( Wrappers . < MaterialInfoDO > lambdaQuery ( )
. in ( MaterialInfoDO : : getEncoding , codeUrlMap . keySet ( ) ) ) ;
if ( existList . isEmpty ( ) ) {
log . warn ( " 未找到任何匹配的物料编码 " ) ;
return ;
}
List < MaterialInfoDO > updateList = existList . stream ( ) . map ( exist - > {
MaterialInfoDO updateDO = new MaterialInfoDO ( ) ;
updateDO . setId ( exist . getId ( ) ) ;
updateDO . setPhotoUrl ( codeUrlMap . get ( exist . getEncoding ( ) ) ) ;
return updateDO ;
} ) . collect ( Collectors . toList ( ) ) ;
if ( ! updateList . isEmpty ( ) ) {
baseMapper . updateBatchById ( updateList ) ;
log . info ( " 成功更新 {} 个物料的照片 " , updateList . size ( ) ) ;
}
//记录未找到的物料编码(可选,方便排查问题)
Set < String > existCodes = existList . stream ( ) . map ( MaterialInfoDO : : getEncoding ) . collect ( Collectors . toSet ( ) ) ;
codeUrlMap . keySet ( )
. stream ( )
. filter ( code - > ! existCodes . contains ( code ) )
. forEach ( code - > log . warn ( " 物料编码 [{}] 不存在,照片更新失败 " , code ) ) ;
}
}