调整基础数据、车船务、项目、合同

This commit is contained in:
2026-09-03 22:40:19 +08:00
parent 7f87cd8c27
commit 809b4eb63a
23 changed files with 279 additions and 31 deletions
@@ -34,6 +34,7 @@ import lombok.EqualsAndHashCode;
import org.springblade.core.mp.base.BaseEntity;
import java.io.Serial;
import java.math.BigDecimal;
/**
* 费用项实体类
@@ -64,6 +65,11 @@ public class FeeItem extends BaseEntity {
*/
@Schema(description = "费用项代码")
private String englishName;
/**
* 税率(百分比)
*/
@Schema(description = "税率(百分比)")
private BigDecimal taxRate;
/**
* 备注
*/
@@ -150,7 +150,7 @@ public class ContractManage extends TenantEntity {
@Schema(description = "其它附件JSON")
private String attachmentsJson;
@Schema(description = "计费方案JSON")
@Schema(description = "计费方案JSON(含规则税率)")
private String billingPlanJson;
@Schema(description = "结算生成规则JSON")
@@ -172,11 +172,17 @@ public class ContractManage extends TenantEntity {
private String changeRecordJson;
@Schema(description = "变更内容")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String changeContent;
@Schema(description = "变更原因")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String changeReason;
@Schema(description = "待审批变更附件JSON")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String changeAttachmentsJson;
@Schema(description = "终止原因")
private String terminateReason;
@@ -34,6 +34,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 费用项 Excel
@@ -60,6 +61,9 @@ public class FeeItemExcel implements Serializable {
@ExcelProperty("*费用项")
private String name;
@ExcelProperty("*税率")
private BigDecimal taxRate;
@ExcelIgnore
private String errorMessage;
@@ -15,6 +15,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
@@ -40,6 +41,9 @@ public class FeeItemExportExcel implements Serializable {
@ExcelProperty("费用项")
private String name;
@ExcelProperty("税率")
private BigDecimal taxRate;
@ExcelProperty("备注")
private String remark;
@@ -33,6 +33,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 费用项导入失败 Excel
@@ -56,6 +57,9 @@ public class FeeItemImportFailureExcel implements Serializable {
@ExcelProperty("*费用项")
private String name;
@ExcelProperty("*税率")
private BigDecimal taxRate;
@ExcelProperty("导入失败原因")
private String failureReason;
@@ -16,6 +16,7 @@
<result column="fee_category" property="feeCategory"/>
<result column="name" property="name"/>
<result column="english_name" property="englishName"/>
<result column="tax_rate" property="taxRate"/>
<result column="remark" property="remark"/>
</resultMap>
@@ -47,6 +47,7 @@ import org.springblade.system.service.IFeeItemService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -66,6 +67,8 @@ public class FeeItemServiceImpl extends BaseServiceImpl<FeeItemMapper, FeeItem>
private static final int NAME_MAX_LENGTH = 50;
private static final int ENGLISH_NAME_MAX_LENGTH = 100;
private static final int REMARK_MAX_LENGTH = 200;
private static final BigDecimal TAX_RATE_MIN = BigDecimal.ZERO;
private static final BigDecimal TAX_RATE_MAX = new BigDecimal("100");
@Override
public IPage<FeeItemVO> selectFeeItemPage(IPage<FeeItemVO> page, FeeItemVO feeItem) {
@@ -115,7 +118,7 @@ public class FeeItemServiceImpl extends BaseServiceImpl<FeeItemMapper, FeeItem>
save(feeItem);
} catch (Exception exception) {
String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败";
failureList.add(toImportFailureExcel(excel, message));
failureList.add(toImportFailureExcel(excel, "" + (index + 2) + "行:" + message));
}
}
return failureList;
@@ -156,6 +159,16 @@ public class FeeItemServiceImpl extends BaseServiceImpl<FeeItemMapper, FeeItem>
if (Func.isNotEmpty(feeItem.getEnglishName()) && feeItem.getEnglishName().length() > ENGLISH_NAME_MAX_LENGTH) {
throw new ServiceException("费用项代码不能超过100字");
}
BigDecimal taxRate = feeItem.getTaxRate();
if (taxRate == null) {
throw new ServiceException("税率不能为空");
}
if (taxRate.compareTo(TAX_RATE_MIN) < 0 || taxRate.compareTo(TAX_RATE_MAX) > 0) {
throw new ServiceException("税率必须在0到100之间");
}
if (taxRate.stripTrailingZeros().scale() > 2) {
throw new ServiceException("税率最多保留2位小数");
}
if (Func.isNotEmpty(feeItem.getRemark()) && feeItem.getRemark().length() > REMARK_MAX_LENGTH) {
throw new ServiceException("备注不能超过200个字");
}
@@ -192,6 +205,7 @@ public class FeeItemServiceImpl extends BaseServiceImpl<FeeItemMapper, FeeItem>
feeItem.setFeeCategory(resolveFeeCategory(excel.getFeeCategory()));
feeItem.setEnglishName(trimToNull(excel.getEnglishName()));
feeItem.setName(trimToEmpty(excel.getName()));
feeItem.setTaxRate(excel.getTaxRate());
feeItem.setStatus(STATUS_ENABLED);
return feeItem;
}
@@ -212,6 +226,7 @@ public class FeeItemServiceImpl extends BaseServiceImpl<FeeItemMapper, FeeItem>
failureExcel.setFeeCategory(excel.getFeeCategory());
failureExcel.setEnglishName(excel.getEnglishName());
failureExcel.setName(excel.getName());
failureExcel.setTaxRate(excel.getTaxRate());
failureExcel.setFailureReason(failureReason);
return failureExcel;
}
@@ -131,7 +131,7 @@ public class ContractManageController extends BladeController {
@ApiOperationSupport(order = 10)
@Operation(summary = "发起变更", description = "传入id、changeContent和changeReason")
public R startChange(@Parameter(description = "主键", required = true) @RequestParam Long id,
@RequestParam String changeContent,
@RequestParam(required = false) String changeContent,
@RequestParam String changeReason) {
return R.status(contractManageService.startChange(id, changeContent, changeReason));
}
@@ -42,16 +42,18 @@ import org.springblade.transport.wrapper.ContractManageWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* 合同管理 服务实现类
@@ -72,6 +74,9 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
private static final String STATUS_CHANGE_REVIEWING = "change_reviewing";
private static final String STATUS_CHANGE_APPROVED = "change_approved";
private static final String STATUS_CHANGE_REJECTED = "change_rejected";
private static final long TEMPORARY_VALIDITY_DAYS = 30L;
private static final BigDecimal TAX_RATE_MIN = BigDecimal.ZERO;
private static final BigDecimal TAX_RATE_MAX = BigDecimal.valueOf(100);
@Override
public IPage<ContractManageVO> selectContractManagePage(IPage<ContractManage> page, ContractManageVO contractManage) {
@@ -93,7 +98,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
public boolean saveDraft(ContractManage contractManage) {
prepare(contractManage);
validateDraft(contractManage);
if (!Objects.equals(contractManage.getContractStage(), STAGE_DRAFT)) {
if (Func.isNotEmpty(contractManage.getId()) && !Objects.equals(contractManage.getContractStage(), STAGE_DRAFT)) {
validateContractPartyCombinationUnique(contractManage);
}
if (Func.isNotEmpty(contractManage.getId())) {
@@ -115,13 +120,16 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
prepare(contractManage);
validateDraft(contractManage);
validateTemporary(contractManage);
validateContractPartyCombinationUnique(contractManage);
if (Func.isNotEmpty(contractManage.getId())) {
validateContractPartyCombinationUnique(contractManage);
}
if (Func.isNotEmpty(contractManage.getId())) {
loadEditable(contractManage.getId());
}
prepareCreateOrUpdate(contractManage);
contractManage.setContractStage(STAGE_TEMPORARY);
contractManage.setApprovalStatus(STATUS_REVIEWING);
applyTemporaryValidity(contractManage);
appendCreateChangeRecordIfAbsent(contractManage);
return saveOrUpdate(contractManage);
}
@@ -133,9 +141,11 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
if (!Objects.equals(contractManage.getContractStage(), STAGE_DRAFT) || !Objects.equals(contractManage.getApprovalStatus(), STATUS_DRAFT)) {
throw new ServiceException("仅草稿合同允许转临时合同");
}
validateContractPartyCombinationUnique(contractManage);
// 临时放开新增合同从草稿转临时合同时的组合重复校验。
// validateContractPartyCombinationUnique(contractManage);
contractManage.setContractStage(STAGE_TEMPORARY);
contractManage.setApprovalStatus(STATUS_REVIEWING);
applyTemporaryValidity(contractManage);
contractManage.setCurrentNode("临时合同审批");
contractManage.setCurrentProcessor("待处理");
return updateById(contractManage);
@@ -149,7 +159,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
throw new ServiceException("仅临时合同允许转正式合同");
}
validateFormal(contractManage);
validateContractPartyCombinationUnique(contractManage);
// 临时放开新增合同从临时转正式合同时的组合重复校验。
// validateContractPartyCombinationUnique(contractManage);
contractManage.setContractStage(STAGE_FORMAL);
contractManage.setApprovalStatus(STATUS_REVIEWING);
contractManage.setCurrentNode("正式合同审批");
@@ -166,8 +177,14 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
contractManage.setCurrentNode("审批通过");
contractManage.setCurrentProcessor(AuthUtil.getUserName());
contractManage.setApprovedTime(LocalDateTime.now());
if (Objects.equals(contractManage.getContractStage(), STAGE_TEMPORARY) && contractManage.getTemporaryStartDate() == null) {
contractManage.setTemporaryStartDate(LocalDate.now());
if (Objects.equals(contractManage.getContractStage(), STAGE_TEMPORARY)) {
ensureTemporaryValidity(contractManage);
}
if (changeReviewing) {
contractManage.setAttachmentsJson(mergeAttachmentJson(contractManage.getAttachmentsJson(), contractManage.getChangeAttachmentsJson()));
contractManage.setChangeAttachmentsJson(null);
contractManage.setChangeContent(null);
contractManage.setChangeReason(null);
}
updateLatestReviewingChangeRecord(contractManage, contractManage.getApprovalStatus(), "审核通过");
return updateById(contractManage);
@@ -181,6 +198,11 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
contractManage.setApprovalStatus(changeReviewing ? STATUS_CHANGE_REJECTED : STATUS_REJECTED);
contractManage.setCurrentNode("已驳回");
contractManage.setCurrentProcessor(AuthUtil.getUserName());
if (changeReviewing) {
contractManage.setChangeAttachmentsJson(null);
contractManage.setChangeContent(null);
contractManage.setChangeReason(null);
}
updateLatestReviewingChangeRecord(contractManage, contractManage.getApprovalStatus(), "审核不通过");
return updateById(contractManage);
}
@@ -193,6 +215,11 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
contractManage.setApprovalStatus(changeReviewing ? STATUS_APPROVED : STATUS_DRAFT);
contractManage.setCurrentNode("已撤回");
contractManage.setCurrentProcessor(AuthUtil.getUserName());
if (changeReviewing) {
contractManage.setChangeAttachmentsJson(null);
contractManage.setChangeContent(null);
contractManage.setChangeReason(null);
}
updateLatestReviewingChangeRecord(contractManage, "withdrawn", "已撤回");
return updateById(contractManage);
}
@@ -205,9 +232,12 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|| !List.of(STATUS_APPROVED, STATUS_CHANGE_APPROVED, STATUS_CHANGE_REJECTED).contains(contractManage.getApprovalStatus())) {
throw new ServiceException("仅正式合同允许发起变更");
}
String normalizedReason = TransportBusinessSupport.trimToNull(changeReason);
TransportBusinessSupport.validateRequired(normalizedReason, "请输入变更原因");
TransportBusinessSupport.validateLength(normalizedReason, 500, "变更原因不能超过500字");
contractManage.setApprovalStatus(STATUS_CHANGE_REVIEWING);
contractManage.setChangeContent(TransportBusinessSupport.trimToNull(changeContent));
contractManage.setChangeReason(TransportBusinessSupport.trimToNull(changeReason));
contractManage.setChangeReason(normalizedReason);
contractManage.setCurrentNode("合同变更审批");
contractManage.setCurrentProcessor("待处理");
appendChangeRecord(contractManage, "合同信息变更", contractManage.getChangeReason(), STATUS_CHANGE_REVIEWING, "审核中");
@@ -223,12 +253,16 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
throw new ServiceException("当前合同状态不允许发起变更");
}
TransportBusinessSupport.validateRequired(request.getSettlementCurrency(), "请选择结算币种");
String changeReason = TransportBusinessSupport.trimToNull(request.getChangeReason());
TransportBusinessSupport.validateRequired(changeReason, "请输入变更原因");
TransportBusinessSupport.validateLength(changeReason, 500, "变更原因不能超过500字");
ContractManage candidate = new ContractManage();
BeanUtil.copyProperties(source, candidate);
candidate.setPartyB(TransportBusinessSupport.trimToNull(request.getPartyB()));
validateContractPartyCombinationUnique(candidate);
validateBillingPlans(request.getBillingPlanJson());
validatePaymentRatios(request.getPaymentRatioJson());
Map<String, Object> beforeData = buildContractChangeSnapshot(source);
source.setContractName(request.getContractName()); source.setPartyB(request.getPartyB());
source.setStartDate(request.getStartDate()); source.setEndDate(request.getEndDate()); source.setContractFormat(request.getContractFormat());
source.setSettlementMode(request.getSettlementMode()); source.setLegalSealFlag(request.getLegalSealFlag()); source.setCopyCount(request.getCopyCount());
@@ -237,8 +271,16 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
normalizeOptionalIntegerFields(source);
source.setBillingPlanJson(request.getBillingPlanJson()); source.setSettlementRuleJson(request.getSettlementRuleJson()); source.setPreSettlementConfigJson(request.getPreSettlementConfigJson()); source.setFormalSettlementConfigJson(request.getFormalSettlementConfigJson()); source.setPaymentRatioJson(request.getPaymentRatioJson());
source.setContractFileJson(request.getContractFileJson()); source.setAttachmentsJson(request.getAttachmentsJson());
updateById(source);
return startChange(source.getId(), request.getChangeContent(), request.getChangeReason());
source.setChangeAttachmentsJson(TransportBusinessSupport.trimToNull(request.getChangeAttachmentsJson()));
Map<String, Object> afterData = buildContractChangeSnapshot(source);
retainChangedSnapshotFields(beforeData, afterData);
source.setApprovalStatus(STATUS_CHANGE_REVIEWING);
source.setChangeContent(TransportBusinessSupport.trimToNull(request.getChangeContent()));
source.setChangeReason(changeReason);
source.setCurrentNode("合同变更审批");
source.setCurrentProcessor("待处理");
appendChangeRecord(source, "合同信息变更", changeReason, STATUS_CHANGE_REVIEWING, "审核中", beforeData, afterData);
return updateById(source);
}
@Override
@@ -299,6 +341,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
target.setApprovedTime(null);
target.setChangeContent(null);
target.setChangeReason(null);
target.setChangeAttachmentsJson(null);
target.setTerminateReason(null);
saveDraft(target);
return detail(target.getId());
@@ -501,6 +544,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
}
}
validateBillingMatchConditions(plan);
validateBillingRuleTaxRates(plan);
}
} catch (ServiceException exception) {
throw exception;
@@ -525,6 +569,34 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
}
}
private void validateBillingRuleTaxRates(Map<?, ?> plan) {
if (!(plan.get("rules") instanceof List<?> rules)) return;
for (int index = 0; index < rules.size(); index++) {
Object ruleValue = rules.get(index);
if (!(ruleValue instanceof Map<?, ?> rule)) continue;
Object value = rule.get("taxRate");
if (value == null || String.valueOf(value).isBlank()) {
throw new ServiceException("计费方案第" + (index + 1) + "行税率不能为空");
}
String taxRateText = String.valueOf(value).trim();
if (!taxRateText.matches("\\d+(\\.\\d{1,2})?")) {
throw new ServiceException("计费方案第" + (index + 1) + "行税率格式不正确");
}
BigDecimal taxRate;
try {
taxRate = new BigDecimal(taxRateText);
} catch (NumberFormatException exception) {
throw new ServiceException("计费方案第" + (index + 1) + "行税率格式不正确");
}
if (taxRate.compareTo(TAX_RATE_MIN) < 0 || taxRate.compareTo(TAX_RATE_MAX) > 0) {
throw new ServiceException("计费方案第" + (index + 1) + "行税率必须在0到100之间");
}
if (taxRate.stripTrailingZeros().scale() > 2) {
throw new ServiceException("计费方案第" + (index + 1) + "行税率最多保留2位小数");
}
}
}
private boolean hasConfiguredValue(Object value) {
if (value instanceof Collection<?> values) {
return values.stream().anyMatch(this::hasConfiguredValue);
@@ -597,6 +669,29 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
TransportBusinessSupport.validateRequired(contractManage.getSignType(), "请选择签约类型");
}
/**
* 临时合同提交时从当前日期开始计算临时效力期,避免使用客户端传入的日期。
*/
private void applyTemporaryValidity(ContractManage contractManage) {
LocalDate temporaryStartDate = LocalDate.now();
contractManage.setTemporaryStartDate(temporaryStartDate);
contractManage.setTemporaryEndDate(temporaryStartDate.plusDays(TEMPORARY_VALIDITY_DAYS));
}
/**
* 审批历史临时合同时仅补齐缺失的日期,不覆盖已按提交时间保存的效力期。
*/
private void ensureTemporaryValidity(ContractManage contractManage) {
LocalDate temporaryStartDate = contractManage.getTemporaryStartDate();
if (temporaryStartDate == null) {
temporaryStartDate = LocalDate.now();
contractManage.setTemporaryStartDate(temporaryStartDate);
}
if (contractManage.getTemporaryEndDate() == null) {
contractManage.setTemporaryEndDate(temporaryStartDate.plusDays(TEMPORARY_VALIDITY_DAYS));
}
}
private void validateFormal(ContractManage contractManage) {
TransportBusinessSupport.validateRequired(contractManage.getSettlementCurrency(), "请选择结算币种");
TransportBusinessSupport.validateRequired(contractManage.getPartyA(), "请输入甲方");
@@ -643,19 +738,66 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
}
private void appendChangeRecord(ContractManage contractManage, String changeType, String changeReason, String status, String statusName) {
appendChangeRecord(contractManage, changeType, changeReason, status, statusName, null, null);
}
private void appendChangeRecord(ContractManage contractManage, String changeType, String changeReason, String status,
String statusName, Map<String, Object> beforeData, Map<String, Object> afterData) {
List<Map<String, Object>> records = parseChangeRecords(contractManage.getChangeRecordJson());
Map<String, Object> record = new LinkedHashMap<>();
record.put("changeDate", LocalDate.now().toString());
record.put("handlerUserId", AuthUtil.getUserId());
record.put("handlerUserName", AuthUtil.getUserName());
record.put("changeType", changeType);
record.put("changeContent", TransportBusinessSupport.trimToNull(contractManage.getChangeContent()));
record.put("changeReason", TransportBusinessSupport.trimToNull(changeReason));
record.put("status", status);
record.put("statusName", statusName);
if (beforeData != null && afterData != null) {
record.put("beforeData", beforeData);
record.put("afterData", afterData);
}
records.add(record);
contractManage.setChangeRecordJson(JsonUtil.toJson(records));
}
private Map<String, Object> buildContractChangeSnapshot(ContractManage contractManage) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("contractName", contractManage.getContractName());
data.put("partyB", contractManage.getPartyB());
data.put("startDate", contractManage.getStartDate());
data.put("endDate", contractManage.getEndDate());
data.put("contractFormat", contractManage.getContractFormat());
data.put("settlementMode", contractManage.getSettlementMode());
data.put("legalSealFlag", contractManage.getLegalSealFlag());
data.put("copyCount", contractManage.getCopyCount());
data.put("settlementCurrency", contractManage.getSettlementCurrency());
data.put("invoiceCycle", contractManage.getInvoiceCycle());
data.put("paymentDays", contractManage.getPaymentDays());
data.put("remark", contractManage.getRemark());
data.put("feeGenerationMode", contractManage.getFeeGenerationMode());
data.put("billingPlanJson", contractManage.getBillingPlanJson());
data.put("settlementRuleJson", contractManage.getSettlementRuleJson());
data.put("preSettlementConfigJson", contractManage.getPreSettlementConfigJson());
data.put("formalSettlementConfigJson", contractManage.getFormalSettlementConfigJson());
data.put("paymentRatioJson", contractManage.getPaymentRatioJson());
data.put("contractFileJson", contractManage.getContractFileJson());
data.put("attachmentsJson", contractManage.getAttachmentsJson());
data.put("changeAttachmentsJson", contractManage.getChangeAttachmentsJson());
return data;
}
private void retainChangedSnapshotFields(Map<String, Object> beforeData, Map<String, Object> afterData) {
Set<String> changedFields = new HashSet<>();
beforeData.forEach((field, value) -> {
if (!Objects.equals(value, afterData.get(field))) {
changedFields.add(field);
}
});
beforeData.keySet().retainAll(changedFields);
afterData.keySet().retainAll(changedFields);
}
private void updateLatestReviewingChangeRecord(ContractManage contractManage, String status, String statusName) {
List<Map<String, Object>> records = parseChangeRecords(contractManage.getChangeRecordJson());
for (int index = records.size() - 1; index >= 0; index--) {
@@ -669,6 +811,47 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
}
}
private String mergeAttachmentJson(String existingJson, String pendingJson) {
List<Object> merged = parseJsonArray(existingJson);
List<Object> pending = parseJsonArray(pendingJson);
if (pending.isEmpty()) {
return TransportBusinessSupport.trimToNull(existingJson);
}
Set<String> keys = new HashSet<>();
merged.forEach(item -> keys.add(attachmentKey(item)));
pending.forEach(item -> {
if (item != null && keys.add(attachmentKey(item))) {
merged.add(item);
}
});
return merged.isEmpty() ? null : JsonUtil.toJson(merged);
}
@SuppressWarnings("unchecked")
private List<Object> parseJsonArray(String value) {
if (Func.isEmpty(value)) {
return new ArrayList<>();
}
try {
Object parsed = JsonUtil.parse(value, List.class);
return parsed instanceof List<?> list ? new ArrayList<>((List<Object>) list) : new ArrayList<>();
} catch (Exception ignored) {
return new ArrayList<>();
}
}
private String attachmentKey(Object attachment) {
if (attachment instanceof Map<?, ?> map) {
for (String field : List.of("url", "link", "fileUrl", "downloadUrl", "uid", "originalName", "name")) {
Object value = map.get(field);
if (value != null && !String.valueOf(value).isBlank()) {
return field + ":" + value;
}
}
}
return String.valueOf(attachment);
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> parseChangeRecords(String value) {
if (Func.isEmpty(value)) {
@@ -297,6 +297,9 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
private void prepareCreateOrUpdate(ProjectApply projectApply) {
if (Func.isEmpty(projectApply.getId())) {
// 新增项目不产生变更记录,避免客户端误传变更字段导致记录回显。
projectApply.setChangeContent(null);
projectApply.setChangeReason(null);
if (Func.isEmpty(projectApply.getApplyNo())) {
projectApply.setApplyNo(nextApplyNo());
}
@@ -941,22 +941,29 @@ public class ReceivablePayableDetailServiceImpl
Map<String, Object> goods, String fallbackName) {
Object actualPath = goods.get("cargoTypePath");
if (matchesCargoTypePath(expectedPath, actualPath)) return true;
String actualCode = firstNotBlank(
goods.get("cargoTypeCode"),
goods.get("secondCargoTypeCode"),
goods.get("firstCargoTypeCode"));
if (!isBlank(expectedCode) && Func.isNotEmpty(actualCode)) {
String expectedCodeText = String.valueOf(expectedCode).trim();
if (expectedCodeText.equals(actualCode)) return true;
if (isFirstLevelCargoType(expectedPath, expectedCodeText)
&& actualCode.startsWith(expectedCodeText)) return true;
}
if (matchesCargoTypeCode(expectedPath, expectedCode, goods)) return true;
if (isBlank(expectedName)) return false;
return matchesCondition(expectedName, stringValue(goods, "cargoType", fallbackName))
|| matchesCondition(expectedName, stringValue(goods, "secondCargoTypeName", ""))
|| matchesCondition(expectedName, stringValue(goods, "firstCargoTypeName", ""));
}
/**
* 一级货物类型使用父级编码匹配二级货物编码。历史货物明细可能同时存在多个编码字段,
* 不能只取第一个非空值,否则二级编码会遮蔽可用于前缀匹配的一级编码。
*/
private boolean matchesCargoTypeCode(Object expectedPath, Object expectedCode,
Map<String, Object> goods) {
if (isBlank(expectedCode)) return false;
String expected = String.valueOf(expectedCode).trim();
boolean firstLevel = isFirstLevelCargoType(expectedPath, expected);
return java.util.stream.Stream.of(goods.get("cargoTypeCode"), goods.get("secondCargoTypeCode"),
goods.get("firstCargoTypeCode"))
.filter(value -> !isBlank(value))
.map(value -> String.valueOf(value).trim())
.anyMatch(actual -> expected.equals(actual) || (firstLevel && actual.startsWith(expected)));
}
private boolean matchesCargoTypePath(Object expectedPath, Object actualPath) {
if (!(expectedPath instanceof Collection<?> expectedValues)
|| !(actualPath instanceof Collection<?> actualValues)
@@ -978,13 +985,6 @@ public class ReceivablePayableDetailServiceImpl
return expectedCode.matches("\\d{2}");
}
private String firstNotBlank(Object... values) {
for (Object value : values) {
if (!isBlank(value)) return String.valueOf(value).trim();
}
return "";
}
private String resolveRegionCode(Long addressId) {
if (addressId == null) return "";
try {
@@ -6521,6 +6521,7 @@ CREATE TABLE "BLADEX"."BLADE_FEE_ITEM" (
"ID" NUMBER(20,0) NOT NULL,
"NAME" NVARCHAR2(50) NOT NULL,
"ENGLISH_NAME" NVARCHAR2(100),
"TAX_RATE" NUMBER(6,2) DEFAULT 0 NOT NULL,
"CREATE_USER" NUMBER(20,0),
"CREATE_DEPT" NUMBER(20,0),
"CREATE_TIME" DATE,
@@ -2565,6 +2565,7 @@ CREATE TABLE "blade_fee_item" (
"fee_category" varchar(50) NOT NULL,
"name" varchar(50) NOT NULL,
"english_name" varchar(100),
"tax_rate" numeric(6,2) NOT NULL DEFAULT 0.00,
"create_user" int8,
"create_dept" int8,
"create_time" timestamp(6),
@@ -1721,6 +1721,7 @@ CREATE TABLE `blade_fee_item` (
`fee_category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用类型',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用项',
`english_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '费用项代码',
`tax_rate` decimal(6,2) NOT NULL DEFAULT '0.00' COMMENT '税率(百分比)',
`remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
`create_user` bigint NULL DEFAULT NULL COMMENT '创建人',
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
@@ -6755,6 +6755,7 @@ CREATE TABLE "BLADE_FEE_ITEM" (
"ID" NUMBER(20,0) NOT NULL,
"NAME" NVARCHAR2(50) NOT NULL,
"ENGLISH_NAME" NVARCHAR2(100),
"TAX_RATE" NUMBER(6,2) DEFAULT 0 NOT NULL,
"CREATE_USER" NUMBER(20,0),
"CREATE_DEPT" NUMBER(20,0),
"CREATE_TIME" DATE,
@@ -2565,6 +2565,7 @@ CREATE TABLE "blade_fee_item" (
"fee_category" varchar(50) NOT NULL,
"name" varchar(50) NOT NULL,
"english_name" varchar(100),
"tax_rate" numeric(6,2) NOT NULL DEFAULT 0.00,
"create_user" int8,
"create_dept" int8,
"create_time" timestamp(6),
@@ -7730,6 +7730,7 @@ CREATE TABLE [dbo].[blade_fee_item] (
[fee_category] nvarchar(50) NOT NULL,
[name] nvarchar(50) NOT NULL,
[english_name] nvarchar(100) NULL,
[tax_rate] decimal(6,2) NOT NULL CONSTRAINT [df_blade_fee_item_tax_rate] DEFAULT (0.00),
[create_user] bigint NULL,
[create_dept] bigint NULL,
[create_time] datetime2(0) NULL,
@@ -28645,6 +28645,7 @@ CREATE TABLE "BLADE_FEE_ITEM" (
"ID" NUMBER(20,0) NOT NULL,
"NAME" NVARCHAR2(50) NOT NULL,
"ENGLISH_NAME" NVARCHAR2(100),
"TAX_RATE" NUMBER(6,2) DEFAULT 0 NOT NULL,
"CREATE_USER" NUMBER(20,0),
"CREATE_DEPT" NUMBER(20,0),
"CREATE_TIME" DATE,
@@ -0,0 +1,5 @@
-- 合同计费方案规则新增税率配置。
-- 税率按规则存储于 billing_plan_json 的 taxRate 属性,无需新增独立表字段。
ALTER TABLE `blade_contract_manage`
MODIFY COLUMN `billing_plan_json` text DEFAULT NULL COMMENT '计费方案JSON(含运输方式、默认方案及规则税率配置)';
@@ -0,0 +1,3 @@
-- 合同变更附件暂存字段,审批通过后归集到其它附件
ALTER TABLE `blade_contract_manage`
ADD COLUMN `change_attachments_json` text DEFAULT NULL COMMENT '待审批变更附件JSON' AFTER `change_reason`;
+1
View File
@@ -7,6 +7,7 @@ CREATE TABLE `blade_fee_item` (
`fee_category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用类型',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用项',
`english_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '费用项代码',
`tax_rate` decimal(6,2) NOT NULL DEFAULT '0.00' COMMENT '税率(百分比)',
`remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
`create_user` bigint NULL DEFAULT NULL COMMENT '创建人',
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
@@ -0,0 +1,5 @@
-- 费用项新增税率字段(百分比)
-- 现有数据以 0.00 初始化,后续新增或修改费用项必须填写 0-100 范围内的税率。
ALTER TABLE `blade_fee_item`
ADD COLUMN `tax_rate` decimal(6,2) NOT NULL DEFAULT '0.00' COMMENT '税率(百分比)' AFTER `english_name`;
@@ -144,12 +144,13 @@ CREATE TABLE `blade_contract_manage` (
`billing_enabled` int(11) DEFAULT '0' COMMENT '计费信息开关',
`contract_file_json` text DEFAULT NULL COMMENT '合同主文件JSON',
`attachments_json` text DEFAULT NULL COMMENT '其它附件JSON',
`billing_plan_json` text DEFAULT NULL COMMENT '计费方案JSON(含运输方式默认方案配置)',
`billing_plan_json` text DEFAULT NULL COMMENT '计费方案JSON(含运输方式默认方案及规则税率配置)',
`settlement_rule_json` text DEFAULT NULL COMMENT '结算生成规则JSON',
`reconciliation_json` text DEFAULT NULL COMMENT '对账配置JSON',
`change_record_json` text DEFAULT NULL COMMENT '变更记录JSON',
`change_content` text DEFAULT NULL COMMENT '变更内容',
`change_reason` varchar(500) DEFAULT NULL COMMENT '变更原因',
`change_attachments_json` text DEFAULT NULL COMMENT '待审批变更附件JSON',
`terminate_reason` varchar(500) DEFAULT NULL COMMENT '终止原因',
`remark` varchar(2000) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE,