完善客商模块

This commit is contained in:
2026-08-01 01:22:46 +08:00
parent e03b554473
commit 454ca91760
15 changed files with 280 additions and 41 deletions

View File

@@ -110,8 +110,8 @@
id AS source_id,
code AS site_code,
name AS address_name,
CONCAT_WS('', country, city) AS region_name,
region_code,
CONCAT_WS('', country, city, district_name) AS region_name,
district_code AS region_code,
detail_address,
longitude,
latitude
@@ -154,8 +154,8 @@
id AS source_id,
code AS site_code,
name AS address_name,
CONCAT_WS('', country, city) AS region_name,
region_code,
CONCAT_WS('', country, city, district_name) AS region_name,
district_code AS region_code,
detail_address,
longitude,
latitude

View File

@@ -20,9 +20,12 @@
<result column="customer_type" property="customerType"/>
<result column="project_name" property="projectName"/>
<result column="registered_address" property="registeredAddress"/>
<result column="registered_region_name" property="registeredRegionName"/>
<result column="registered_detail_address" property="registeredDetailAddress"/>
<result column="legal_person" property="legalPerson"/>
<result column="contact_phone" property="contactPhone"/>
<result column="dept_id" property="deptId"/>
<result column="dept_ids" property="deptIds"/>
<result column="dept_name" property="deptName"/>
<result column="invoice_tax_rate" property="invoiceTaxRate"/>
<result column="business_scope" property="businessScope"/>
@@ -62,9 +65,12 @@
customer_type,
project_name,
registered_address,
registered_region_name,
registered_detail_address,
legal_person,
contact_phone,
dept_id,
dept_ids,
dept_name,
invoice_tax_rate,
business_scope,

View File

@@ -63,6 +63,7 @@ import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
import org.springblade.transport.pojo.vo.CustomerContactVO;
import org.springblade.transport.pojo.vo.CustomerCreditScoreDetailVO;
import org.springblade.transport.pojo.vo.CustomerCreditScoreVO;
import org.springblade.transport.pojo.vo.CreditRatingStandardVO;
import org.springblade.transport.pojo.vo.CustomerInvoiceInfoVO;
import org.springblade.transport.pojo.vo.CustomerReceiptAccountVO;
import org.springblade.transport.service.ICustomerArchiveService;
@@ -73,14 +74,12 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -101,8 +100,6 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
private static final String APPROVAL_APPROVED = "approved";
private static final String APPROVAL_REJECTED = "rejected";
private static final String CUSTOMER_CODE_PREFIX = "KS";
private static final Set<String> CUSTOMER_NATURES = Set.of("有限公司", "个体工商户");
private static final Set<String> CUSTOMER_TYPES = Set.of("客户", "承运商");
private final CustomerContactMapper contactMapper;
private final CustomerReceiptAccountMapper receiptAccountMapper;
@@ -258,8 +255,13 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
customer.setCustomerNature(trimToEmpty(customer.getCustomerNature()));
customer.setUnifiedCreditCode(trimToEmpty(customer.getUnifiedCreditCode()));
customer.setCustomerType(trimToEmpty(customer.getCustomerType()));
customer.setBusinessScope(trimToNull(customer.getBusinessScope()));
customer.setRegisteredRegionName(trimToNull(customer.getRegisteredRegionName()));
customer.setRegisteredDetailAddress(trimToNull(customer.getRegisteredDetailAddress()));
customer.setRegisteredAddress(buildRegisteredAddress(customer));
customer.setLegalPerson(trimToEmpty(customer.getLegalPerson()));
customer.setContactPhone(trimToEmpty(customer.getContactPhone()));
customer.setDeptIds(trimToNull(customer.getDeptIds()));
customer.setDeptName(trimToEmpty(customer.getDeptName()));
customer.setAccessType(Func.isEmpty(customer.getAccessType()) ? ACCESS_TEMPORARY : customer.getAccessType());
customer.setApprovalStatus(Func.isEmpty(customer.getApprovalStatus()) ? APPROVAL_DRAFT : customer.getApprovalStatus());
@@ -279,9 +281,6 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
if (Func.isEmpty(customer.getCustomerNature())) {
throw new ServiceException("客商性质不能为空");
}
if (!CUSTOMER_NATURES.contains(customer.getCustomerNature())) {
throw new ServiceException("客商性质只能选择有限公司或个体工商户");
}
if (Func.isEmpty(customer.getUnifiedCreditCode())) {
throw new ServiceException("统一社会信用代码不能为空");
}
@@ -311,10 +310,13 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
validateLength(customer.getFullName(), 100, "客商全称最多100个字符");
validateLength(customer.getLegalPerson(), 10, "法人/负责人最多10个汉字");
validateLength(customer.getContactPhone(), 11, "联系电话必须为11位手机号");
validateLength(customer.getRegisteredAddress(), 200, "地址最多200个字符");
validateLength(customer.getRegisteredRegionName(), 100, "省市区最多100个字符");
validateLength(customer.getRegisteredDetailAddress(), 255, "详细地址最多255个字符");
validateLength(customer.getRegisteredAddress(), 400, "地址最多400个字符");
validateLength(customer.getBusinessScope(), 500, "经营范围最多500个字符");
validateLength(customer.getRemark(), 500, "备注最多500个字符");
validateNonNegative(customer.getInvoiceTaxRate(), "开票税点不能小于0");
validateMax(customer.getInvoiceTaxRate(), new BigDecimal("13"), "开票税点必须在0到13之间");
validateNonNegative(customer.getRegisteredCapital(), "注册资金不能小于0");
validateNonNegative(customer.getMaxCreditLimit(), "最大资金使用额度不能小于0");
validateNonNegative(customer.getApplyCreditLimit(), "申请总资金使用额度不能小于0");
@@ -322,24 +324,20 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
}
private void validateCustomerType(String customerType) {
List<String> typeList = Arrays.stream(customerType.replace("", ",").split(","))
List<String> typeList = Func.toStrList(customerType.replace("", ","))
.stream()
.map(String::trim)
.filter(Func::isNotEmpty)
.toList();
if (Func.isEmpty(typeList)) {
throw new ServiceException("客商类型不能为空");
}
for (String type : typeList) {
if (!CUSTOMER_TYPES.contains(type)) {
throw new ServiceException("客商类型只能选择客户或承运商");
}
}
}
private void validateApproval(CustomerArchiveVO customer) {
if (Objects.equals(customer.getAccessType(), ACCESS_FORMAL)) {
if (Func.isEmpty(customer.getQualificationAttachments())) {
throw new ServiceException("正式客商提交审核前必须上传资质附件");
throw new ServiceException("正式客商提交审核前必须上传客商材料");
}
boolean hasCompletedScore = customer.getScores().stream()
.anyMatch(score -> "已完成".equals(score.getSelfStatus()) || "已完成".equals(score.getReviewStatus()) || Func.isNotEmpty(score.getFinalScore()));
@@ -403,6 +401,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
if (Func.isEmpty(invoiceVO.getInvoiceTitle()) && Func.isEmpty(invoiceVO.getTaxNo())) {
continue;
}
invoiceVO.setRegisteredAddress(buildAddress(invoiceVO.getRegisteredRegionName(), invoiceVO.getRegisteredDetailAddress(), invoiceVO.getRegisteredAddress()));
invoiceVO.setReceiverAddress(buildAddress(invoiceVO.getReceiverRegionName(), invoiceVO.getReceiverDetailAddress(), invoiceVO.getReceiverAddress()));
CustomerInvoiceInfo invoice = Objects.requireNonNull(BeanUtil.copyProperties(invoiceVO, CustomerInvoiceInfo.class));
invoice.setId(IdWorker.getId());
invoice.setCustomerId(customerId);
@@ -417,6 +417,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
continue;
}
Long scoreId = IdWorker.getId();
scoreVO.setProofAttachments(normalizeScoreProofAttachments(scoreVO));
CustomerCreditScore score = Objects.requireNonNull(BeanUtil.copyProperties(scoreVO, CustomerCreditScore.class));
score.setId(scoreId);
score.setCustomerId(customerId);
@@ -424,7 +425,11 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
creditScoreMapper.insert(score);
List<CustomerCreditScoreDetailVO> details = scoreVO.getDetails() == null ? new ArrayList<>() : scoreVO.getDetails();
for (int detailIndex = 0; detailIndex < details.size(); detailIndex++) {
CustomerCreditScoreDetail detail = Objects.requireNonNull(BeanUtil.copyProperties(details.get(detailIndex), CustomerCreditScoreDetail.class));
CustomerCreditScoreDetailVO detailVO = details.get(detailIndex);
if (Func.isEmpty(detailVO.getScore())) {
detailVO.setScore(resolveFullScore(detailVO.getOptionsJson()));
}
CustomerCreditScoreDetail detail = Objects.requireNonNull(BeanUtil.copyProperties(detailVO, CustomerCreditScoreDetail.class));
detail.setId(IdWorker.getId());
detail.setScoreId(scoreId);
detail.setQuantificationId(score.getQuantificationId());
@@ -477,7 +482,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
private List<CustomerCreditScoreVO> loadScores(Long customerId) {
List<CustomerCreditScore> scores = creditScoreMapper.selectList(Wrappers.<CustomerCreditScore>lambdaQuery()
.eq(CustomerCreditScore::getCustomerId, customerId)
.eq(CustomerCreditScore::getIsDeleted, 0)
.and(wrapper -> wrapper.eq(CustomerCreditScore::getIsDeleted, 0).or().isNull(CustomerCreditScore::getIsDeleted))
.orderByDesc(CustomerCreditScore::getScoreDate)
.orderByDesc(CustomerCreditScore::getCreateTime));
if (Func.isEmpty(scores)) {
@@ -486,18 +491,45 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
List<Long> scoreIds = scores.stream().map(CustomerCreditScore::getId).toList();
Map<Long, List<CustomerCreditScoreDetailVO>> detailMap = creditScoreDetailMapper.selectList(Wrappers.<CustomerCreditScoreDetail>lambdaQuery()
.in(CustomerCreditScoreDetail::getScoreId, scoreIds)
.eq(CustomerCreditScoreDetail::getIsDeleted, 0)
.and(wrapper -> wrapper.eq(CustomerCreditScoreDetail::getIsDeleted, 0).or().isNull(CustomerCreditScoreDetail::getIsDeleted))
.orderByAsc(CustomerCreditScoreDetail::getSort))
.stream()
.map(detail -> Objects.requireNonNull(BeanUtil.copyProperties(detail, CustomerCreditScoreDetailVO.class)))
.collect(Collectors.groupingBy(CustomerCreditScoreDetailVO::getScoreId));
return scores.stream().map(score -> {
CustomerCreditScoreVO scoreVO = Objects.requireNonNull(BeanUtil.copyProperties(score, CustomerCreditScoreVO.class));
scoreVO.setAttachments(parseScoreAttachments(score.getProofAttachments()));
scoreVO.setDetails(detailMap.getOrDefault(score.getId(), new ArrayList<>()));
scoreVO.setStandards(loadStandards(score.getQuantificationId()));
return scoreVO;
}).toList();
}
private String normalizeScoreProofAttachments(CustomerCreditScoreVO scoreVO) {
if (Func.isNotEmpty(scoreVO.getProofAttachments())) {
return trimToNull(scoreVO.getProofAttachments());
}
Object attachments = scoreVO.getAttachments();
if (Func.isEmpty(attachments)) {
return null;
}
if (attachments instanceof String attachmentText) {
return trimToNull(attachmentText);
}
return JsonUtil.toJson(attachments);
}
private Object parseScoreAttachments(String value) {
if (Func.isEmpty(value)) {
return new ArrayList<>();
}
try {
return JsonUtil.parse(value, List.class);
} catch (Exception exception) {
return new ArrayList<>();
}
}
private List<CustomerChangeRecordVO> loadChangeRecords(Long customerId) {
return changeRecordMapper.selectList(Wrappers.<CustomerChangeRecord>lambdaQuery()
.eq(CustomerChangeRecord::getCustomerId, customerId)
@@ -549,7 +581,9 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
detail.setItemName(item.getItemName());
detail.setOptionDescription(item.getOptionDescription());
detail.setScoreDescription(item.getScoreDescription());
detail.setOptionsJson(buildOptionsJson(optionMap.getOrDefault(item.getId(), new ArrayList<>())));
List<CreditScoreItemOption> itemOptions = optionMap.getOrDefault(item.getId(), new ArrayList<>());
detail.setScore(resolveFullScore(itemOptions));
detail.setOptionsJson(buildOptionsJson(itemOptions));
detail.setSelfScore(BigDecimal.ZERO);
detail.setReviewScore(BigDecimal.ZERO);
details.add(detail);
@@ -570,6 +604,61 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
return JsonUtil.toJson(optionList);
}
private List<CreditRatingStandardVO> loadStandards(Long quantificationId) {
if (Func.isEmpty(quantificationId)) {
return new ArrayList<>();
}
return standardMapper.selectList(Wrappers.<CreditRatingStandard>lambdaQuery()
.eq(CreditRatingStandard::getQuantificationId, quantificationId)
.eq(CreditRatingStandard::getIsDeleted, 0)
.orderByAsc(CreditRatingStandard::getSort))
.stream()
.map(standard -> Objects.requireNonNull(BeanUtil.copyProperties(standard, CreditRatingStandardVO.class)))
.toList();
}
private BigDecimal resolveFullScore(List<CreditScoreItemOption> options) {
return options.stream()
.map(CreditScoreItemOption::getScore)
.filter(Objects::nonNull)
.max(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO);
}
private BigDecimal resolveFullScore(String optionsJson) {
if (Func.isEmpty(optionsJson)) {
return BigDecimal.ZERO;
}
try {
Object value = JsonUtil.parse(optionsJson, List.class);
if (!(value instanceof List<?> options)) {
return BigDecimal.ZERO;
}
return options.stream()
.map(this::resolveOptionScore)
.filter(Objects::nonNull)
.max(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO);
} catch (Exception exception) {
return BigDecimal.ZERO;
}
}
private BigDecimal resolveOptionScore(Object option) {
if (!(option instanceof Map<?, ?> optionMap)) {
return null;
}
Object score = optionMap.get("score");
if (score == null) {
return null;
}
try {
return new BigDecimal(String.valueOf(score));
} catch (NumberFormatException exception) {
return null;
}
}
private String nextCustomerCode() {
CustomerArchive latest = list(Wrappers.<CustomerArchive>lambdaQuery()
.likeRight(CustomerArchive::getCustomerCode, CUSTOMER_CODE_PREFIX)
@@ -650,6 +739,25 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
}
}
private void validateMax(BigDecimal value, BigDecimal max, String message) {
if (Func.isNotEmpty(value) && value.compareTo(max) > 0) {
throw new ServiceException(message);
}
}
private String buildRegisteredAddress(CustomerArchiveVO customer) {
return buildAddress(customer.getRegisteredRegionName(), customer.getRegisteredDetailAddress(), customer.getRegisteredAddress());
}
private String buildAddress(String regionNameValue, String detailAddressValue, String fallbackAddress) {
String regionName = trimToEmpty(regionNameValue);
String detailAddress = trimToEmpty(detailAddressValue);
if (Func.isNotEmpty(regionName) || Func.isNotEmpty(detailAddress)) {
return regionName + detailAddress;
}
return trimToNull(fallbackAddress);
}
private String trimToEmpty(String value) {
return value == null ? "" : value.trim();
}