✨ 结算单用印类型与项目变更调整材料

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-09 22:53:52 +08:00
co-authored by Cursor
parent d43b6a2566
commit 2ab7b893fd
16 changed files with 214 additions and 5 deletions
@@ -30,6 +30,8 @@ public class FormalSettlementSaveRequest implements Serializable {
private String businessModel;
/** 运输方式(业务字典 transport_type) */
private String transportType;
/** 用印类型(多选,顿号拼接:公章、业务章) */
private String sealTypes;
private LocalDate exchangeRateDate;
private BigDecimal exchangeRate;
private String attachmentsJson;
@@ -61,6 +61,9 @@ public class PreSettlementSaveRequest implements Serializable {
@Schema(description = "运输方式(业务字典 transport_type)")
private String transportType;
@Schema(description = "用印类型(多选,顿号拼接:公章、业务章)")
private String sealTypes;
@Schema(description = "汇率日期")
private LocalDate exchangeRateDate;
@@ -96,6 +96,10 @@ public class FormalSettlement extends TenantEntity {
* 运输方式(业务字典 transport_type)
*/
private String transportType;
/**
* 用印类型(多选,顿号拼接:公章、业务章)
*/
private String sealTypes;
/**
* 本位币,默认 RMB
*/
@@ -98,6 +98,9 @@ public class PreSettlement extends TenantEntity {
@Schema(description = "运输方式(业务字典 transport_type)")
private String transportType;
@Schema(description = "用印类型(多选,顿号拼接:公章、业务章)")
private String sealTypes;
@Schema(description = "本位币")
private String localCurrency;
@@ -188,6 +188,9 @@ public class ProjectChange extends TenantEntity {
@Schema(description = "项目附件JSON")
private String attachmentsJson;
@Schema(description = "调整材料JSON(含原附件快照与调整原因)")
private String adjustAttachmentsJson;
@Schema(description = "审批状态")
private String approvalStatus;
@@ -101,6 +101,7 @@ public class FormalSettlementController extends BladeController {
excel.setSettlementAmount(formatMoney(vo.getSettlementAmount(), vo.getCurrency()));
excel.setBusinessModelName(vo.getBusinessModelName());
excel.setTransportTypeName(vo.getTransportTypeName());
excel.setSealTypes(vo.getSealTypes());
excel.setLocalSettlementAmount(formatMoney(vo.getLocalSettlementAmount(), vo.getLocalCurrency()));
excel.setExchangeRate(vo.getExchangeRate() == null ? "" : vo.getExchangeRate().stripTrailingZeros().toPlainString());
excel.setInvoiceStatusName(invoiceStatusName(vo.getInvoiceStatus(), vo.getSettlementType()));
@@ -252,6 +252,7 @@ public class PreSettlementController extends BladeController {
excel.setSettlementAmount(formatMoney(vo.getSettlementAmount(), vo.getCurrency()));
excel.setBusinessModelName(vo.getBusinessModelName());
excel.setTransportTypeName(vo.getTransportTypeName());
excel.setSealTypes(vo.getSealTypes());
excel.setLocalSettlementAmount(formatMoney(vo.getLocalSettlementAmount(), vo.getLocalCurrency()));
excel.setExchangeRate(vo.getExchangeRate() == null ? "" : vo.getExchangeRate().stripTrailingZeros().toPlainString());
excel.setAdvanceAppliedAmount(formatMoney(vo.getAdvanceAppliedAmount(), vo.getCurrency()));
@@ -43,6 +43,8 @@ public class FormalSettlementExcel implements Serializable {
private String businessModelName;
@ExcelProperty("运输方式")
private String transportTypeName;
@ExcelProperty("用印类型")
private String sealTypes;
@ExcelProperty("本位币结算金额")
private String localSettlementAmount;
@ExcelProperty("结算汇率")
@@ -54,6 +54,8 @@ public class PreSettlementExcel implements Serializable {
private String businessModelName;
@ExcelProperty("运输方式")
private String transportTypeName;
@ExcelProperty("用印类型")
private String sealTypes;
@ExcelProperty("本位币结算金额")
private String localSettlementAmount;
@ExcelProperty("结算汇率")
@@ -651,6 +651,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
if (first == null) copyHeader(contract, settlementType, directDetails.get(0), settlement); else copyHeader(first, settlement);
settlement.setBusinessModel(limit(request.getBusinessModel(), 64));
settlement.setTransportType(limit(request.getTransportType(), 64));
settlement.setSealTypes(TransportBusinessSupport.normalizeSettlementSealTypes(request.getSealTypes()));
settlement.setExchangeRateDate(request.getExchangeRateDate());
settlement.setExchangeRate(request.getExchangeRate() == null ? BigDecimal.ONE : positive(request.getExchangeRate(), "结算汇率"));
BigDecimal sourceAmount = sources.stream().map(PreSettlement::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
@@ -417,6 +417,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
fillContract(settlement, contract);
settlement.setBusinessModel(limitText(request.getBusinessModel(), 64, "业务模式"));
settlement.setTransportType(limitText(request.getTransportType(), 64, "运输方式"));
settlement.setSealTypes(TransportBusinessSupport.normalizeSettlementSealTypes(request.getSealTypes()));
settlement.setExchangeRateDate(request.getExchangeRateDate());
settlement.setExchangeRate(request.getExchangeRate());
settlement.setAttachmentsJson(request.getAttachmentsJson());
@@ -63,10 +63,12 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
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;
import java.util.stream.Collectors;
/**
@@ -345,6 +347,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
/**
* 将变更单可回写字段映射为临时 ProjectApply,供 copyChangeFields 复用。
* 项目材料:以变更单原材料为基础,按调整材料替换/追加后回写原项目档案。
*/
private ProjectApply toProjectApplySnapshot(ProjectChange change) {
ProjectApply snapshot = new ProjectApply();
@@ -378,12 +381,132 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
snapshot.setCustomerJson(change.getCustomerJson());
snapshot.setCarrierJson(change.getCarrierJson());
snapshot.setSituationRemark(change.getSituationRemark());
snapshot.setAttachmentsJson(change.getAttachmentsJson());
snapshot.setAttachmentsJson(applyAdjustAttachments(change.getAttachmentsJson(), change.getAdjustAttachmentsJson()));
snapshot.setChangeContent(change.getChangeContent());
snapshot.setChangeReason(change.getChangeReason());
return snapshot;
}
/**
* 审批通过后应用调整材料:有原附件则替换,无原附件则追加;新材料同步到项目档案【项目材料】。
*/
@SuppressWarnings("unchecked")
private String applyAdjustAttachments(String attachmentsJson, String adjustAttachmentsJson) {
List<Map<String, Object>> materials = parseAttachmentMapList(attachmentsJson);
List<Map<String, Object>> adjustRows = parseAttachmentMapList(adjustAttachmentsJson);
if (adjustRows.isEmpty()) {
return TransportBusinessSupport.trimToNull(attachmentsJson);
}
Set<Integer> replacedIndexes = new HashSet<>();
for (Map<String, Object> adjustRow : adjustRows) {
if (adjustRow == null || adjustRow.isEmpty()) {
continue;
}
Map<String, Object> nextMaterial = toProjectMaterialRow(adjustRow);
if (nextMaterial.isEmpty()) {
continue;
}
int originIndex = findOriginAttachmentIndex(materials, adjustRow, replacedIndexes);
if (originIndex >= 0) {
materials.set(originIndex, nextMaterial);
replacedIndexes.add(originIndex);
} else {
materials.add(nextMaterial);
}
}
return materials.isEmpty() ? null : JsonUtil.toJson(materials);
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> parseAttachmentMapList(String value) {
if (Func.isEmpty(value)) {
return new ArrayList<>();
}
try {
Object parsed = JsonUtil.parse(value, List.class);
if (!(parsed instanceof List<?> list)) {
return new ArrayList<>();
}
List<Map<String, Object>> rows = new ArrayList<>();
for (Object item : list) {
if (item instanceof Map<?, ?> map) {
Map<String, Object> row = new LinkedHashMap<>();
map.forEach((key, val) -> row.put(String.valueOf(key), val));
rows.add(row);
}
}
return rows;
} catch (Exception ignored) {
return new ArrayList<>();
}
}
private Map<String, Object> toProjectMaterialRow(Map<String, Object> adjustRow) {
Map<String, Object> material = new LinkedHashMap<>(adjustRow);
material.remove("adjustReason");
material.remove("originAttachment");
material.remove("originKey");
return material;
}
@SuppressWarnings("unchecked")
private int findOriginAttachmentIndex(List<Map<String, Object>> materials, Map<String, Object> adjustRow,
Set<Integer> replacedIndexes) {
Object originObj = adjustRow.get("originAttachment");
Map<String, Object> origin = null;
if (originObj instanceof Map<?, ?> map) {
origin = new LinkedHashMap<>();
Map<String, Object> finalOrigin = origin;
map.forEach((key, val) -> finalOrigin.put(String.valueOf(key), val));
}
String originKey = String.valueOf(adjustRow.getOrDefault("originKey", "")).trim();
for (int index = 0; index < materials.size(); index++) {
if (replacedIndexes.contains(index)) {
continue;
}
Map<String, Object> current = materials.get(index);
if (Func.isNotEmpty(originKey) && originKey.equals(attachmentIdentity(current))) {
return index;
}
if (origin != null && attachmentIdentity(current).equals(attachmentIdentity(origin))) {
return index;
}
}
return -1;
}
private String attachmentIdentity(Map<String, Object> row) {
if (row == null || row.isEmpty()) {
return "";
}
String uid = String.valueOf(row.getOrDefault("uid", "")).trim();
if (Func.isNotEmpty(uid) && !"null".equals(uid)) {
return "uid:" + uid;
}
for (String field : List.of("url", "link", "fileUrl", "downloadUrl", "src", "domain")) {
String value = String.valueOf(row.getOrDefault(field, "")).trim();
if (Func.isNotEmpty(value) && !"null".equals(value)) {
return "url:" + value;
}
}
String fileType = String.valueOf(row.getOrDefault("fileType", "")).trim();
String name = firstAttachmentName(row);
if (Func.isNotEmpty(fileType) || Func.isNotEmpty(name)) {
return "name:" + fileType + "|" + name;
}
return "";
}
private String firstAttachmentName(Map<String, Object> row) {
for (String field : List.of("originalName", "name", "fileName")) {
String value = String.valueOf(row.getOrDefault(field, "")).trim();
if (Func.isNotEmpty(value) && !"null".equals(value)) {
return value;
}
}
return "";
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeDraft(String ids) {
@@ -126,6 +126,7 @@ public class ProjectChangeServiceImpl extends BaseServiceImpl<ProjectChangeMappe
projectChange.setChangeDate(LocalDate.now());
projectChange.setChangeContent(null);
projectChange.setChangeReason(null);
projectChange.setAdjustAttachmentsJson(null);
projectChange.setBeforeDataJson(JsonUtil.toJson(buildSnapshot(projectApply)));
projectChange.setApprovalStatus(STATUS_DRAFT);
projectChange.setCurrentNode("草稿");
@@ -350,6 +351,7 @@ public class ProjectChangeServiceImpl extends BaseServiceImpl<ProjectChangeMappe
target.setCarrierJson(source.getCarrierJson());
target.setSituationRemark(source.getSituationRemark());
target.setAttachmentsJson(source.getAttachmentsJson());
target.setAdjustAttachmentsJson(source.getAdjustAttachmentsJson());
if (source.getReceivableDays() != null && source.getReceivableDays() == -1) {
target.setReceivableDays(null);
}
@@ -34,6 +34,7 @@ import org.springblade.system.cache.RoleDataScopeCache;
import org.springblade.system.cache.SysCache;
import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.system.pojo.enums.RoleDataScopeType;
import org.springblade.system.pojo.vo.RoleDataScopeVO;
import org.springblade.transport.mapper.ProjectApplyMapper;
import org.springblade.transport.pojo.entity.ProjectApply;
@@ -61,6 +62,8 @@ public final class TransportBusinessSupport {
public static final String DATA_SOURCE_MANUAL = "手工创建";
public static final String DATA_SOURCE_PLAN_DISPATCH = "计划调度";
public static final String DATA_SOURCE_EXTERNAL = "外部系统";
/** 结算单用印类型固定选项(展示顺序固定) */
public static final List<String> SETTLEMENT_SEAL_TYPE_OPTIONS = List.of("公章", "业务章");
/** 站点名(省+市+区县)列宽:blade_waybill.departure_name / arrival_name 为 varchar(100) */
public static final int REGION_NAME_MAX_LENGTH = 100;
@@ -134,7 +137,8 @@ public final class TransportBusinessSupport {
}
/**
* 解析指定菜单下的角色数据权限(超管视为全部组织),并与用户「数据管理权限」组织取并集。
* 解析指定菜单下的角色数据权限(超管视为全部组织)。
* 全部组织、本人创建以角色权限为准;所在部门及下级按用户数据权限组织。
*/
public static RoleDataScopeVO resolveRoleDataScope(String menuCode) {
if (isAdministrator()) {
@@ -166,7 +170,8 @@ public final class TransportBusinessSupport {
}
/**
* 解析最宽角色数据权限,并与用户「数据管理权限」组织取并集。
* 解析最宽角色数据权限。
* 全部组织、本人创建以角色权限为准;所在部门及下级按用户数据权限组织。
*/
public static RoleDataScopeVO resolveWidestDataScope() {
if (isAdministrator()) {
@@ -177,9 +182,12 @@ public final class TransportBusinessSupport {
}
/**
* 角色组织权限与用户数据管理组织取并集。
* 按角色数据权限类型决定是否采用用户数据权限组织。
* <p>
* 全部组织 / 本人创建 不因用户管理组织改变;其余类型将用户管理组织(含下级)并入 deptIds。
* 全部组织、本人创建以角色权限为准,不按用户数据权限收窄。
* 所在部门及下级改为用户「数据管理权限」组织(含下级);同时存在自定义组织时取并集。
* 自定义组织仍与用户数据权限组织取并集。
* 所在部门及下级且用户未配置数据权限组织时,返回不可命中的组织,避免回落到所属组织或全部数据。
*/
private static RoleDataScopeVO mergeUserManageDepts(RoleDataScopeVO scope) {
if (scope == null || scope.isAllDept() || scope.isOwnCreate()) {
@@ -187,6 +195,19 @@ public final class TransportBusinessSupport {
}
Long userId = AuthUtil.getUserId();
List<Long> manageDeptIds = UserCache.getManageDeptIds(userId);
if (RoleDataScopeType.DEPT_AND_CHILD.matches(scope.getScopeType())) {
LinkedHashSet<Long> deptIds = new LinkedHashSet<>();
if (Func.isNotEmpty(scope.getDeptIds())) {
deptIds.addAll(scope.getDeptIds());
}
if (Func.isNotEmpty(manageDeptIds)) {
deptIds.addAll(manageDeptIds);
}
if (deptIds.isEmpty()) {
return RoleDataScopeVO.ofDepts(RoleDataScopeType.DEPT_AND_CHILD.getCode(), List.of(-1L));
}
return RoleDataScopeVO.ofDepts(RoleDataScopeType.DEPT_AND_CHILD.getCode(), List.copyOf(deptIds));
}
if (Func.isEmpty(manageDeptIds)) {
return scope;
}
@@ -495,6 +516,37 @@ public final class TransportBusinessSupport {
return trimValue.isEmpty() ? null : trimValue;
}
/**
* 归一化结算单用印类型:仅允许「公章」「业务章」,多选用顿号拼接,并按固定顺序去重。
*
* @param value 原始用印类型(支持顿号、逗号、空白分隔)
* @return 归一化后的用印类型,未选择时返回 null
*/
public static String normalizeSettlementSealTypes(String value) {
String source = trimToNull(value);
if (Func.isEmpty(source)) {
return null;
}
String[] parts = source.split("[、,,\\s]+");
LinkedHashSet<String> selected = new LinkedHashSet<>();
for (String part : parts) {
String item = trimToNull(part);
if (Func.isEmpty(item)) {
continue;
}
if (!SETTLEMENT_SEAL_TYPE_OPTIONS.contains(item)) {
throw new ServiceException("用印类型仅支持:公章、业务章");
}
selected.add(item);
}
if (selected.isEmpty()) {
return null;
}
return SETTLEMENT_SEAL_TYPE_OPTIONS.stream()
.filter(selected::contains)
.collect(Collectors.joining("、"));
}
/**
* 归一化运输计划数据来源,保证列表只出现约定的三种来源。
*
@@ -0,0 +1,3 @@
-- 项目变更:调整材料(记录原附件,审批通过后替换并同步到原项目档案项目材料)
ALTER TABLE `blade_project_change`
ADD COLUMN `adjust_attachments_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '调整材料JSON(含原附件快照与调整原因)' AFTER `attachments_json`;
@@ -0,0 +1,6 @@
-- 预付结算单、正式结算单(付款/收款):用印类型(多选,顿号拼接存储:公章、业务章)
ALTER TABLE `blade_pre_settlement`
ADD COLUMN `seal_types` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用印类型(多选,顿号拼接:公章、业务章)' AFTER `transport_type`;
ALTER TABLE `blade_formal_settlement`
ADD COLUMN `seal_types` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用印类型(多选,顿号拼接:公章、业务章)' AFTER `transport_type`;