package com.ruoyi.sales.service.impl;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.ruoyi.basic.mapper.CustomerMapper;
|
import com.ruoyi.basic.pojo.Customer;
|
import com.ruoyi.common.exception.base.BaseException;
|
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.other.mapper.TempFileMapper;
|
import com.ruoyi.other.pojo.TempFile;
|
import com.ruoyi.sales.dto.MonthlyAmountDto;
|
import com.ruoyi.sales.dto.SalesLedgerDto;
|
import com.ruoyi.sales.mapper.*;
|
import com.ruoyi.sales.pojo.*;
|
import com.ruoyi.sales.service.ISalesLedgerService;
|
import lombok.RequiredArgsConstructor;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.commons.io.FilenameUtils;
|
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Value;
|
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.io.IOException;
|
import java.lang.reflect.Field;
|
import java.math.BigDecimal;
|
import java.nio.file.Files;
|
import java.nio.file.Path;
|
import java.nio.file.Paths;
|
import java.nio.file.StandardCopyOption;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.YearMonth;
|
import java.time.format.DateTimeFormatter;
|
import java.util.*;
|
import java.util.concurrent.TimeUnit;
|
import java.util.function.Function;
|
import java.util.stream.Collectors;
|
|
/**
|
* 销售台账Service业务层处理
|
*
|
* @author ruoyi
|
* @date 2025-05-08
|
*/
|
@Service
|
@RequiredArgsConstructor
|
@Slf4j
|
public class SalesLedgerServiceImpl extends ServiceImpl<SalesLedgerMapper, SalesLedger> implements ISalesLedgerService {
|
|
private final SalesLedgerMapper salesLedgerMapper;
|
|
private final CustomerMapper customerMapper;
|
|
private final SalesLedgerProductMapper salesLedgerProductMapper;
|
|
private final CommonFileMapper commonFileMapper;
|
|
private final TempFileMapper tempFileMapper;
|
|
private final ReceiptPaymentMapper receiptPaymentMapper;
|
|
private final InvoiceLedgerMapper invoiceLedgerMapper;
|
|
@Autowired
|
private InvoiceRegistrationProductMapper invoiceRegistrationProductMapper;
|
|
@Value("${file.upload-dir}")
|
private String uploadDir;
|
|
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(SalesLedgerDto salesLedgerDto) {
|
LambdaQueryWrapper<SalesLedger> queryWrapper = new LambdaQueryWrapper<>();
|
if (StringUtils.isNotBlank(salesLedgerDto.getCustomerName())) {
|
queryWrapper.eq(SalesLedger::getCustomerName, salesLedgerDto.getCustomerName());
|
}
|
return salesLedgerMapper.selectList(queryWrapper);
|
}
|
|
public SalesLedgerDto getSalesLedgerWithProducts(SalesLedgerDto salesLedgerDto) {
|
// 1. 查询主表
|
SalesLedger salesLedger = salesLedgerMapper.selectById(salesLedgerDto.getId());
|
if (salesLedger == null) {
|
throw new BaseException("台账不存在");
|
}
|
|
// 2. 查询子表
|
LambdaQueryWrapper<SalesLedgerProduct> productWrapper = new LambdaQueryWrapper<>();
|
productWrapper.eq(SalesLedgerProduct::getSalesLedgerId, salesLedger.getId());
|
List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(productWrapper);
|
|
// 3.查询上传文件
|
LambdaQueryWrapper<CommonFile> salesLedgerFileWrapper = new LambdaQueryWrapper<>();
|
salesLedgerFileWrapper.eq(CommonFile::getCommonId, salesLedger.getId());
|
List<CommonFile> salesLedgerFiles = commonFileMapper.selectList(salesLedgerFileWrapper);
|
|
// 4. 转换 DTO
|
SalesLedgerDto resultDto = new SalesLedgerDto();
|
BeanUtils.copyProperties(salesLedger, resultDto);
|
if (!products.isEmpty()) {
|
resultDto.setHasChildren(true);
|
resultDto.setProductData(products);
|
resultDto.setSalesLedgerFiles(salesLedgerFiles);
|
}
|
return resultDto;
|
}
|
|
@Override
|
public List<Map<String, Object>> getSalesNo() {
|
LambdaQueryWrapper<SalesLedger> queryWrapper = Wrappers.lambdaQuery();
|
queryWrapper.select(SalesLedger::getId, SalesLedger::getSalesContractNo);
|
|
// 获取原始查询结果
|
List<Map<String, Object>> result = salesLedgerMapper.selectMaps(queryWrapper);
|
|
// 将下划线命名转换为驼峰命名
|
return result.stream().map(map -> map.entrySet().stream()
|
.collect(Collectors.toMap(
|
entry -> underlineToCamel(entry.getKey()),
|
Map.Entry::getValue))
|
).collect(Collectors.toList());
|
}
|
|
@Override
|
public BigDecimal getContractAmount() {
|
LocalDate now = LocalDate.now();
|
YearMonth currentMonth = YearMonth.from(now);
|
|
// 创建LambdaQueryWrapper
|
LambdaQueryWrapper<SalesLedger> queryWrapper = new LambdaQueryWrapper<>();
|
queryWrapper.ge(SalesLedger::getEntryDate, currentMonth.atDay(1).atStartOfDay()) // 大于等于本月第一天
|
.lt(SalesLedger::getEntryDate, currentMonth.plusMonths(1).atDay(1).atStartOfDay()); // 小于下月第一天
|
|
// 执行查询并计算总和
|
List<SalesLedger> salesLedgers = salesLedgerMapper.selectList(queryWrapper);
|
|
BigDecimal totalContractAmount = salesLedgers.stream()
|
.map(SalesLedger::getContractAmount)
|
.filter(Objects::nonNull)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
return totalContractAmount;
|
}
|
|
@Override
|
public List getTopFiveList() {
|
// 查询原始数据
|
LambdaQueryWrapper<SalesLedger> queryWrapper = Wrappers.lambdaQuery();
|
queryWrapper.select(SalesLedger::getCustomerId,
|
SalesLedger::getCustomerName,
|
SalesLedger::getContractAmount)
|
.orderByDesc(SalesLedger::getContractAmount);
|
List<SalesLedger> records = salesLedgerMapper.selectList(queryWrapper);
|
|
// 按客户ID分组并聚合金额
|
Map<Long, GroupedCustomer> groupedMap = new LinkedHashMap<>(); // 使用LinkedHashMap保持排序
|
for (SalesLedger record : records) {
|
groupedMap.computeIfAbsent(record.getCustomerId(),
|
k -> new GroupedCustomer(record.getCustomerId(), record.getCustomerName()))
|
.addAmount(record.getContractAmount());
|
}
|
|
// 转换为结果列表并取前5
|
return groupedMap.values().stream()
|
.sorted(Comparator.comparing(GroupedCustomer::getTotalAmount).reversed())
|
.limit(5)
|
.map(customer -> {
|
Map<String, Object> result = new HashMap<>();
|
result.put("customerId", customer.getCustomerId());
|
result.put("customerName", customer.getCustomerName());
|
result.put("totalAmount", customer.getTotalAmount());
|
return result;
|
})
|
.collect(Collectors.toList());
|
}
|
|
@Override
|
public List<MonthlyAmountDto> getAmountHalfYear() {
|
LocalDate now = LocalDate.now();
|
YearMonth currentMonth = YearMonth.from(now);
|
|
List<MonthlyAmountDto> monthlyAmounts = new ArrayList<>();
|
|
for (int i = 0; i < 6; i++) {
|
YearMonth targetMonth = currentMonth.minusMonths(i);
|
LocalDate firstDayOfMonth = targetMonth.atDay(1);
|
LocalDate firstDayOfNextMonth = targetMonth.plusMonths(1).atDay(1);
|
|
LocalDateTime startOfMonth = firstDayOfMonth.atStartOfDay();
|
LocalDateTime startOfNextMonth = firstDayOfNextMonth.atStartOfDay();
|
|
LambdaQueryWrapper<ReceiptPayment> receiptPaymentLambdaQueryWrapper = new LambdaQueryWrapper<>();
|
receiptPaymentLambdaQueryWrapper.ge(ReceiptPayment::getCreateTime, startOfMonth)
|
.lt(ReceiptPayment::getCreateTime, startOfNextMonth);
|
|
LambdaQueryWrapper<InvoiceLedger> invoiceLedgerLambdaQueryWrapper = new LambdaQueryWrapper<>();
|
invoiceLedgerLambdaQueryWrapper.ge(InvoiceLedger::getCreateTime, startOfMonth)
|
.lt(InvoiceLedger::getCreateTime, startOfNextMonth);
|
|
// 获取回款金额
|
List<ReceiptPayment> receiptPaymentList = receiptPaymentMapper.selectList(receiptPaymentLambdaQueryWrapper);
|
//开票金额
|
List<InvoiceLedger> invoiceLedgerList = invoiceLedgerMapper.selectList(invoiceLedgerLambdaQueryWrapper);
|
|
// 使用 Stream 求和
|
BigDecimal invoiceAmount = invoiceLedgerList.stream()
|
.map(InvoiceLedger::getInvoiceTotal)
|
.filter(Objects::nonNull)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
BigDecimal receiptAmount = receiptPaymentList.stream()
|
.map(ReceiptPayment::getReceiptPaymentAmount)
|
.filter(Objects::nonNull)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
MonthlyAmountDto monthlyAmount = new MonthlyAmountDto();
|
monthlyAmount.setMonth(targetMonth.format(DateTimeFormatter.ofPattern("yyyy-MM")));
|
monthlyAmount.setInvoiceAmount(invoiceAmount);
|
monthlyAmount.setReceiptAmount(receiptAmount);
|
|
monthlyAmounts.add(monthlyAmount);
|
}
|
|
return monthlyAmounts;
|
}
|
|
// 内部类用于存储聚合结果
|
private static class GroupedCustomer {
|
private final Long customerId;
|
private final String customerName;
|
private BigDecimal totalAmount = BigDecimal.ZERO;
|
|
public GroupedCustomer(Long customerId, String customerName) {
|
this.customerId = customerId;
|
this.customerName = customerName;
|
}
|
|
public void addAmount(BigDecimal amount) {
|
if (amount != null) {
|
this.totalAmount = this.totalAmount.add(amount);
|
}
|
}
|
|
public Long getCustomerId() {
|
return customerId;
|
}
|
|
public String getCustomerName() {
|
return customerName;
|
}
|
|
public BigDecimal getTotalAmount() {
|
return totalAmount;
|
}
|
}
|
|
/**
|
* 下划线命名转驼峰命名
|
*/
|
private String underlineToCamel(String param) {
|
if (param == null || "".equals(param.trim())) {
|
return "";
|
}
|
int len = param.length();
|
StringBuilder sb = new StringBuilder(len);
|
for (int i = 0; i < len; i++) {
|
char c = param.charAt(i);
|
if (c == '_') {
|
if (++i < len) {
|
sb.append(Character.toUpperCase(param.charAt(i)));
|
}
|
} else {
|
sb.append(Character.toLowerCase(c));
|
}
|
}
|
return sb.toString();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int deleteSalesLedgerByIds(Long[] 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);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int addOrUpdateSalesLedger(SalesLedgerDto salesLedgerDto) {
|
try {
|
// 1. 校验客户信息
|
Customer customer = customerMapper.selectById(salesLedgerDto.getCustomerId());
|
if (customer == null) {
|
throw new BaseException("客户不存在");
|
}
|
|
// 2. DTO转Entity
|
SalesLedger salesLedger = convertToEntity(salesLedgerDto);
|
salesLedger.setCustomerName(customer.getCustomerName());
|
salesLedger.setTenantId(customer.getTenantId());
|
|
// 3. 新增或更新主表
|
if (salesLedger.getId() == null) {
|
String contractNo = generateSalesContractNo();
|
salesLedger.setSalesContractNo(contractNo);
|
salesLedgerMapper.insert(salesLedger);
|
} else {
|
salesLedgerMapper.updateById(salesLedger);
|
}
|
|
// 4. 处理子表数据
|
List<SalesLedgerProduct> productList = salesLedgerDto.getProductData();
|
if (productList != null && !productList.isEmpty()) {
|
handleSalesLedgerProducts(salesLedger.getId(), productList, salesLedgerDto.getType());
|
updateMainContractAmount(
|
salesLedger.getId(),
|
productList,
|
SalesLedgerProduct::getTaxInclusiveTotalPrice,
|
salesLedgerMapper,
|
SalesLedger.class
|
);
|
}
|
|
// 5. 迁移临时文件到正式目录
|
if (salesLedgerDto.getTempFileIds() != null && !salesLedgerDto.getTempFileIds().isEmpty()) {
|
migrateTempFilesToFormal(salesLedger.getId(), salesLedgerDto.getTempFileIds());
|
}
|
return 1;
|
} catch (IOException e) {
|
throw new BaseException("文件迁移失败: " + e.getMessage());
|
}
|
}
|
|
// 文件迁移方法
|
|
/**
|
* 将临时文件迁移到正式目录
|
*
|
* @param businessId 业务ID(销售台账ID)
|
* @param tempFileIds 临时文件ID列表
|
* @throws IOException 文件操作异常
|
*/
|
private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
|
if (CollectionUtils.isEmpty(tempFileIds)) {
|
return;
|
}
|
|
// 构建正式目录路径(按业务类型和日期分组)
|
String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
|
|
Path formalDirPath = Paths.get(formalDir);
|
|
// 确保正式目录存在(递归创建)
|
if (!Files.exists(formalDirPath)) {
|
Files.createDirectories(formalDirPath);
|
}
|
|
for (String tempFileId : tempFileIds) {
|
// 查询临时文件记录
|
TempFile tempFile = tempFileMapper.selectById(tempFileId);
|
if (tempFile == null) {
|
log.warn("临时文件不存在,跳过处理: {}", tempFileId);
|
continue;
|
}
|
|
// 构建正式文件名(包含业务ID和时间戳,避免冲突)
|
String originalFilename = tempFile.getOriginalName();
|
String fileExtension = FilenameUtils.getExtension(originalFilename);
|
String formalFilename = businessId + "_" +
|
System.currentTimeMillis() + "_" +
|
UUID.randomUUID().toString().substring(0, 8) +
|
(StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
|
|
Path formalFilePath = formalDirPath.resolve(formalFilename);
|
|
try {
|
// 执行文件迁移(使用原子操作确保安全性)
|
Files.move(
|
Paths.get(tempFile.getTempPath()),
|
formalFilePath,
|
StandardCopyOption.REPLACE_EXISTING,
|
StandardCopyOption.ATOMIC_MOVE
|
);
|
log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
|
|
// 更新文件记录(关联到业务ID)
|
CommonFile fileRecord = new CommonFile();
|
fileRecord.setCommonId(businessId);
|
fileRecord.setName(originalFilename);
|
fileRecord.setUrl(formalFilePath.toString());
|
fileRecord.setCreateTime(LocalDateTime.now());
|
fileRecord.setType("1");
|
commonFileMapper.insert(fileRecord);
|
|
// 删除临时文件记录
|
tempFileMapper.deleteById(tempFile);
|
|
log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
|
} catch (IOException e) {
|
log.error("文件迁移失败: {}", tempFile.getTempPath(), e);
|
// 可选择回滚事务或记录失败文件
|
throw new IOException("文件迁移异常", e);
|
}
|
}
|
}
|
|
|
private void handleSalesLedgerProducts(Long salesLedgerId, List<SalesLedgerProduct> products, Integer type) {
|
// 按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()) {
|
for (SalesLedgerProduct product : updateList) {
|
product.setType(type);
|
salesLedgerProductMapper.updateById(product);
|
}
|
}
|
// 执行插入操作
|
if (!insertList.isEmpty()) {
|
for (SalesLedgerProduct salesLedgerProduct : insertList) {
|
salesLedgerProduct.setType(type);
|
salesLedgerProduct.setNoInvoiceNum(salesLedgerProduct.getQuantity().intValue());
|
salesLedgerProduct.setNoInvoiceAmount(salesLedgerProduct.getTaxInclusiveTotalPrice());
|
salesLedgerProductMapper.insert(salesLedgerProduct);
|
}
|
}
|
}
|
|
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;
|
}
|
|
public <T, S> void updateMainContractAmount(
|
Long mainId,
|
List<T> subList,
|
Function<T, BigDecimal> amountGetter,
|
BaseMapper<S> mainMapper,
|
Class<S> mainEntityClass) {
|
|
if (mainId == null || subList == null || subList.isEmpty()) {
|
return;
|
}
|
|
// 计算子表金额总和
|
BigDecimal totalAmount = subList.stream()
|
.map(amountGetter)
|
.filter(Objects::nonNull)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
// 构造主表更新对象(支持任意主表类型)
|
try {
|
S entity = mainEntityClass.getDeclaredConstructor().newInstance();
|
Field idField = mainEntityClass.getDeclaredField("id");
|
idField.setAccessible(true);
|
idField.set(entity, mainId);
|
|
// 设置 contractAmount 字段,注意这里假设字段名为 "contractAmount"
|
Field amountField = mainEntityClass.getDeclaredField("contractAmount");
|
amountField.setAccessible(true);
|
amountField.set(entity, totalAmount);
|
|
mainMapper.updateById(entity);
|
} catch (Exception e) {
|
throw new RuntimeException("动态更新主表金额失败", e);
|
}
|
}
|
}
|