| | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; |
| | | import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.basic.dto.ProductDto; |
| | | import com.ruoyi.basic.dto.ProductModelAnticlockwiseDto; |
| | | import com.ruoyi.basic.dto.ProductModelDto; |
| | | import com.ruoyi.basic.mapper.ProductMapper; |
| | | import com.ruoyi.basic.mapper.ProductModelMapper; |
| | | import com.ruoyi.basic.pojo.Product; |
| | | import com.ruoyi.basic.pojo.ProductModel; |
| | | import com.ruoyi.basic.service.IProductModelService; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import com.ruoyi.common.utils.SecurityUtils; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.common.utils.bean.BeanUtils; |
| | | import com.ruoyi.common.utils.poi.ExcelUtil; |
| | | import com.ruoyi.common.utils.uuid.IdUtils; |
| | | import com.ruoyi.framework.web.domain.AjaxResult; |
| | | import com.ruoyi.sales.mapper.SalesLedgerProductMapper; |
| | | import com.ruoyi.sales.pojo.SalesLedgerProduct; |
| | | import lombok.AllArgsConstructor; |
| | | import org.springframework.data.redis.core.RedisTemplate; |
| | | import org.springframework.data.redis.core.script.DefaultRedisScript; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.Arrays; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import javax.validation.constraints.NotNull; |
| | | import java.util.*; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | |
| | | @Service |
| | | @AllArgsConstructor |
| | | public class ProductModelServiceImpl extends ServiceImpl<ProductModelMapper, ProductModel> implements IProductModelService { |
| | | private static final String PRODUCT_MODEL_LOCK_PREFIX = "product_model_anticlockwise:"; |
| | | private static final long LOCK_WAIT_TIMEOUT_SECONDS = 10L; |
| | | private static final long LOCK_EXPIRE_TIME_SECONDS = 30L; |
| | | private static final String FINISHED_PRODUCT_NAME = "\u6210\u54c1"; |
| | | private final RedisTemplate redisTemplate; |
| | | |
| | | private final ProductMapper productMapper; |
| | | private final SalesLedgerProductMapper salesLedgerProductMapper; |
| | |
| | | |
| | | if (productModelDto.getId() == null) { |
| | | ProductModel productModel = new ProductModel(); |
| | | BeanUtils.copyProperties(productModelDto,productModel); |
| | | productModelMapper.insert(productModel); |
| | | |
| | | String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); |
| | | String idStr = String.format("%06d", productModel.getId()); // 六位补0 |
| | | String newProductCode = "BM" + dateStr + idStr; |
| | | // 更新数据库中的productCode |
| | | productModel.setProductCode(newProductCode); |
| | | return productModelMapper.updateById(productModel); |
| | | BeanUtils.copyProperties(productModelDto, productModel); |
| | | return productModelMapper.insert(productModel); |
| | | } else { |
| | | return productModelMapper.updateById(productModelDto); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | @Override |
| | |
| | | public List<ProductModel> selectModelList(ProductDto productDto) { |
| | | LambdaQueryWrapper<ProductModel> queryWrapper = new LambdaQueryWrapper<>(); |
| | | queryWrapper.eq(ProductModel::getProductId, productDto.getId()); |
| | | queryWrapper.eq(productDto.getCreateUser() != null, ProductModel::getCreateUser, productDto.getCreateUser()); |
| | | queryWrapper.eq(productDto.getDeptId() != null, ProductModel::getDeptId, productDto.getDeptId()); |
| | | if (ObjectUtils.isNotEmpty(productDto.getDeptIds())) { |
| | | queryWrapper.in( ProductModel::getDeptId, Arrays.asList(productDto.getDeptIds())); |
| | | } |
| | | return productModelMapper.selectList(queryWrapper); |
| | | } |
| | | |
| | | @Override |
| | | public List<ProductModel> selectModelListByProductIds(@NotNull List<Long> ids) { |
| | | return productModelMapper.selectModelListByProductIds(ids); |
| | | } |
| | | |
| | | /** |
| | | * 根据id查询产品规格分页查询 |
| | | * |
| | | * @param page |
| | | * @param productDto |
| | | * @return |
| | |
| | | } |
| | | |
| | | @Override |
| | | public Boolean importProduct(MultipartFile file) { |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public AjaxResult importProductModel(MultipartFile file, Integer productId) { |
| | | if (productId == null) { |
| | | return AjaxResult.error("请先选择产品再导入规格型号"); |
| | | } |
| | | |
| | | Product product = productMapper.selectById(productId); |
| | | if (product == null) { |
| | | return AjaxResult.error("选择的产品不存在"); |
| | | } |
| | | |
| | | try { |
| | | ExcelUtil<ProductModel> productModelExcelUtil = new ExcelUtil<>(ProductModel.class); |
| | | List<ProductModel> productModelList = productModelExcelUtil.importExcel(file.getInputStream()); |
| | | Map<String, List<ProductModel>> collect = productModelList.stream().collect(Collectors.groupingBy(ProductModel::getProductName)); |
| | | collect.forEach((k,v)->{ |
| | | Product product = productMapper.selectOne(new LambdaQueryWrapper<Product>().eq(Product::getProductName, k).last("LIMIT 1")); |
| | | if (product != null) { |
| | | v.forEach(productModel -> { |
| | | productModel.setProductId(product.getId()); |
| | | }); |
| | | this.saveOrUpdateBatch(v); |
| | | |
| | | if (CollectionUtils.isEmpty(productModelList)) { |
| | | return AjaxResult.error("导入数据不能为空"); |
| | | } |
| | | |
| | | // 获取当前产品下所有的规格型号名 |
| | | List<ProductModel> existingModels = list(new LambdaQueryWrapper<ProductModel>().eq(ProductModel::getProductId, productId)); |
| | | Set<String> existingModelNames = existingModels.stream().map(ProductModel::getModel).collect(Collectors.toSet()); |
| | | |
| | | List<ProductModel> waitToSaveList = new ArrayList<>(); |
| | | int skipCount = 0; |
| | | |
| | | for (int i = 0; i < productModelList.size(); i++) { |
| | | ProductModel item = productModelList.get(i); |
| | | int rowNum = i + 2; |
| | | |
| | | if (StringUtils.isEmpty(item.getModel())) { |
| | | return AjaxResult.error("第 " + rowNum + " 行导入失败: [规格型号] 不能为空"); |
| | | } |
| | | }); |
| | | return true; |
| | | }catch (Exception e) { |
| | | e.printStackTrace(); |
| | | if (StringUtils.isEmpty(item.getUnit())) { |
| | | return AjaxResult.error("第 " + rowNum + " 行导入失败: [单位] 不能为空"); |
| | | } |
| | | |
| | | // 去重,如果已包含该型号,则跳过 |
| | | if (existingModelNames.contains(item.getModel())) { |
| | | skipCount++; |
| | | continue; |
| | | } |
| | | |
| | | item.setProductId(product.getId()); |
| | | waitToSaveList.add(item); |
| | | |
| | | existingModelNames.add(item.getModel()); |
| | | } |
| | | |
| | | if (!waitToSaveList.isEmpty()) { |
| | | saveBatch(waitToSaveList); |
| | | } |
| | | |
| | | if (skipCount == 0) { |
| | | return AjaxResult.success(String.format("成功导入 %d 条数据", waitToSaveList.size())); |
| | | } else { |
| | | return AjaxResult.success(String.format("成功导入 %d 条,跳过已存在数据 %d 条", waitToSaveList.size(), skipCount)); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("导入产品规格异常", e); |
| | | throw new ServiceException("导入失败"); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | //反向新增成品产品,只有销售关联新增的时候调用 |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long productModelAnticlockwise(ProductModelAnticlockwiseDto productModelDto) { |
| | | Long deptId = SecurityUtils.getDeptId()[0]; |
| | | |
| | | // 如果提供了ID,直接更新 |
| | | if (ObjectUtils.isNotEmpty(productModelDto.getId())) { |
| | | return updateProductModelById(productModelDto, deptId); |
| | | } |
| | | |
| | | // 检查是否已存在相同的产品型号 |
| | | ProductModel existingModel = productModelMapper.selectOldProductModel( |
| | | productModelDto.getModel(), productModelDto.getProductName() |
| | | ); |
| | | if (existingModel != null) { |
| | | return updateExistingProductModel(existingModel, productModelDto, deptId); |
| | | } |
| | | |
| | | // 新增时需要加锁防止并发创建 |
| | | String lockKey = buildProductModelLockKey(FINISHED_PRODUCT_NAME, |
| | | productModelDto.getProductName(), productModelDto.getModel()); |
| | | String lockValue = UUID.randomUUID().toString(); |
| | | acquireLock(lockKey, lockValue); |
| | | try { |
| | | Long productId = getOrCreateProduct(productModelDto.getProductName(), deptId); |
| | | |
| | | ProductModel productModel = new ProductModel(); |
| | | productModel.setProductId(productId); |
| | | productModel.setModel(productModelDto.getModel()); |
| | | productModel.setUnit(productModelDto.getUnit()); |
| | | productModel.setSubUnit(productModelDto.getSubUnit()); |
| | | productModel.setDeptId(deptId); |
| | | productModelMapper.insert(productModel); |
| | | return productModel.getId(); |
| | | } finally { |
| | | releaseLock(lockKey, lockValue); |
| | | } |
| | | } |
| | | |
| | | private Long updateProductModelById(ProductModelAnticlockwiseDto productModelDto, Long deptId) { |
| | | ProductModel productModel = productModelMapper.selectById(productModelDto.getId()); |
| | | if (productModel == null) { |
| | | throw new ServiceException("产品型号不存在"); |
| | | } |
| | | return updateExistingProductModel(productModel, productModelDto, deptId); |
| | | } |
| | | |
| | | private Long updateExistingProductModel(ProductModel productModel, ProductModelAnticlockwiseDto dto, Long deptId) { |
| | | productModel.setModel(dto.getModel()); |
| | | productModel.setUnit(dto.getUnit()); |
| | | productModel.setSubUnit(dto.getSubUnit()); |
| | | productModel.setDeptId(deptId); |
| | | productModelMapper.updateById(productModel); |
| | | |
| | | updateProductName(productModel.getProductId(), dto.getProductName(), deptId); |
| | | return productModel.getId(); |
| | | } |
| | | |
| | | private Long getOrCreateProduct(String productName, Long deptId) { |
| | | // 获取或创建父产品"成品" |
| | | Product parentProduct = getOrCreateParentProduct(FINISHED_PRODUCT_NAME, deptId); |
| | | |
| | | // 查找或创建具体产品 |
| | | Product product = productMapper.selectOne(new QueryWrapper<Product>().lambda() |
| | | .eq(Product::getProductName, productName) |
| | | .eq(Product::getParentId, parentProduct.getId()) |
| | | .last("limit 1")); |
| | | |
| | | if (product == null) { |
| | | product = new Product(); |
| | | product.setProductName(productName); |
| | | product.setParentId(parentProduct.getId()); |
| | | product.setDeptId(deptId); |
| | | productMapper.insert(product); |
| | | } |
| | | return product.getId(); |
| | | } |
| | | |
| | | private Product getOrCreateParentProduct(String parentName, Long deptId) { |
| | | Product parentProduct = productMapper.selectOne(new QueryWrapper<Product>().lambda() |
| | | .eq(Product::getProductName, parentName) |
| | | .last("limit 1")); |
| | | |
| | | if (parentProduct == null) { |
| | | parentProduct = new Product(); |
| | | parentProduct.setProductName(parentName); |
| | | parentProduct.setDeptId(deptId); |
| | | productMapper.insert(parentProduct); |
| | | } |
| | | return parentProduct; |
| | | } |
| | | |
| | | private void updateProductName(Long productId, String productName, Long deptId) { |
| | | Product product = productMapper.selectById(productId); |
| | | if (product != null) { |
| | | product.setProductName(productName); |
| | | product.setDeptId(deptId); |
| | | productMapper.updateById(product); |
| | | } |
| | | } |
| | | |
| | | private String buildProductModelLockKey(String parentName, String productName, String model) { |
| | | return PRODUCT_MODEL_LOCK_PREFIX + parentName + ":" + productName + ":" + model; |
| | | } |
| | | |
| | | private void acquireLock(String lockKey, String lockValue) { |
| | | long startWaitTime = System.currentTimeMillis(); |
| | | while (System.currentTimeMillis() - startWaitTime < LOCK_WAIT_TIMEOUT_SECONDS * 1000) { |
| | | Boolean locked = redisTemplate.opsForValue().setIfAbsent( |
| | | lockKey, lockValue, LOCK_EXPIRE_TIME_SECONDS, TimeUnit.SECONDS |
| | | ); |
| | | if (Boolean.TRUE.equals(locked)) { |
| | | return; |
| | | } |
| | | try { |
| | | Thread.sleep(100L); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | throw new ServiceException("Get product model lock interrupted"); |
| | | } |
| | | } |
| | | throw new ServiceException("Get product model lock timeout"); |
| | | } |
| | | |
| | | private void releaseLock(String lockKey, String lockValue) { |
| | | String luaScript = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end"; |
| | | redisTemplate.execute( |
| | | new DefaultRedisScript<>(luaScript, Long.class), |
| | | Collections.singletonList(lockKey), |
| | | lockValue |
| | | ); |
| | | } |
| | | |
| | | } |