|
|
@@ -1,11 +1,335 @@
|
|
|
package com.usky.vpp.service.impl;
|
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.usky.common.core.bean.CommonPage;
|
|
|
+import com.usky.common.core.exception.BusinessException;
|
|
|
+import com.usky.vpp.domain.VppContract;
|
|
|
+import com.usky.vpp.domain.VppContractTemplate;
|
|
|
+import com.usky.vpp.domain.VppCustomer;
|
|
|
+import com.usky.vpp.mapper.VppContractMapper;
|
|
|
+import com.usky.vpp.mapper.VppContractTemplateMapper;
|
|
|
+import com.usky.vpp.mapper.VppCustomerMapper;
|
|
|
import com.usky.vpp.service.VppContractService;
|
|
|
+import com.usky.vpp.service.vo.VppContractRequestVO;
|
|
|
+import com.usky.vpp.service.vo.VppContractResponseVO;
|
|
|
+import com.usky.vpp.util.VppAuditHelper;
|
|
|
+import com.usky.vpp.util.VppFieldValidator;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
+
|
|
|
+import javax.servlet.ServletOutputStream;
|
|
|
+import javax.servlet.http.HttpServletResponse;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.UnsupportedEncodingException;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.function.Function;
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
- * VppContractService 默认实现(待按业务完善)
|
|
|
+ * VppContractService 默认实现
|
|
|
+ * @author fyc
|
|
|
+ * @email yuchuan.fu@chinausky.com
|
|
|
+ * @date 2026/7/4
|
|
|
*/
|
|
|
@Service
|
|
|
public class VppContractServiceImpl implements VppContractService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private VppContractMapper vppContractMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppContractTemplateMapper vppContractTemplateMapper;
|
|
|
+ @Autowired
|
|
|
+ private VppCustomerMapper vppCustomerMapper;
|
|
|
+
|
|
|
+ private final RestTemplate restTemplate = new RestTemplate();
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Boolean create(VppContractRequestVO vo) {
|
|
|
+ // ========== 根据 templateId 查询模板编码并生成合同编号 ==========
|
|
|
+ VppContractTemplate template = getTemplateById(vo.getTemplateId());
|
|
|
+ String contractNo = VppFieldValidator.generateContractNo(template.getTemplateCode());
|
|
|
+
|
|
|
+ // ========== 合同编号全租户唯一性校验(极小概率冲突) ==========
|
|
|
+ checkContractNoUnique(contractNo, null);
|
|
|
+
|
|
|
+ VppContract contract = new VppContract();
|
|
|
+ contract.setContractNo(contractNo);
|
|
|
+ contract.setCustomerId(vo.getCustomerId());
|
|
|
+ contract.setTemplateId(vo.getTemplateId());
|
|
|
+ contract.setContractType(vo.getContractType());
|
|
|
+ contract.setContractName(vo.getContractName());
|
|
|
+ contract.setContractStatus(vo.getContractStatus());
|
|
|
+ contract.setSignDate(vo.getSignDate());
|
|
|
+ contract.setEffectiveDate(vo.getEffectiveDate());
|
|
|
+ contract.setExpireDate(vo.getExpireDate());
|
|
|
+ contract.setFileUrl(vo.getFileUrl());
|
|
|
+ contract.setShareRatio(vo.getShareRatio());
|
|
|
+ contract.setPriceJson(VppFieldValidator.validateJson(vo.getPriceJson(), "价格定义"));
|
|
|
+ contract.setAccountInfoJson(VppFieldValidator.validateJson(vo.getAccountInfoJson(), "账户信息"));
|
|
|
+ contract.setRemark(vo.getRemark());
|
|
|
+
|
|
|
+ VppAuditHelper.fillCreate(contract);
|
|
|
+
|
|
|
+ int insert = vppContractMapper.insert(contract);
|
|
|
+ return insert > 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Boolean update(VppContractRequestVO vo) {
|
|
|
+ if (vo.getId() == null) {
|
|
|
+ throw new BusinessException("主键ID不能为空!");
|
|
|
+ }
|
|
|
+
|
|
|
+ LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(VppContract::getId, vo.getId())
|
|
|
+ .eq(VppContract::getDeleteFlag, 0);
|
|
|
+ VppContract existingRecord = vppContractMapper.selectOne(queryWrapper);
|
|
|
+
|
|
|
+ if (existingRecord == null) {
|
|
|
+ throw new BusinessException("合同不存在或已被删除!");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 仅更新非空字段
|
|
|
+ if (vo.getCustomerId() != null) {
|
|
|
+ existingRecord.setCustomerId(vo.getCustomerId());
|
|
|
+ }
|
|
|
+ if (vo.getTemplateId() != null) {
|
|
|
+ existingRecord.setTemplateId(vo.getTemplateId());
|
|
|
+ }
|
|
|
+ if (vo.getContractType() != null) {
|
|
|
+ existingRecord.setContractType(vo.getContractType());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(vo.getContractName())) {
|
|
|
+ existingRecord.setContractName(vo.getContractName());
|
|
|
+ }
|
|
|
+ if (vo.getContractStatus() != null) {
|
|
|
+ existingRecord.setContractStatus(vo.getContractStatus());
|
|
|
+ }
|
|
|
+ if (vo.getSignDate() != null) {
|
|
|
+ existingRecord.setSignDate(vo.getSignDate());
|
|
|
+ }
|
|
|
+ if (vo.getEffectiveDate() != null) {
|
|
|
+ existingRecord.setEffectiveDate(vo.getEffectiveDate());
|
|
|
+ }
|
|
|
+ if (vo.getExpireDate() != null) {
|
|
|
+ existingRecord.setExpireDate(vo.getExpireDate());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(vo.getFileUrl())) {
|
|
|
+ existingRecord.setFileUrl(vo.getFileUrl());
|
|
|
+ }
|
|
|
+ if (vo.getShareRatio() != null) {
|
|
|
+ existingRecord.setShareRatio(vo.getShareRatio());
|
|
|
+ }
|
|
|
+ if (vo.getPriceJson() != null) {
|
|
|
+ existingRecord.setPriceJson(VppFieldValidator.validateJson(vo.getPriceJson(), "价格定义"));
|
|
|
+ }
|
|
|
+ if (vo.getAccountInfoJson() != null) {
|
|
|
+ existingRecord.setAccountInfoJson(VppFieldValidator.validateJson(vo.getAccountInfoJson(), "账户信息"));
|
|
|
+ }
|
|
|
+ if (vo.getRemark() != null) {
|
|
|
+ existingRecord.setRemark(vo.getRemark());
|
|
|
+ }
|
|
|
+
|
|
|
+ VppAuditHelper.fillUpdate(existingRecord);
|
|
|
+
|
|
|
+ int result = vppContractMapper.updateById(existingRecord);
|
|
|
+ return result > 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public CommonPage<VppContractResponseVO> page(Long id, LocalDate signDateStart, LocalDate signDateEnd,
|
|
|
+ Long templateId, Integer contractType, Long customerId,
|
|
|
+ Integer contractStatus, Integer pageNum, Integer pageSize) {
|
|
|
+ IPage<VppContract> page = new Page<>(pageNum, pageSize);
|
|
|
+
|
|
|
+ LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(VppContract::getDeleteFlag, 0)
|
|
|
+ .eq(id != null, VppContract::getId, id)
|
|
|
+ .ge(signDateStart != null, VppContract::getSignDate, signDateStart)
|
|
|
+ .le(signDateEnd != null, VppContract::getSignDate, signDateEnd)
|
|
|
+ .eq(templateId != null, VppContract::getTemplateId, templateId)
|
|
|
+ .eq(contractType != null, VppContract::getContractType, contractType)
|
|
|
+ .eq(customerId != null, VppContract::getCustomerId, customerId)
|
|
|
+ .eq(contractStatus != null, VppContract::getContractStatus, contractStatus)
|
|
|
+ .orderByDesc(VppContract::getCreateTime);
|
|
|
+ page = vppContractMapper.selectPage(page, queryWrapper);
|
|
|
+
|
|
|
+ List<VppContract> records = page.getRecords();
|
|
|
+ if (records == null || records.isEmpty()) {
|
|
|
+ return new CommonPage<>(Collections.emptyList(), page.getTotal(), pageSize, pageNum);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 批量反查客户名称和模板名称 ==========
|
|
|
+ Set<Long> customerIds = records.stream().map(VppContract::getCustomerId).filter(Objects::nonNull).collect(Collectors.toSet());
|
|
|
+ Set<Long> templateIds = records.stream().map(VppContract::getTemplateId).filter(Objects::nonNull).collect(Collectors.toSet());
|
|
|
+
|
|
|
+ final Map<Long, String> customerNameMap = customerIds.isEmpty() ? Collections.emptyMap()
|
|
|
+ : vppCustomerMapper.selectBatchIds(customerIds).stream()
|
|
|
+ .collect(Collectors.toMap(VppCustomer::getId, VppCustomer::getCustomerName, (a, b) -> a));
|
|
|
+
|
|
|
+ final Map<Long, String> templateNameMap = templateIds.isEmpty() ? Collections.emptyMap()
|
|
|
+ : vppContractTemplateMapper.selectBatchIds(templateIds).stream()
|
|
|
+ .collect(Collectors.toMap(VppContractTemplate::getId, VppContractTemplate::getTemplateName, (a, b) -> a));
|
|
|
+
|
|
|
+ // ========== 组装响应VO ==========
|
|
|
+ List<VppContractResponseVO> list = records.stream().map(entity -> {
|
|
|
+ VppContractResponseVO vo = new VppContractResponseVO();
|
|
|
+ vo.setId(entity.getId());
|
|
|
+ vo.setContractNo(entity.getContractNo());
|
|
|
+ vo.setCustomerId(entity.getCustomerId());
|
|
|
+ vo.setCustomerName(customerNameMap.get(entity.getCustomerId()));
|
|
|
+ vo.setTemplateId(entity.getTemplateId());
|
|
|
+ vo.setTemplateName(templateNameMap.get(entity.getTemplateId()));
|
|
|
+ vo.setContractType(entity.getContractType());
|
|
|
+ vo.setContractName(entity.getContractName());
|
|
|
+ vo.setContractStatus(entity.getContractStatus());
|
|
|
+ vo.setSignDate(entity.getSignDate());
|
|
|
+ vo.setEffectiveDate(entity.getEffectiveDate());
|
|
|
+ vo.setExpireDate(entity.getExpireDate());
|
|
|
+ vo.setFileUrl(entity.getFileUrl());
|
|
|
+ vo.setShareRatio(entity.getShareRatio());
|
|
|
+ vo.setPriceJson(entity.getPriceJson());
|
|
|
+ vo.setAccountInfoJson(entity.getAccountInfoJson());
|
|
|
+ vo.setRemark(entity.getRemark());
|
|
|
+ vo.setTenantId(entity.getTenantId());
|
|
|
+ vo.setCreateTime(entity.getCreateTime());
|
|
|
+ vo.setUpdateTime(entity.getUpdateTime());
|
|
|
+ vo.setCreatedBy(entity.getCreatedBy());
|
|
|
+ vo.setUpdatedBy(entity.getUpdatedBy());
|
|
|
+ return vo;
|
|
|
+ }).collect(Collectors.toList());
|
|
|
+
|
|
|
+ return new CommonPage<>(list, page.getTotal(), pageSize, pageNum);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Boolean delete(Long id) {
|
|
|
+ LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(VppContract::getId, id)
|
|
|
+ .eq(VppContract::getDeleteFlag, 0);
|
|
|
+ VppContract contract = vppContractMapper.selectOne(queryWrapper);
|
|
|
+
|
|
|
+ if (contract == null) {
|
|
|
+ throw new BusinessException("合同不存在或已被删除!");
|
|
|
+ }
|
|
|
+
|
|
|
+ VppAuditHelper.fillSoftDelete(contract);
|
|
|
+
|
|
|
+ int i = vppContractMapper.updateById(contract);
|
|
|
+ return i > 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void download(Long id, HttpServletResponse response) {
|
|
|
+ LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(VppContract::getId, id)
|
|
|
+ .eq(VppContract::getDeleteFlag, 0);
|
|
|
+ VppContract contract = vppContractMapper.selectOne(queryWrapper);
|
|
|
+
|
|
|
+ if (contract == null) {
|
|
|
+ writeErrorResponse(response, HttpStatus.BAD_REQUEST.value(), "合同不存在或已被删除!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String fileUrl = contract.getFileUrl();
|
|
|
+ String fileName = contract.getContractName();
|
|
|
+ if (StringUtils.isBlank(fileUrl)) {
|
|
|
+ writeErrorResponse(response, HttpStatus.BAD_REQUEST.value(), "合同文件URL为空,无法下载!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 设置响应头 ==========
|
|
|
+ String downloadFileName = fileName;
|
|
|
+ int dotIndex = fileUrl.lastIndexOf('.');
|
|
|
+ if (dotIndex > 0 && !fileName.contains(".")) {
|
|
|
+ downloadFileName = fileName + fileUrl.substring(dotIndex);
|
|
|
+ }
|
|
|
+ response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
|
|
+ response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
|
|
|
+ "attachment; filename=" + encodeFileName(downloadFileName));
|
|
|
+
|
|
|
+ // ========== 下载文件并写入响应流 ==========
|
|
|
+ try {
|
|
|
+ byte[] fileBytes = restTemplate.getForObject(fileUrl, byte[].class);
|
|
|
+ if (fileBytes == null || fileBytes.length == 0) {
|
|
|
+ writeErrorResponse(response, HttpStatus.NOT_FOUND.value(), "合同文件内容为空!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ ServletOutputStream sos = response.getOutputStream();
|
|
|
+ sos.write(fileBytes);
|
|
|
+ sos.flush();
|
|
|
+ } catch (Exception e) {
|
|
|
+ if (!response.isCommitted()) {
|
|
|
+ writeErrorResponse(response, HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
|
|
+ "下载合同文件失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 内部辅助方法 ==========
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据模板ID查询模板信息
|
|
|
+ */
|
|
|
+ private VppContractTemplate getTemplateById(Long templateId) {
|
|
|
+ if (templateId == null) {
|
|
|
+ throw new BusinessException("模板ID不能为空!");
|
|
|
+ }
|
|
|
+ LambdaQueryWrapper<VppContractTemplate> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(VppContractTemplate::getId, templateId)
|
|
|
+ .eq(VppContractTemplate::getDeleteFlag, 0);
|
|
|
+ VppContractTemplate template = vppContractTemplateMapper.selectOne(wrapper);
|
|
|
+ if (template == null) {
|
|
|
+ throw new BusinessException("合同模板不存在或已被删除!");
|
|
|
+ }
|
|
|
+ return template;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 合同编号全租户唯一性校验
|
|
|
+ */
|
|
|
+ private void checkContractNoUnique(String contractNo, Long excludeId) {
|
|
|
+ LambdaQueryWrapper<VppContract> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(VppContract::getContractNo, contractNo)
|
|
|
+ .eq(VppContract::getDeleteFlag, 0)
|
|
|
+ .ne(excludeId != null, VppContract::getId, excludeId);
|
|
|
+ Integer count = vppContractMapper.selectCount(wrapper);
|
|
|
+ if (count != null && count > 0) {
|
|
|
+ throw new BusinessException("合同编号【" + contractNo + "】已存在,请重新提交!");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void writeErrorResponse(HttpServletResponse response, int status, String message) {
|
|
|
+ response.setStatus(status);
|
|
|
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
|
|
+ response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
|
|
+ try {
|
|
|
+ response.getWriter().write("{\"code\":\"" + status + "\",\"msg\":\"" + message + "\"}");
|
|
|
+ } catch (IOException e) {
|
|
|
+ // 忽略写入异常
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String encodeFileName(String fileName) {
|
|
|
+ try {
|
|
|
+ return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
|
|
|
+ } catch (UnsupportedEncodingException e) {
|
|
|
+ return fileName;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|