| | |
| | | package com.ruoyi.sales.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.basic.mapper.CustomerMapper; |
| | | import com.ruoyi.basic.pojo.Customer; |
| | |
| | | import com.ruoyi.sales.pojo.SalesLedgerProduct; |
| | | import com.ruoyi.sales.service.ISalesLedgerService; |
| | | import lombok.AllArgsConstructor; |
| | | import org.springframework.beans.BeanUtils; |
| | | 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 java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.time.LocalDate; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.*; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | |
| | | private CustomerMapper customerMapper; |
| | | |
| | | private SalesLedgerProductMapper salesLedgerProductMapper; |
| | | |
| | | private static final String LOCK_PREFIX = "contract_no_lock:"; |
| | | private static final long LOCK_WAIT_TIMEOUT = 10; // 锁等待超时时间(秒) |
| | | private static final long LOCK_EXPIRE_TIME = 30; // 锁自动过期时间(秒) |
| | | |
| | | private final RedisTemplate<String, String> redisTemplate; |
| | | |
| | | @Override |
| | | public List<SalesLedger> selectSalesLedgerList(SalesLedger salesLedger) { |
| | |
| | | List<SalesLedgerProduct> ledgerProducts = productMap.getOrDefault(ledger.getId(), Collections.emptyList()); |
| | | if (!ledgerProducts.isEmpty()) { |
| | | dto.setHasChildren(true); |
| | | dto.setChildren(ledgerProducts); |
| | | dto.setProductData(ledgerProducts); |
| | | } |
| | | return dto; |
| | | }).collect(Collectors.toList()); |
| | |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int deleteSalesLedgerByIds(Long[] ids) { |
| | | return salesLedgerMapper.deleteBatchIds(Arrays.asList(ids)); |
| | | List<Long> idList = Arrays.stream(ids) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList()); |
| | | |
| | | if (CollectionUtils.isEmpty(idList)) { |
| | | return 0; |
| | | } |
| | | // 1. 先删除子表数据 |
| | | LambdaQueryWrapper<SalesLedgerProduct> productWrapper = new LambdaQueryWrapper<>(); |
| | | productWrapper.in(SalesLedgerProduct::getSalesLedgerId, idList); |
| | | salesLedgerProductMapper.delete(productWrapper); |
| | | |
| | | // 2. 再删除主表数据 |
| | | return salesLedgerMapper.deleteBatchIds(idList); |
| | | } |
| | | |
| | | public int addOrUpdateSalesLedger(SalesLedger salesLedger) { |
| | | LambdaQueryWrapper<Customer> queryWrapper = new LambdaQueryWrapper<>(); |
| | | queryWrapper.eq(Customer::getId, salesLedger.getCustomerId()); |
| | | Customer customer = customerMapper.selectOne(queryWrapper); |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int addOrUpdateSalesLedger(SalesLedgerDto salesLedgerDto) { |
| | | // 1. 校验客户信息 |
| | | Customer customer = customerMapper.selectById(salesLedgerDto.getCustomerId()); |
| | | if (customer == null) { |
| | | throw new BaseException("未查询到对应的 Customer 信息"); |
| | | throw new BaseException("客户不存在"); |
| | | } |
| | | |
| | | // 2. DTO转Entity |
| | | SalesLedger salesLedger = convertToEntity(salesLedgerDto); |
| | | salesLedger.setCustomerName(customer.getCustomerName()); |
| | | salesLedger.setTenantId(customer.getTenantId()); |
| | | return saveOrUpdates(salesLedger); |
| | | |
| | | // 3. 新增或更新主表 |
| | | if (salesLedger.getId() == null) { |
| | | // 生成合同编号 |
| | | String contractNo = generateSalesContractNo(); |
| | | salesLedger.setSalesContractNo(contractNo); |
| | | salesLedgerMapper.insert(salesLedger); |
| | | } else { |
| | | salesLedgerMapper.updateById(salesLedger); |
| | | } |
| | | |
| | | // 4. 处理子表数据 |
| | | if (salesLedgerDto.getProductData() != null && !salesLedgerDto.getProductData().isEmpty()) { |
| | | handleSalesLedgerProducts(salesLedger.getId(), salesLedgerDto.getProductData()); |
| | | } |
| | | |
| | | return 1; // 操作成功返回1 |
| | | } |
| | | |
| | | private int saveOrUpdates(SalesLedger salesLedger) { |
| | | if (salesLedger.getId() == null) { |
| | | return salesLedgerMapper.insert(salesLedger); |
| | | } else { |
| | | return salesLedgerMapper.updateById(salesLedger); |
| | | private void handleSalesLedgerProducts(Long salesLedgerId, List<SalesLedgerProduct> products) { |
| | | // 按ID分组,区分新增和更新的记录 |
| | | Map<Boolean, List<SalesLedgerProduct>> partitionedProducts = products.stream() |
| | | .peek(p -> p.setSalesLedgerId(salesLedgerId)) |
| | | .collect(Collectors.partitioningBy(p -> p.getId() != null)); |
| | | |
| | | List<SalesLedgerProduct> updateList = partitionedProducts.get(true); |
| | | List<SalesLedgerProduct> insertList = partitionedProducts.get(false); |
| | | |
| | | // 执行更新操作 |
| | | if (!updateList.isEmpty()) { |
| | | salesLedgerProductMapper.updateBatchSomeColumn(updateList); |
| | | } |
| | | // 执行插入操作 |
| | | if (!insertList.isEmpty()) { |
| | | salesLedgerProductMapper.insertBatchSomeColumn(insertList); |
| | | } |
| | | } |
| | | |
| | | private SalesLedger convertToEntity(SalesLedgerDto dto) { |
| | | SalesLedger entity = new SalesLedger(); |
| | | BeanUtils.copyProperties(dto, entity); |
| | | return entity; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public String generateSalesContractNo() { |
| | | LocalDate currentDate = LocalDate.now(); |
| | | String datePart = currentDate.format(DateTimeFormatter.BASIC_ISO_DATE); |
| | | String lockKey = LOCK_PREFIX + datePart; |
| | | String lockValue = Thread.currentThread().getId() + "-" + System.nanoTime(); // 唯一标识锁持有者 |
| | | |
| | | try { |
| | | // 1. 尝试获取分布式锁(循环直到超时) |
| | | long startWaitTime = System.currentTimeMillis(); |
| | | while (System.currentTimeMillis() - startWaitTime < LOCK_WAIT_TIMEOUT * 1000) { |
| | | // SET key value NX PX 30000:仅当锁不存在时获取,设置30秒过期 |
| | | Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, LOCK_EXPIRE_TIME, TimeUnit.SECONDS); |
| | | if (Boolean.TRUE.equals(locked)) { |
| | | break; // 成功获取锁 |
| | | } |
| | | // 短暂休眠避免忙等待 |
| | | try { |
| | | Thread.sleep(100); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | throw new RuntimeException("获取锁时被中断", e); |
| | | } |
| | | } |
| | | |
| | | if (!redisTemplate.hasKey(lockKey)) { |
| | | throw new RuntimeException("获取合同编号生成锁失败:超时"); |
| | | } |
| | | |
| | | // 2. 查询当天已存在的序列号(与原逻辑一致) |
| | | List<Integer> existingSequences = salesLedgerMapper.selectSequencesByDate(datePart); |
| | | int nextSequence = findFirstMissingSequence(existingSequences); |
| | | |
| | | return datePart + String.format("%02d", nextSequence); |
| | | } finally { |
| | | // 3. 释放锁(使用Lua脚本保证原子性,避免误删其他线程的锁) |
| | | 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 // 只有持有相同值的线程才能删除锁 |
| | | ); |
| | | } |
| | | } |
| | | |
| | | private int findFirstMissingSequence(List<Integer> sequences) { |
| | | if (sequences.isEmpty()) { |
| | | return 1; |
| | | } |
| | | // 排序后查找第一个缺失的正整数(与原逻辑一致) |
| | | sequences.sort(Integer::compareTo); |
| | | int next = 1; |
| | | for (int seq : sequences) { |
| | | if (seq == next) { |
| | | next++; |
| | | } else if (seq > next) { |
| | | break; |
| | | } |
| | | } |
| | | return next; |
| | | } |
| | | } |