> rows;
+
+ @Schema(description = "分批追加")
+ private Boolean append;
+
+ @Schema(description = "列表筛选:项目名称")
+ private String projectNameLike;
+
+ @Schema(description = "列表筛选:创建人")
+ private Long createUser;
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementImportVoidRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementImportVoidRequest.java
new file mode 100644
index 00000000..bb330c14
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementImportVoidRequest.java
@@ -0,0 +1,51 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 导入结算单作废请求
+ *
+ * @author Chill
+ */
+@Data
+@Schema(description = "导入结算单作废请求")
+public class SettlementImportVoidRequest implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "批次主键")
+ private Long id;
+
+ @Schema(description = "作废原因")
+ private String reason;
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementProjectAdvanceWriteOffRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementProjectAdvanceWriteOffRequest.java
new file mode 100644
index 00000000..be281de0
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementProjectAdvanceWriteOffRequest.java
@@ -0,0 +1,53 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 结算单项目预付核销保存行
+ *
+ * @author Chill
+ */
+@Data
+@Schema(description = "结算单项目预付核销保存行")
+public class SettlementProjectAdvanceWriteOffRequest implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "项目预付付款申请ID")
+ private Long paymentApplicationId;
+
+ @Schema(description = "本次核销金额")
+ private BigDecimal writeOffAmount;
+
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
index fa780bbf..5ea8bec1 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
@@ -161,4 +161,13 @@ public class CustomerArchive extends TenantEntity {
@Schema(description = "审核通过时间")
private LocalDateTime approvedTime;
+ @Schema(description = "金蝶协同状态:none未推送/exist金蝶已有/synced已推送/failed推送失败")
+ private String kingdeeStatus;
+
+ @Schema(description = "金蝶推送失败原因")
+ private String kingdeeFailReason;
+
+ @Schema(description = "金蝶推送时间")
+ private LocalDateTime kingdeeSyncTime;
+
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java
index 107b7a82..0a03304b 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java
@@ -21,6 +21,8 @@ import java.time.LocalDateTime;
/**
* 正式结算单实体类
+ *
+ * 结算单主表,明细快照见 {@link FormalSettlementDetail},合计费用见 {@link FormalSettlementSummaryFee}
*
* @author Chill
*/
@@ -30,38 +32,144 @@ import java.time.LocalDateTime;
@Schema(description = "正式结算单")
public class FormalSettlement extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 正式结算单号(同租户唯一)
+ */
private String formalSettlementNo;
+ /**
+ * 来源类型:预结算合并/应收应付/混合来源
+ */
private String sourceType;
+ /**
+ * 结算类型:receivable-应收 payable-应付
+ */
private String settlementType;
+ /**
+ * 所属项目ID
+ */
private Long projectId;
+ /**
+ * 所属项目名称
+ */
private String projectName;
+ /**
+ * 所属组织ID
+ */
private Long deptId;
+ /**
+ * 所属组织名称
+ */
private String deptName;
+ /**
+ * 结算合同ID
+ */
private Long contractId;
+ /**
+ * 结算合同编号
+ */
private String contractNo;
+ /**
+ * 结算合同名称
+ */
private String contractName;
+ /**
+ * 付款方名称
+ */
private String payerName;
+ /**
+ * 收款方名称
+ */
private String payeeName;
+ /**
+ * 结算币种,默认 RMB
+ */
private String currency;
+ /**
+ * 结算金额(结算币种,含全部合计费用)
+ */
private BigDecimal settlementAmount;
+ /**
+ * 本位币,默认 RMB
+ */
private String localCurrency;
+ /**
+ * 本位币结算金额 = 结算金额 × 结算汇率
+ */
private BigDecimal localSettlementAmount;
+ /**
+ * 已申请付款金额,含来源预结算的预付
+ */
private BigDecimal appliedPaymentAmount;
+ /**
+ * 已收/已付金额合计,含来源预结算的预付
+ */
private BigDecimal paidAmount;
+ /**
+ * 剩余可付金额 = 结算金额 - 已收已付合计,最小为 0
+ */
private BigDecimal remainingPayableAmount;
+ /**
+ * 已开票/已收票金额,取开票申请与收票登记的分摊合计
+ */
private BigDecimal invoiceAmount;
+ /**
+ * 汇率日期
+ */
private LocalDate exchangeRateDate;
+ /**
+ * 结算汇率
+ */
private BigDecimal exchangeRate;
+ /**
+ * 开票状态(应收为开票、应付为收票):unreceived-未开票 partial-部分开票 completed-已开票
+ */
private String invoiceStatus;
+ /**
+ * 收付款状态:unpaid-未收付款 partial-部分收付款 paid-已收付款
+ */
private String paymentStatus;
+ /**
+ * 审核状态:draft-草稿 reviewing-审批中 approved-审批通过 returned-已驳回 voided-已作废
+ */
private String approvalStatus;
+ /**
+ * 当前审批节点,如「草稿」「财务审核」「审批通过」
+ */
private String currentNode;
+ /**
+ * 当前处理人姓名
+ */
private String currentProcessor;
+ /**
+ * 金蝶单据编号
+ */
private String kingdeeBillNo;
+ /**
+ * 金蝶单据id(星瀚内部主键,A902 附件上传定位用)
+ */
+ private String kingdeeBillId;
+ /**
+ * 金蝶同步状态:unsynced-未同步 synced-已同步 failed-同步失败
+ */
private String kingdeeSyncStatus;
+ /**
+ * 附件JSON
+ */
private String attachmentsJson;
+ /**
+ * 备注
+ */
private String remark;
+ /**
+ * 审核通过时间
+ */
private LocalDateTime approvedTime;
+ /**
+ * 同步金蝶时间
+ */
private LocalDateTime syncedTime;
+ /**
+ * 作废原因,驳回时记录驳回原因
+ */
private String voidReason;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java
index bf3cda88..899dffe1 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java
@@ -27,37 +27,136 @@ import java.time.LocalDateTime;
@TableName("blade_formal_settlement_detail")
public class FormalSettlementDetail extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 正式结算单ID
+ */
private Long formalSettlementId;
+ /**
+ * 来源预结算单ID
+ */
private Long sourcePreSettlementId;
+ /**
+ * 来源预结算明细ID
+ */
private Long sourcePreSettlementDetailId;
+ /**
+ * 来源应收应付明细ID
+ */
private Long sourceDetailId;
+ /**
+ * 行号
+ */
private Integer lineNo;
+ /**
+ * 单据编号
+ */
private String documentNo;
+ /**
+ * 运单ID
+ */
private Long waybillId;
+ /**
+ * 运单号
+ */
private String waybillNo;
+ /**
+ * 车牌号
+ */
private String vehicleNo;
+ /**
+ * 发货地址
+ */
private String departureAddress;
+ /**
+ * 收货地址
+ */
private String arrivalAddress;
+ /**
+ * 发货联系人
+ */
private String departureContact;
+ /**
+ * 发货联系方式
+ */
private String departurePhone;
+ /**
+ * 收货联系人
+ */
private String arrivalContact;
+ /**
+ * 收货联系方式
+ */
private String arrivalPhone;
+ /**
+ * 实际发货时间
+ */
private LocalDateTime actualDepartureTime;
+ /**
+ * 实际完成时间
+ */
private LocalDateTime actualCompletionTime;
+ /**
+ * 运输类型
+ */
private String transportType;
+ /**
+ * 货物名称
+ */
private String cargoName;
+ /**
+ * 货物类型
+ */
private String cargoType;
+ /**
+ * 运输总量
+ */
private BigDecimal transportQuantity;
+ /**
+ * 数量单位
+ */
private String quantityUnit;
+ /**
+ * 里程
+ */
private BigDecimal mileage;
+ /**
+ * 批次号
+ */
private String batchNo;
+ /**
+ * 运输单价
+ */
private BigDecimal unitPrice;
+ /**
+ * 运费
+ */
private BigDecimal freightAmount;
+ /**
+ * 费用项JSON
+ */
private String feeItemsJson;
+ /**
+ * 原金额
+ */
private BigDecimal originalAmount;
+ /**
+ * 调整金额
+ */
private BigDecimal adjustAmount;
+ /**
+ * 结算金额(含税)
+ */
private BigDecimal settlementAmountTax;
+ /**
+ * 结算金额(不含税)
+ */
private BigDecimal settlementAmountNoTax;
+ /**
+ * 币种
+ */
private String currency;
+ /**
+ * 备注
+ */
private String remark;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java
index b7e5892a..c4e62e26 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java
@@ -26,20 +26,68 @@ import java.math.BigDecimal;
@TableName("blade_formal_settlement_detail_fee")
public class FormalSettlementDetailFee extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 正式结算明细ID
+ */
private Long formalSettlementDetailId;
+ /**
+ * 源费用行ID
+ */
private Long sourceFeeId;
+ /**
+ * 行号
+ */
private String lineNo;
+ /**
+ * 货物名称
+ */
private String cargoName;
+ /**
+ * 货物类型
+ */
private String cargoType;
+ /**
+ * 运输量
+ */
private BigDecimal transportQuantity;
+ /**
+ * 数量单位
+ */
private String quantityUnit;
+ /**
+ * 里程
+ */
private BigDecimal mileage;
+ /**
+ * 运输单价
+ */
private BigDecimal unitPrice;
+ /**
+ * 运费
+ */
private BigDecimal freightAmount;
+ /**
+ * 费用项JSON
+ */
private String feeItemsJson;
+ /**
+ * 原金额
+ */
private BigDecimal originalAmount;
+ /**
+ * 调整金额
+ */
private BigDecimal adjustAmount;
+ /**
+ * 结算金额(含税)
+ */
private BigDecimal settlementAmountTax;
+ /**
+ * 结算金额(不含税)
+ */
private BigDecimal settlementAmountNoTax;
+ /**
+ * 备注
+ */
private String remark;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java
index 962f7ad4..fdfb94eb 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java
@@ -26,12 +26,20 @@ import java.math.BigDecimal;
@TableName("blade_formal_settlement_payment")
public class FormalSettlementPayment extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /** 正式结算单id */
private Long formalSettlementId;
+ /** 付款单号(租户内唯一) */
private String paymentNo;
+ /** 付款类型(final:结算尾款) */
private String paymentType;
+ /** 申请付款金额 */
private BigDecimal appliedAmount;
+ /** 已付金额 */
private BigDecimal paidAmount;
+ /** 单据状态(reviewing:审核中,approved:审核通过,returned:已退回,voided:已作废,paid:已付款) */
private String billStatus;
+ /** 金蝶(星瀚)单据编号 */
private String kingdeeBillNo;
+ /** 备注 */
private String remark;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java
index 9292f68e..acfd797f 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java
@@ -28,13 +28,40 @@ import java.math.BigDecimal;
@Schema(description = "正式结算合计费用")
public class FormalSettlementSummaryFee extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 正式结算单ID
+ */
private Long formalSettlementId;
+ /**
+ * 行号
+ */
private Integer lineNo;
+ /**
+ * 费用类型
+ */
private String feeType;
+ /**
+ * 费用项
+ */
private String feeItem;
+ /**
+ * 原金额
+ */
private BigDecimal originalAmount;
+ /**
+ * 调整金额
+ */
private BigDecimal adjustAmount;
+ /**
+ * 结算金额
+ */
private BigDecimal settlementAmount;
+ /**
+ * 备注
+ */
private String remark;
+ /**
+ * 是否手工添加
+ */
private Integer manualFlag;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java
index bf03a3ce..2b89b282 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java
@@ -47,37 +47,140 @@ import java.time.LocalDateTime;
@Schema(description = "开票申请")
public class InvoiceApplication extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 开票申请单号(同租户唯一,A303-1 billno 幂等候选键)
+ */
private String applicationNo;
+ /**
+ * 所属项目ID(结算单快照带出)
+ */
private Long projectId;
+ /**
+ * 所属项目名称(结算单快照带出)
+ */
private String projectName;
+ /**
+ * 申请部门(所属组织)ID
+ */
private Long deptId;
+ /**
+ * 申请部门(所属组织)名称
+ */
private String deptName;
+ /**
+ * 开票单位名称(销方=结算单收款方快照,金蝶 salername 取数来源)
+ */
private String issuerName;
+ /**
+ * 受票方客商档案ID
+ */
private Long receiverCustomerId;
+ /**
+ * 受票方单位名称(客商开票信息企业全称快照,金蝶 buyername)
+ */
private String receiverName;
+ /**
+ * 发票类型:electronic_normal-数电普票 / electronic_special-数电专票
+ */
private String invoiceType;
+ /**
+ * 可开票金额(关联结算单剩余可开票额度合计)
+ */
private BigDecimal availableInvoiceAmount;
+ /**
+ * 本次开票金额(商品行价税合计,金蝶 totalamount)
+ */
private BigDecimal invoiceAmount;
+ /**
+ * 申请日期(提交审批时置当日)
+ */
private LocalDate applicationDate;
+ /**
+ * 申请人姓名
+ */
private String applicantName;
+ /**
+ * 承办部门ID(结算单所属组织快照)
+ */
private Long undertakingDeptId;
+ /**
+ * 承办部门名称(结算单所属组织快照)
+ */
private String undertakingDeptName;
+ /**
+ * 部门邮箱(发票推送邮箱,多个以分号分隔,最多3个,金蝶 buyeremail)
+ */
private String departmentEmails;
+ /**
+ * 受票方客商开票信息ID(blade_customer_invoice_info)
+ */
private Long receiverInvoiceInfoId;
+ /**
+ * 受票方纳税人识别号(开票信息快照,金蝶 buyertaxno)
+ */
private String taxpayerNo;
+ /**
+ * 受票方开户行(开票信息快照)
+ */
private String bankName;
+ /**
+ * 受票方开户账号(开票信息快照)
+ */
private String bankAccount;
+ /**
+ * 受票方注册地址(开票信息快照)
+ */
private String registeredAddress;
+ /**
+ * 受票方联系人
+ */
private String contactName;
+ /**
+ * 受票方联系电话
+ */
private String contactPhone;
+ /**
+ * 受票方邮箱
+ */
private String email;
+ /**
+ * 审批状态:draft-草稿 reviewing-审批中 approved-审批通过 returned-已驳回 voided-已作废
+ */
private String approvalStatus;
+ /**
+ * 当前审批节点,如「草稿」「开票审核」「审批通过」
+ */
private String currentNode;
+ /**
+ * 当前处理人姓名
+ */
private String currentProcessor;
+ /**
+ * 金蝶开票申请单编号(A303-1 返回,A303-5 查询 serialNo)
+ */
private String kingdeeBillNo;
+ /**
+ * 金蝶开票申请单id(星瀚内部主键,A902 附件上传定位用)
+ */
+ private String kingdeeBillId;
+ /**
+ * 金蝶同步状态:unsynced-未同步 synced-已同步 failed-同步失败 invoiced-开票完成(终态)
+ */
private String kingdeeStatus;
+ /**
+ * 同步金蝶时间
+ */
private LocalDateTime syncedTime;
+ /**
+ * 附件JSON
+ */
private String attachmentsJson;
+ /**
+ * 备注
+ */
private String remark;
+ /**
+ * 作废原因,驳回时记录驳回原因
+ */
private String voidReason;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationDetail.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationDetail.java
index 44104935..2d43fc59 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationDetail.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationDetail.java
@@ -44,25 +44,88 @@ import java.time.LocalDateTime;
@TableName("blade_invoice_application_detail")
public class InvoiceApplicationDetail extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 开票申请ID
+ */
private Long invoiceApplicationId;
+ /**
+ * 正式结算单ID
+ */
private Long formalSettlementId;
+ /**
+ * 来源正式结算明细ID
+ */
private Long formalSettlementDetailId;
+ /**
+ * 行号
+ */
private Integer lineNo;
+ /**
+ * 结算明细单据号(快照)
+ */
private String documentNo;
+ /**
+ * 运单号(快照)
+ */
private String waybillNo;
+ /**
+ * 车牌号(快照)
+ */
private String vehicleNo;
+ /**
+ * 起运地(快照)
+ */
private String departureAddress;
+ /**
+ * 到达地(快照)
+ */
private String arrivalAddress;
+ /**
+ * 实际发运时间(快照)
+ */
private LocalDateTime actualDepartureTime;
+ /**
+ * 实际完成时间(快照)
+ */
private LocalDateTime actualCompletionTime;
+ /**
+ * 运输方式(快照,字典 transport_type)
+ */
private String transportType;
+ /**
+ * 货物名称(快照)
+ */
private String cargoName;
+ /**
+ * 货物类型(快照)
+ */
private String cargoType;
+ /**
+ * 运输数量(快照)
+ */
private BigDecimal transportQuantity;
+ /**
+ * 数量单位(快照)
+ */
private String quantityUnit;
+ /**
+ * 里程(快照)
+ */
private BigDecimal mileage;
+ /**
+ * 批次号(快照)
+ */
private String batchNo;
+ /**
+ * 运费金额(快照)
+ */
private BigDecimal freightAmount;
+ /**
+ * 费用项明细JSON(快照)
+ */
private String feeItemsJson;
+ /**
+ * 结算金额(含税,快照)
+ */
private BigDecimal settlementAmountTax;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationInvoice.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationInvoice.java
new file mode 100644
index 00000000..8e145070
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationInvoice.java
@@ -0,0 +1,80 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
+ * Copyright of this software remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springblade.core.tenant.mp.TenantEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 开票申请金蝶发票结果实体类
+ *
+ * 定时任务经 A303-5 开票申请发票查询回写的发票明细(发票号唯一键幂等落库),
+ * 发票文件地址(板式文件/数电PDF/XML/预览图)独立落库供详情页查看下载,
+ * 原文 JSON 整体留档便于联调与追溯
+ *
+ * @author Chill
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("blade_invoice_application_invoice")
+@Schema(description = "开票申请金蝶发票结果")
+public class InvoiceApplicationInvoice extends TenantEntity {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "开票申请ID")
+ private Long invoiceApplicationId;
+
+ @Schema(description = "金蝶开票申请单编号(A303-5 serialNo)")
+ private String serialNo;
+
+ @Schema(description = "发票号码")
+ private String invoiceNo;
+
+ @Schema(description = "开票日期")
+ private LocalDateTime invoiceDate;
+
+ @Schema(description = "价税合计")
+ private BigDecimal invoiceAmount;
+
+ @Schema(description = "板式文件下载地址(数电票为OFD,航信百望税盘普通电票为PDF,电子专票/UKey电票为OFD)")
+ private String invoiceFileUrl;
+
+ @Schema(description = "PDF图片预览地址")
+ private String invoiceImageUrl;
+
+ @Schema(description = "数电PDF下载地址(发票云5.0.025版本返回)")
+ private String invoicePdfFileUrl;
+
+ @Schema(description = "数电XML下载地址(发票云5.0.025版本返回)")
+ private String invoiceXmlFileUrl;
+
+ @Schema(description = "A303-5 返回原文JSON留档")
+ private String rawJson;
+
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java
index 30ded9f6..60f04c39 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java
@@ -43,18 +43,64 @@ import java.math.BigDecimal;
@TableName("blade_invoice_application_line")
public class InvoiceApplicationLine extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 开票申请ID
+ */
private Long invoiceApplicationId;
+ /**
+ * 所属发票张次ID
+ */
private Long invoiceSheetId;
+ /**
+ * 行号(张内序号)
+ */
private Integer lineNo;
+ /**
+ * 商品和服务分类名称(开票项目主数据选择,金蝶 goodssimplename)
+ */
private String goodsCategory;
+ /**
+ * 货物或服务简称(开票项目主数据选择,金蝶 goodsname)
+ */
private String goodsName;
+ /**
+ * 税收分类编码(选择开票内容时快照,推送金蝶 A303-1 分录直接取该值)
+ */
+ private String taxClassificationCode;
+ /**
+ * 单位
+ */
private String unit;
+ /**
+ * 数量
+ */
private BigDecimal quantity;
+ /**
+ * 不含税单价
+ */
private BigDecimal unitPriceNoTax;
+ /**
+ * 不含税金额
+ */
private BigDecimal amountNoTax;
+ /**
+ * 含税金额(历史兼容字段,与含税合计同值)
+ */
private BigDecimal amountWithTax;
+ /**
+ * 税率(0-100 百分比值,推送金蝶时转小数文本)
+ */
private BigDecimal taxRate;
+ /**
+ * 税额
+ */
private BigDecimal taxAmount;
+ /**
+ * 价税合计(含税合计,金蝶 taxamount)
+ */
private BigDecimal totalAmount;
+ /**
+ * 行备注
+ */
private String remark;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationRecord.java
index 8a411bd8..01c9e9b0 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationRecord.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationRecord.java
@@ -42,12 +42,36 @@ import java.io.Serial;
@TableName("blade_invoice_application_record")
public class InvoiceApplicationRecord extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 开票申请ID
+ */
private Long invoiceApplicationId;
+ /**
+ * 操作类型:create/save/submit/approve/return/void/sync 等
+ */
private String actionType;
+ /**
+ * 操作名称(展示用,如「提交审批」「同步金蝶」)
+ */
private String actionName;
+ /**
+ * 操作前审批状态
+ */
private String fromStatus;
+ /**
+ * 操作后审批状态
+ */
private String toStatus;
+ /**
+ * 操作人姓名
+ */
private String operatorName;
+ /**
+ * 原因说明(驳回/作废原因等)
+ */
private String reason;
+ /**
+ * 金蝶单据编号(同步类操作记录)
+ */
private String kingdeeBillNo;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSettlement.java
index 57a03396..ac6ba4ca 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSettlement.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSettlement.java
@@ -43,10 +43,28 @@ import java.math.BigDecimal;
@TableName("blade_invoice_application_settlement")
public class InvoiceApplicationSettlement extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 开票申请ID
+ */
private Long invoiceApplicationId;
+ /**
+ * 正式结算单ID
+ */
private Long formalSettlementId;
+ /**
+ * 正式结算单号(关联时快照)
+ */
private String formalSettlementNo;
+ /**
+ * 结算金额(关联时快照)
+ */
private BigDecimal settlementAmount;
+ /**
+ * 可开票金额(关联时快照)
+ */
private BigDecimal availableInvoiceAmount;
+ /**
+ * 本申请分摊开票金额(结算单开票状态按此汇总)
+ */
private BigDecimal allocatedInvoiceAmount;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSheet.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSheet.java
index 8550946d..bee703e4 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSheet.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationSheet.java
@@ -43,7 +43,20 @@ import java.math.BigDecimal;
@TableName("blade_invoice_application_sheet")
public class InvoiceApplicationSheet extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 开票申请ID
+ */
private Long invoiceApplicationId;
+ /**
+ * 发票张序号(从 1 开始)
+ */
private Integer sheetNo;
+ /**
+ * 本张开票金额(张内商品行价税合计)
+ */
private BigDecimal invoiceAmount;
+ /**
+ * 金蝶开票申请单编号(本张发票对应,A303-1 返回,A303-5 查询 serialNo)
+ */
+ private String kingdeeBillNo;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java
index 67859e1c..9023e07e 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceipt.java
@@ -36,6 +36,7 @@ import org.springblade.core.tenant.mp.TenantEntity;
import java.io.Serial;
import java.math.BigDecimal;
import java.time.LocalDate;
+import java.time.LocalDateTime;
/**
* 收票登记实体类
@@ -48,36 +49,135 @@ import java.time.LocalDate;
@Schema(description = "收票登记")
public class InvoiceReceipt extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 关联金蝶票据池镜像主键(blade_kingdee_invoice_pool.id)
+ */
@JsonSerialize(using = ToStringSerializer.class)
private Long kingdeeInvoicePoolId;
+ /**
+ * 发票号码(同租户唯一,一张发票仅可登记一次)
+ */
private String invoiceNo;
+ /**
+ * 开票日期
+ */
private LocalDate invoiceDate;
+ /**
+ * 发票类型(如「数电发票(增值税专用发票)」)
+ */
private String invoiceType;
+ /**
+ * 税率(百分数,13 表示 13%)
+ */
private BigDecimal taxRate;
+ /**
+ * 开票金额
+ */
private BigDecimal invoiceAmount;
+ /**
+ * 税额
+ */
private BigDecimal taxAmount;
+ /**
+ * 受票单位(购方)
+ */
private String receiverName;
+ /**
+ * 开票单位(销方)
+ */
private String issuerName;
+ /**
+ * 所属项目id
+ */
@JsonSerialize(using = ToStringSerializer.class)
private Long projectId;
+ /**
+ * 所属项目名称
+ */
private String projectName;
+ /**
+ * 所属组织id
+ */
@JsonSerialize(using = ToStringSerializer.class)
private Long deptId;
+ /**
+ * 所属组织名称
+ */
private String deptName;
+ /**
+ * 付款方
+ */
private String payerName;
+ /**
+ * 收款方
+ */
private String payeeName;
+ /**
+ * 开票单位开户银行
+ */
private String bankName;
+ /**
+ * 开票单位银行账号
+ */
private String bankAccount;
+ /**
+ * 开票单位开户网点
+ */
private String issuingBank;
+ /**
+ * 联系电话
+ */
private String phone;
+ /**
+ * 客户邮箱(分号分隔,最多 3 个)
+ */
private String customerEmails;
+ /**
+ * 部门邮箱(分号分隔,最多 3 个)
+ */
private String departmentEmails;
+ /**
+ * 审批状态(draft 草稿 / reviewing 审批中 / approved 审批通过 / returned 已驳回 / voided 已作废)
+ */
private String approvalStatus;
+ /**
+ * 当前审批节点
+ */
private String currentNode;
+ /**
+ * 当前处理人
+ */
private String currentProcessor;
- private String kingdeeBillNo;
- private String kingdeeStatus;
+ /**
+ * 金蝶收票单单据编号(A303-2 批量保存生成/幂等预检命中回填)
+ */
+ private String kingdeeReceivedInvoiceNo;
+ /**
+ * 金蝶收票单id(星瀚内部主键,A902 附件上传定位用)
+ */
+ private String kingdeeReceivedInvoiceId;
+ /**
+ * 收票单推送状态(unsynced 未推送 / pushed 已保存收票单 / assigned 已指定应付 / failed 推送失败)
+ */
+ private String kingdeePushStatus;
+ /**
+ * 收票单推送失败原因(金蝶侧删除/红冲/查验不通过等异常提示人工)
+ */
+ private String kingdeePushMsg;
+ /**
+ * 最近一次收票单推送/回写时间
+ */
+ private LocalDateTime kingdeePushTime;
+ /**
+ * 附件信息JSON
+ */
private String attachmentsJson;
+ /**
+ * 备注(最多 200 字)
+ */
private String remark;
+ /**
+ * 作废原因
+ */
private String voidReason;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeAttachmentSync.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeAttachmentSync.java
new file mode 100644
index 00000000..c8c0377d
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeAttachmentSync.java
@@ -0,0 +1,84 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springblade.core.tenant.mp.TenantEntity;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * 金蝶附件同步实体类(A902 附件上传到指定单据,单附件行级状态)
+ *
+ * @author Chill
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("blade_kingdee_attachment_sync")
+@Schema(description = "金蝶附件同步")
+public class KingdeeAttachmentSync extends TenantEntity {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 业务单据类型(pay_application/formal_settlement_ap/formal_settlement_ar/invoice_receipt/invoice_application)
+ */
+ private String bizType;
+ /**
+ * 业务单据id
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long bizId;
+ /**
+ * 附件幂等键(用户附件=附件url;发票文件=A801-2 的 path)
+ */
+ private String attachmentKey;
+ /**
+ * 附件名称(展示/排查用)
+ */
+ private String attachmentName;
+ /**
+ * 金蝶侧文件路径(A902 返回 data.path)
+ */
+ private String kingdeePath;
+ /**
+ * 同步状态:1 成功 / 0 失败
+ */
+ private Integer syncStatus;
+ /**
+ * 失败原因(超限/下载失败/金蝶报错)
+ */
+ private String failReason;
+ /**
+ * 最近上传时间
+ */
+ private LocalDateTime syncTime;
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeBillLock.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeBillLock.java
new file mode 100644
index 00000000..1817ce2e
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeBillLock.java
@@ -0,0 +1,87 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springblade.core.tenant.mp.TenantEntity;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * 金蝶单据锁定记录实体类
+ *
+ * 一行 = 一张金蝶目标单:推送成功后 lockBusiData 锁定(FAILED→LOCKED),金蝶经 unLockNotifyBusiData
+ * 回调解锁(→UNLOCKED,DELETE 时废除源单并还原关联数据);同时承担当前锁状态(编辑守卫/回调定位)
+ * 与锁定补偿扫表(FAILED+try_count)、锁定/解锁留痕
+ *
+ * @author Chill
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("blade_kingdee_bill_lock")
+@Schema(description = "金蝶单据锁定记录")
+public class KingdeeBillLock extends TenantEntity {
+ @Serial private static final long serialVersionUID = 1L;
+ /**
+ * WLTMS 单据类型(SETTLEMENT/INVOICE_APPLY,预留 PAY_APPLY/RECEIVED_INVOICE)
+ */
+ private String sourceBillType;
+ /**
+ * WLTMS 业务主键
+ */
+ private Long sourceBillId;
+ /**
+ * WLTMS 单号(正式结算单号/开票申请单号)
+ */
+ private String sourceBillNo;
+ /**
+ * 金蝶单据类型(ApFin_other_BT_S/arfin_other_BT_S/cdm_paybill_bank_elect_BT_S 等固定值)
+ */
+ private String targetBillType;
+ /**
+ * 金蝶单据编号(即业务单 kingdeeBillNo,开票申请按发票张次逐张一行)
+ */
+ private String targetBillNo;
+ /**
+ * 锁状态:failed-锁定失败(补偿重试对象)/locked-金蝶锁定中/unlocked-已解锁(终态)
+ */
+ private String lockStatus;
+ /**
+ * 最近动作来源操作类型:SAVE-单据新增,DELETE-单据删除(DELETE 回调触发废除还原)
+ */
+ private String operationType;
+ /**
+ * 锁定重试次数(补偿任务用,超上限停试)
+ */
+ private Integer tryCount;
+ /**
+ * 锁定时间
+ */
+ private LocalDateTime lockTime;
+ /**
+ * 解锁时间
+ */
+ private LocalDateTime unlockTime;
+ /**
+ * 推单发起人(锁定操作人)
+ */
+ private String lockOperator;
+ /**
+ * 回调操作人(金蝶侧操作人)
+ */
+ private String notifyOperator;
+ /**
+ * 锁定失败原因
+ */
+ private String failReason;
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java
index 261110c3..79eb12e7 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java
@@ -47,22 +47,108 @@ import java.time.LocalDateTime;
@Schema(description = "金蝶进项发票票据池镜像")
public class KingdeeInvoicePool extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /**
+ * 发票号码(同租户唯一,A801-1 invoiceNo,镜像 upsert 匹配键)
+ */
private String invoiceNo;
+ /**
+ * 金蝶发票流水号(票池唯一标识,A801-1 serialNo)
+ */
+ private String serialNo;
+ /**
+ * 开票日期
+ */
private LocalDate invoiceDate;
+ /**
+ * 发票类型(票易通编码转名称后落库,如「数电发票(增值税专用发票)」)
+ */
private String invoiceType;
+ /**
+ * 票易通原始发票类型编码(A801-1 invoiceType,如 26/27,A303-2 invoicetypef7_number 取数)
+ */
+ private String invoiceTypeCode;
+ /**
+ * 税率(百分数,13 表示 13%)
+ */
private BigDecimal taxRate;
+ /**
+ * 开票金额(A801-1 价税合计 totalAmount)
+ */
private BigDecimal invoiceAmount;
+ /**
+ * 税额
+ */
private BigDecimal taxAmount;
+ /**
+ * 受票单位(购方名称,A801-1 buyerName)
+ */
private String receiverName;
+ /**
+ * 购方税号(A801-1 buyerTaxNo)
+ */
+ private String buyerTaxNo;
+ /**
+ * 开票单位(销方名称,A801-1 salerName)
+ */
private String issuerName;
+ /**
+ * 销方税号(A801-1 salerTaxNo)
+ */
+ private String salerTaxNo;
+ /**
+ * 开票单位开户银行
+ */
private String bankName;
+ /**
+ * 开票单位银行账号
+ */
private String bankAccount;
+ /**
+ * 开票单位开户网点
+ */
private String issuingBank;
+ /**
+ * 联系电话
+ */
private String phone;
+ /**
+ * 客户邮箱(分号分隔,最多 3 个)
+ */
private String customerEmails;
+ /**
+ * 部门邮箱(分号分隔,最多 3 个)
+ */
private String departmentEmails;
+ /**
+ * 金蝶单据号(票池发票取发票流水号 serialNo)
+ */
private String kingdeeBillNo;
+ /**
+ * 金蝶同步状态(unsynced 未同步 / synced 已同步 / failed 同步失败)
+ */
private String kingdeeStatus;
+ /**
+ * 查验状态(A801-1 checkStatus,0 未查验 / 1 通过 / 2 不通过)
+ */
+ private String checkStatus;
+ /**
+ * 附件信息JSON(结构 {"source":"piaozone","path":"A801-2下载path","name":"文件名"})
+ */
private String attachmentsJson;
+ /**
+ * A801-1 发票明细行JSON(BfiInvoicePoolItemVO 列表,票池存档;收票单分录已改与应付单同源结算费用行)
+ */
+ private String itemsJson;
+ /**
+ * 票池数据更新时间(A801-1 billCreateTime,镜像排序字段)
+ */
private LocalDateTime sourceUpdatedTime;
+ /**
+ * 剩余可用金额(A801-1 availableAmount,金蝶可用税额口径,选票本地过滤用:>0 才可选)
+ */
+ private BigDecimal availableAmount;
+ /**
+ * 金额使用状态(A801-1 amountUseStatus,0 未使用可选 / 2 全部使用;选票本地过滤用)
+ */
+ private String amountUseStatus;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java
index 79b75c1b..533394ae 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java
@@ -26,6 +26,8 @@
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -49,16 +51,105 @@ public class KingdeeReceiptFlow extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
+ /**
+ * 金蝶认领公告id(A301-2 返回 id,认领推送 A301-3 必填)
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long billId;
+ /**
+ * 金蝶付款人id(A301-2 公告对应往来单位 id,认领推送 A301-3 用)
+ */
+ private String payerId;
+ /**
+ * 公告认领状态(A301-2 claimstatus:0未认领/1已认领)
+ */
+ private String kingdeeClaimStatus;
+ /**
+ * 我已认领(A301-2 claimme)
+ */
+ private Boolean claimMe;
+ /**
+ * 未认领金额(A301-2 unclaimamount)
+ */
+ private BigDecimal unclaimedAmount;
+ /**
+ * 手续费(A301-2 fee)
+ */
+ private BigDecimal fee;
+ /**
+ * 收款人(A301-2 recpayee)
+ */
+ private String payeeName;
+ /**
+ * 收款通知单号(A301-2 billno)
+ */
private String receiptNoticeNo;
+ /**
+ * 付款人名称(A301-2 recviewpayer)
+ */
private String payerName;
+ /**
+ * 收款金额(A301-2 reamount)
+ */
private BigDecimal receiptAmount;
+ /**
+ * 对方单位名称(A301-2 oppunit)
+ */
private String counterpartyName;
+ /**
+ * 对方银行账号(A301-2 oppbanknumber)
+ */
private String counterpartyAccount;
+ /**
+ * 对方开户行(A301-2 oppbank)
+ */
private String counterpartyBank;
+ /**
+ * 摘要(A301-2 description)
+ */
private String summary;
+ /**
+ * 交易日期(A301-2 tradetime)
+ */
private LocalDateTime transactionTime;
+ /**
+ * 明细流水号(A301-2 detailid,回退 tradedetailno / bankdetailno,幂等键)
+ */
private String detailSerialNo;
+ /**
+ * 已认领金额(本地认领驱动,初始为0)
+ */
private BigDecimal claimedAmount;
+ /**
+ * 认领状态(unclaimed未认领/partial部分认领/claimed认领完成,累计认领与公告未认领金额对表)
+ */
private String claimStatus;
+ /**
+ * 推送状态(none未推送/pushed已推送/failed推送失败;已推送为终态闸门,不允许二次推送)
+ */
+ private String pushStatus;
+ /**
+ * 最近一次推送失败原因(成功后清空)
+ */
+ private String pushFailReason;
+ /**
+ * 首次推送金蝶成功时间
+ */
+ private LocalDateTime pushedTime;
+ /**
+ * 金蝶推送来源单据编码(分摊结算单号下划线拼接,推送时生成落库;分摊不变则重试编码不变,A301-1 回写匹配键)
+ */
+ private String awrpSrcCode;
+ /**
+ * 金蝶收款单号(推送响应回填 / A301-1 回写推进)
+ */
+ private String kingdeeBillNo;
+ /**
+ * 金蝶回写状态(none未推送/pushed已推送/approved审核通过/received已收款/failed失败)
+ */
+ private String kingdeeBillStatus;
+ /**
+ * 来源更新时间(取交易日期,同步时刷新)
+ */
private LocalDateTime sourceUpdatedTime;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
index 1d6d033c..45ab1f4c 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
@@ -2,18 +2,24 @@
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*
- * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
*
- * 1. This software is for development use only under a valid license from BladeX.
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
*
- * 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
*
- * 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
- * Copyright of this software remains with BladeX.
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
*
- * Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
*
- * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
*
* Author: Chill Zhuang (bladejava@qq.com)
*/
@@ -34,43 +40,84 @@ import java.time.LocalDate;
@TableName("blade_payment_application")
public class PaymentApplication extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
+ /** 付款申请单号 */
private String paymentNo;
+ /** 付款类型:project_advance 项目预付 / progress_advance 进度预付 / settlement_payment 结算付款 */
private String paymentType;
+ /** 正式结算单id */
private Long settlementId;
+ /** 正式结算单号 */
private String settlementNo;
+ /** 预结算单id */
private Long preSettlementId;
+ /** 预结算单号 */
private String preSettlementNo;
+ /** 项目id */
private Long projectId;
+ /** 项目名称 */
private String projectName;
+ /** 所属组织id */
private Long deptId;
+ /** 所属组织名称 */
private String deptName;
+ /** 合同id */
private Long contractId;
+ /** 合同编号 */
private String contractNo;
+ /** 合同名称 */
private String contractName;
+ /** 付款方 */
private String payerName;
+ /** 收款方 */
private String payeeName;
+ /** 结算金额 */
private BigDecimal settlementAmount;
+ /** 应付金额 */
private BigDecimal payableAmount;
+ /** 来源单据类型:项目预付 / 正式结算单 / 预结算单 */
private String billType;
+ /** 付款比例(%) */
private BigDecimal paymentRatio;
+ /** 申请付款金额 */
private BigDecimal appliedAmount;
+ /** 付款方式,业务字典 pay_method */
private String paymentMethod;
+ /** 汇票台账id(票据付款时关联) */
private Long billLedgerId;
+ /** 票据号码 */
private String billNo;
+ /** 收款账户id */
private Long receiptAccountId;
+ /** 收款账户名称 */
private String receiptAccountName;
+ /** 收款银行名称 */
private String bankName;
+ /** 收款账号 */
private String bankAccount;
+ /** 申请人 */
private String applicantName;
+ /** 申请日期 */
private LocalDate applyDate;
+ /** 发票匹配状态:unmatched 未匹配 / matched 已匹配 */
private String invoiceStatus;
+ /** 匹配发票金额 */
private BigDecimal matchedInvoiceAmount;
+ /** 已付款金额 */
private BigDecimal paidAmount;
+ /** 审批状态:draft 草稿 / reviewing 审批中 / approved 审批通过 / returned 已驳回 / voided 已作废 */
private String approvalStatus;
+ /** 当前审批节点 */
private String currentNode;
+ /** 当前处理人 */
private String currentProcessor;
+ /** 金蝶单据号(A103-2 查询回填的星瀚单据编号 billno,如 FKSQ-xxxx) */
private String kingdeeBillNo;
+ /** 金蝶单据id(A103-1/A103-3 推送返回的星瀚内部主键,附件上传等使用) */
+ private String kingdeeBillId;
+ /** 金蝶同步状态:unsynced 未生成 / synced 已生成 / paying 付款中 / paid 已付款 / closed 已关闭 / failed 生成失败 */
private String kingdeeStatus;
+ /** 附件信息JSON */
private String attachmentsJson;
+ /** 备注 */
private String remark;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java
index 5526da8e..eb85e2c4 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectApply.java
@@ -70,10 +70,10 @@ public class ProjectApply extends TenantEntity {
@Schema(description = "业务部门")
private String businessDeptName;
- @Schema(description = "承办部门ID")
- private Long undertakeDeptId;
+ @Schema(description = "平台公司ID,多个逗号分隔")
+ private String undertakeDeptId;
- @Schema(description = "承办部门")
+ @Schema(description = "平台公司,多个顿号分隔")
private String undertakeDeptName;
@Schema(description = "项目由来")
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java
index fc7f7862..2294d0a9 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaim.java
@@ -65,7 +65,13 @@ public class ReceiptClaim extends TenantEntity {
private String attachmentsJson;
private String remark;
private String claimStatus;
+ /**
+ * 金蝶收款单号(冗余展示:推送成功/A301-1 回写时由流水级联刷新)
+ */
private String kingdeeBillNo;
+ /**
+ * 金蝶回写状态(冗余展示:与流水 kingdee_bill_status 同步刷新)
+ */
private String kingdeeBillStatus;
@JsonSerialize(using = ToStringSerializer.class)
private Long voidedBy;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementImportBatch.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementImportBatch.java
new file mode 100644
index 00000000..88a3bcd4
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementImportBatch.java
@@ -0,0 +1,115 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springblade.core.tenant.mp.TenantEntity;
+
+import java.io.Serial;
+
+/**
+ * 导入结算单批次
+ *
+ * @author Chill
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("blade_settlement_import_batch")
+@Schema(description = "导入结算单批次")
+public class SettlementImportBatch extends TenantEntity {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "批次号")
+ private String batchNo;
+
+ @Schema(description = "项目ID")
+ private Long projectId;
+
+ @Schema(description = "项目名称")
+ private String projectName;
+
+ @Schema(description = "结算类型 receivable/payable")
+ private String settlementType;
+
+ @Schema(description = "结算单类型 formal/pre")
+ private String settlementBillType;
+
+ @Schema(description = "客户合同ID")
+ private Long customerContractId;
+
+ @Schema(description = "客户合同名称")
+ private String customerContractName;
+
+ @Schema(description = "客户名称")
+ private String customerName;
+
+ @Schema(description = "承运商合同ID")
+ private Long carrierContractId;
+
+ @Schema(description = "承运商合同名称")
+ private String carrierContractName;
+
+ @Schema(description = "承运商名称")
+ private String carrierName;
+
+ @Schema(description = "运单模式 new")
+ private String waybillMode;
+
+ @Schema(description = "导入状态 draft/completed/voided")
+ private String importStatus;
+
+ @Schema(description = "运单数量")
+ private Integer waybillCount;
+
+ @Schema(description = "明细数量")
+ private Integer detailCount;
+
+ @Schema(description = "正式结算单ID")
+ private Long formalSettlementId;
+
+ @Schema(description = "正式结算单号")
+ private String formalSettlementNo;
+
+ @Schema(description = "预结算单ID")
+ private Long preSettlementId;
+
+ @Schema(description = "预结算单号")
+ private String preSettlementNo;
+
+ @Schema(description = "动态费用列名JSON")
+ private String feeItemNamesJson;
+
+ @Schema(description = "备注")
+ private String remark;
+
+ @Schema(description = "关联运单导入批次ID")
+ private Long waybillImportBatchId;
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementProjectAdvanceWriteOff.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementProjectAdvanceWriteOff.java
new file mode 100644
index 00000000..57be9c22
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementProjectAdvanceWriteOff.java
@@ -0,0 +1,90 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springblade.core.tenant.mp.TenantEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 结算单项目预付核销实体类
+ *
+ * @author Chill
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("blade_settlement_project_advance_write_off")
+@Schema(description = "结算单项目预付核销")
+public class SettlementProjectAdvanceWriteOff extends TenantEntity {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "结算单类型:pre/formal")
+ private String settlementType;
+
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "预结算/正式结算单ID")
+ private Long settlementId;
+
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "项目预付付款申请ID")
+ private Long paymentApplicationId;
+
+ @Schema(description = "预付单号")
+ private String paymentNo;
+
+ @Schema(description = "项目预付金额")
+ private BigDecimal projectAdvanceAmount;
+
+ @Schema(description = "本次核销金额")
+ private BigDecimal writeOffAmount;
+
+ @Schema(description = "金蝶预付单号")
+ private String kingdeeBillNo;
+
+ @TableField(exist = false)
+ @Schema(description = "可核销金额(动态计算)")
+ private BigDecimal availableWriteOffAmount;
+
+ @TableField(exist = false)
+ @Schema(description = "预付单创建人姓名")
+ private String createUserName;
+
+ @TableField(exist = false)
+ @Schema(description = "预付单创建时间(展示用)")
+ private Date paymentCreateTime;
+
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java
index d79f1502..a3157095 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java
@@ -249,6 +249,9 @@ public class Waybill extends TenantEntity {
@Schema(description = "导入批次ID")
private Long importBatchId;
+ @Schema(description = "导入结算单批次ID")
+ private Long settlementImportBatchId;
+
@Schema(description = "关联单号")
private String relationNo;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java
index 1e507376..dc889541 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java
@@ -9,6 +9,7 @@
package org.springblade.transport.pojo.vo;
import com.baomidou.mybatisplus.annotation.TableField;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.transport.pojo.entity.FormalSettlement;
@@ -18,10 +19,13 @@ import org.springblade.transport.pojo.entity.FormalSettlementInvoice;
import org.springblade.transport.pojo.entity.FormalSettlementSource;
import org.springblade.transport.pojo.entity.FormalSettlementPayment;
import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
+import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
+import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
import java.time.LocalDate;
+import java.time.LocalDateTime;
import java.util.List;
/**
@@ -31,7 +35,7 @@ import java.util.List;
*/
@Data
@EqualsAndHashCode(callSuper = true)
-public class FormalSettlementVO extends FormalSettlement {
+public class FormalSettlementVO extends FormalSettlement implements KingdeeUnlockMark {
@Serial private static final long serialVersionUID = 1L;
@TableField(exist = false) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate createStartDate;
@TableField(exist = false) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate createEndDate;
@@ -39,6 +43,21 @@ public class FormalSettlementVO extends FormalSettlement {
@TableField(exist = false) private String ids;
@TableField(exist = false) private String approvalStatusName;
@TableField(exist = false) private String settlementTypeName;
+ @TableField(exist = false) private Integer attachmentCount;
+ @TableField(exist = false) private String attachmentSyncStatus;
+ @TableField(exist = false) private String attachmentSyncStatusName;
+ @TableField(exist = false) @Schema(description = "金蝶锁状态:locked-锁定中/failed-锁定失败;unlocked为终态不返回,null=无锁或已解锁")
+ private String kingdeeLockStatus;
+ @TableField(exist = false) @Schema(description = "金蝶锁失败原因(仅锁定失败时有值)")
+ private String kingdeeLockFailReason;
+ @TableField(exist = false) @Schema(description = "金蝶锁重试次数(达上限5后不再自动补偿,需重推同步金蝶)")
+ private Integer kingdeeTryCount;
+ @TableField(exist = false) @Schema(description = "金蝶解锁时间(非空代表该单曾被金蝶解锁/删单)")
+ private LocalDateTime kingdeeUnlockTime;
+ @TableField(exist = false) @Schema(description = "金蝶侧解锁操作人(回调报文 operator)")
+ private String kingdeeUnlockOperator;
+ @TableField(exist = false) @Schema(description = "解锁动作名称:金蝶删单/金蝶锁单")
+ private String kingdeeUnlockOperationName;
@TableField(exist = false) private String preSettlementNos;
@TableField(exist = false) private String preSettlementNo;
@TableField(exist = false) private List sources;
@@ -46,6 +65,10 @@ public class FormalSettlementVO extends FormalSettlement {
@TableField(exist = false) private List summaryFees;
@TableField(exist = false) private List payments;
@TableField(exist = false) private List invoices;
+ @TableField(exist = false) @Schema(description = "项目预付核销")
+ private List projectAdvanceWriteOffs;
+ @TableField(exist = false) @Schema(description = "开票申请经金蝶回写的发票结果(含文件地址,只读展示)")
+ private List issuedInvoices;
@TableField(exist = false) private List paymentApplications;
@TableField(exist = false) private List adjustments;
@TableField(exist = false) private List changeRecords;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java
index 664d02b0..d195bbc5 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java
@@ -30,10 +30,12 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.transport.pojo.entity.InvoiceApplication;
import org.springblade.transport.pojo.entity.InvoiceApplicationDetail;
+import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
import org.springblade.transport.pojo.entity.InvoiceApplicationRecord;
import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
import java.io.Serial;
+import java.time.LocalDateTime;
import java.util.List;
/**
@@ -43,14 +45,27 @@ import java.util.List;
*/
@Data
@EqualsAndHashCode(callSuper = true)
-public class InvoiceApplicationVO extends InvoiceApplication {
+public class InvoiceApplicationVO extends InvoiceApplication implements KingdeeUnlockMark {
@Serial private static final long serialVersionUID = 1L;
@TableField(exist = false) private String settlementNos;
@TableField(exist = false) private String approvalStatusName;
@TableField(exist = false) private String kingdeeStatusName;
+ @TableField(exist = false) private Integer attachmentCount;
+ @TableField(exist = false) private String attachmentSyncStatus;
+ @TableField(exist = false) private String attachmentSyncStatusName;
+ @TableField(exist = false) private String kingdeeLockStatus;
+ @TableField(exist = false) private String kingdeeLockFailReason;
+ @TableField(exist = false) private Integer kingdeeTryCount;
+ /** 金蝶解锁终态标识(unlocked 锁记录回填):解锁时间非空即代表该单曾被金蝶解锁/删单 */
+ @TableField(exist = false) private LocalDateTime kingdeeUnlockTime;
+ /** 金蝶侧解锁操作人(回调报文 operator) */
+ @TableField(exist = false) private String kingdeeUnlockOperator;
+ /** 解锁动作名称:金蝶删单/金蝶锁单 */
+ @TableField(exist = false) private String kingdeeUnlockOperationName;
@TableField(exist = false) private String createUserName;
@TableField(exist = false) private List settlements;
@TableField(exist = false) private List sheets;
@TableField(exist = false) private List details;
@TableField(exist = false) private List records;
+ @TableField(exist = false) private List invoices;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceReceiptVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceReceiptVO.java
index 8be4dcf2..2f344ff1 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceReceiptVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceReceiptVO.java
@@ -48,7 +48,10 @@ public class InvoiceReceiptVO extends InvoiceReceipt {
@Serial private static final long serialVersionUID = 1L;
@TableField(exist = false) private String settlementNos;
@TableField(exist = false) private String approvalStatusName;
- @TableField(exist = false) private String kingdeeStatusName;
+ @TableField(exist = false) private String kingdeePushStatusName;
+ @TableField(exist = false) private Integer attachmentCount;
+ @TableField(exist = false) private String attachmentSyncStatus;
+ @TableField(exist = false) private String attachmentSyncStatusName;
@TableField(exist = false) private String createUserName;
@TableField(exist = false) private String updateUserName;
@TableField(exist = false) private List settlements;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeInvoicePoolVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeInvoicePoolVO.java
new file mode 100644
index 00000000..001bae66
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeInvoicePoolVO.java
@@ -0,0 +1,157 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 金蝶票据池镜像统一视图(含本地认领/登记标记;认领口径:可匹配与默认认领金额=价税合计)
+ *
+ * @author Chill
+ */
+@Data
+@Schema(description = "金蝶票据池镜像统一视图")
+public class KingdeeInvoicePoolVO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /** 镜像主键,仅已入镜像(已登记)的票非空 */
+ @Schema(description = "镜像主键")
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long id;
+
+ /** 发票号码 */
+ @Schema(description = "发票号码")
+ private String invoiceNo;
+
+ /** 金蝶发票流水号(票池唯一标识) */
+ @Schema(description = "金蝶发票流水号")
+ private String serialNo;
+
+ /** 开票日期 yyyy-MM-dd */
+ @Schema(description = "开票日期")
+ private String invoiceDate;
+
+ /** 发票类型名称(镜像落库时已由编码转名称) */
+ @Schema(description = "发票类型名称")
+ private String invoiceType;
+
+ /** 税率百分数,如 13 */
+ @Schema(description = "税率百分数")
+ private BigDecimal taxRate;
+
+ /** 价税合计 */
+ @Schema(description = "发票金额(价税合计)")
+ private BigDecimal invoiceAmount;
+
+ /** 税额 */
+ @Schema(description = "税额")
+ private BigDecimal taxAmount;
+
+ /** 受票单位(购方名称) */
+ @Schema(description = "受票单位")
+ private String receiverName;
+
+ /** 购方税号 */
+ @Schema(description = "购方税号")
+ private String buyerTaxNo;
+
+ /** 开票单位(销方名称) */
+ @Schema(description = "开票单位")
+ private String issuerName;
+
+ /** 销方税号 */
+ @Schema(description = "销方税号")
+ private String salerTaxNo;
+
+ /** 销方开户行名称 */
+ @Schema(description = "销方开户行名称")
+ private String bankName;
+
+ /** 销方银行账号 */
+ @Schema(description = "销方银行账号")
+ private String bankAccount;
+
+ /** 销方开户行及账号 */
+ @Schema(description = "开户行及账号")
+ private String issuingBank;
+
+ /** 销方电话 */
+ @Schema(description = "电话")
+ private String phone;
+
+ /** 客户邮箱 JSON 串 */
+ @Schema(description = "客户邮箱JSON")
+ private String customerEmails;
+
+ /** 部门邮箱 JSON 串 */
+ @Schema(description = "部门邮箱JSON")
+ private String departmentEmails;
+
+ /** 金蝶流水号(登记侧展示为金蝶单据号) */
+ @Schema(description = "金蝶单据号")
+ private String kingdeeBillNo;
+
+ /** 同步状态 unsynced/synced/failed */
+ @Schema(description = "金蝶同步状态")
+ private String kingdeeStatus;
+
+ /** 查验状态 */
+ @Schema(description = "查验状态")
+ private String checkStatus;
+
+ /** 附件 JSON(票易通下载 path) */
+ @Schema(description = "附件信息JSON")
+ private String attachmentsJson;
+
+ /** 可认领金额,口径同发票金额 */
+ @Schema(description = "可认领金额")
+ private BigDecimal availableInvoiceAmount;
+
+ /** 剩余可用金额(金蝶可用税额口径,与可认领金额的价税合计口径不同) */
+ @Schema(description = "剩余可用金额")
+ private BigDecimal availableTaxAmount;
+
+ /** 默认认领金额,口径同发票金额 */
+ @Schema(description = "默认认领金额")
+ private BigDecimal matchedAmount;
+
+ /** 是否已被正式结算单认领(实时查询返回行的本地装饰标记,仅展示不作过滤) */
+ @Schema(description = "是否已认领")
+ private Boolean claimed;
+
+ /** 是否已存在非作废收票登记(实时查询返回行的本地装饰标记,仅展示不作过滤) */
+ @Schema(description = "是否已登记")
+ private Boolean registered;
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeUnlockMark.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeUnlockMark.java
new file mode 100644
index 00000000..ca15b000
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeUnlockMark.java
@@ -0,0 +1,36 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.vo;
+
+import java.time.LocalDateTime;
+
+/**
+ * 金蝶解锁终态标识展示契约:需在页面展示「金蝶删单/解锁」标识的 VO 实现之,
+ * 由 {@code KingdeeBillLockService#fillUnlockMarks} 统一回填(列表/详情共用)
+ *
+ * @author Chill
+ */
+public interface KingdeeUnlockMark {
+
+ /**
+ * 金蝶解锁时间(unlocked 锁记录回填),非空即代表该单曾被金蝶解锁/删单
+ */
+ void setKingdeeUnlockTime(LocalDateTime kingdeeUnlockTime);
+
+ /**
+ * 金蝶侧解锁操作人(回调报文 operator)
+ */
+ void setKingdeeUnlockOperator(String kingdeeUnlockOperator);
+
+ /**
+ * 解锁动作名称:金蝶删单/金蝶锁单
+ */
+ void setKingdeeUnlockOperationName(String kingdeeUnlockOperationName);
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java
index 6e6c183a..8c079128 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java
@@ -29,12 +29,13 @@ import org.springblade.transport.pojo.entity.PaymentApplicationSettlement;
import java.io.Serial;
import java.time.LocalDate;
+import java.time.LocalDateTime;
import java.util.List;
/** 付款申请视图。 @author Chill */
@Data
@EqualsAndHashCode(callSuper = true)
-public class PaymentApplicationVO extends PaymentApplication {
+public class PaymentApplicationVO extends PaymentApplication implements KingdeeUnlockMark {
@Serial private static final long serialVersionUID = 1L;
@TableField(exist = false) private LocalDate applyStartDate;
@TableField(exist = false) private LocalDate applyEndDate;
@@ -43,6 +44,18 @@ public class PaymentApplicationVO extends PaymentApplication {
@TableField(exist = false) private String paymentTypeName;
@TableField(exist = false) private String approvalStatusName;
@TableField(exist = false) private String kingdeeStatusName;
+ @TableField(exist = false) private Integer attachmentCount;
+ @TableField(exist = false) private String attachmentSyncStatus;
+ @TableField(exist = false) private String attachmentSyncStatusName;
+ @TableField(exist = false) private String kingdeeLockStatus;
+ @TableField(exist = false) private String kingdeeLockFailReason;
+ @TableField(exist = false) private Integer kingdeeTryCount;
+ /** 金蝶解锁终态标识(unlocked 锁记录回填):解锁时间非空即代表该单曾被金蝶解锁/删单,供页面展示明显标识 */
+ @TableField(exist = false) private LocalDateTime kingdeeUnlockTime;
+ /** 金蝶侧解锁操作人(回调报文 operator) */
+ @TableField(exist = false) private String kingdeeUnlockOperator;
+ /** 解锁动作名称:金蝶删单/金蝶锁单 */
+ @TableField(exist = false) private String kingdeeUnlockOperationName;
@TableField(exist = false) private List invoices;
@TableField(exist = false) private List paymentRecords;
@TableField(exist = false) private List settlements;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java
index f4573a63..36955339 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java
@@ -35,6 +35,7 @@ import org.springblade.transport.pojo.entity.PreSettlementChangeRecord;
import org.springblade.transport.pojo.entity.PreSettlementDetail;
import org.springblade.transport.pojo.entity.PreSettlementDetailFee;
import org.springblade.transport.pojo.entity.PreSettlementSummaryFee;
+import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
@@ -100,6 +101,10 @@ public class PreSettlementVO extends PreSettlement {
@Schema(description = "预付信息")
private List advances;
+ @TableField(exist = false)
+ @Schema(description = "项目预付核销")
+ private List projectAdvanceWriteOffs;
+
@TableField(exist = false)
@Schema(description = "变更记录")
private List changeRecords;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectAdvanceWriteOffOptionVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectAdvanceWriteOffOptionVO.java
new file mode 100644
index 00000000..42d85b13
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectAdvanceWriteOffOptionVO.java
@@ -0,0 +1,72 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.vo;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 可选项目预付核销选项
+ *
+ * @author Chill
+ */
+@Data
+@Schema(description = "可选项目预付核销选项")
+public class ProjectAdvanceWriteOffOptionVO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @JsonSerialize(using = ToStringSerializer.class)
+ @Schema(description = "付款申请ID")
+ private Long paymentApplicationId;
+
+ @Schema(description = "预付单号")
+ private String paymentNo;
+
+ @Schema(description = "项目预付金额")
+ private BigDecimal projectAdvanceAmount;
+
+ @Schema(description = "可核销金额")
+ private BigDecimal availableWriteOffAmount;
+
+ @Schema(description = "金蝶预付单号")
+ private String kingdeeBillNo;
+
+ @Schema(description = "创建人姓名")
+ private String createUserName;
+
+ @Schema(description = "创建时间")
+ private Date createTime;
+
+}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptClaimRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptClaimRecordVO.java
index 9802b661..400af8ae 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptClaimRecordVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptClaimRecordVO.java
@@ -76,6 +76,11 @@ public class ReceiptClaimRecordVO extends ReceiptClaim {
private String claimStatusName;
@TableField(exist = false)
private String kingdeeBillStatusName;
+ /**
+ * 关联流水推送状态(none未推送/pushed已推送/failed推送失败,查询条件)
+ */
+ @TableField(exist = false)
+ private String pushStatus;
@TableField(exist = false)
private List settlements;
@TableField(exist = false)
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java
index b282c745..f32a9491 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ReceiptFlowVO.java
@@ -56,6 +56,12 @@ public class ReceiptFlowVO extends KingdeeReceiptFlow {
@TableField(exist = false)
private String claimStatusName;
@TableField(exist = false)
+ private String kingdeeClaimStatusName;
+ @TableField(exist = false)
+ private String pushStatusName;
+ @TableField(exist = false)
+ private String kingdeeBillStatusName;
+ @TableField(exist = false)
private BigDecimal remainingAmount;
@TableField(exist = false)
private String createUserName;
@@ -73,6 +79,16 @@ public class ReceiptFlowVO extends KingdeeReceiptFlow {
@TableField(exist = false)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime transactionEndTime;
+ /**
+ * 收款金额区间-最小值(查询条件)
+ */
+ @TableField(exist = false)
+ private BigDecimal receiptAmountStart;
+ /**
+ * 收款金额区间-最大值(查询条件)
+ */
+ @TableField(exist = false)
+ private BigDecimal receiptAmountEnd;
@TableField(exist = false)
private List claimRecords = new ArrayList<>();
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementImportBatchVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementImportBatchVO.java
new file mode 100644
index 00000000..3f856a62
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementImportBatchVO.java
@@ -0,0 +1,69 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springblade.transport.pojo.entity.SettlementImportBatch;
+
+import java.io.Serial;
+import java.util.List;
+
+/**
+ * 导入结算单批次视图
+ *
+ * @author Chill
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "导入结算单批次视图")
+public class SettlementImportBatchVO extends SettlementImportBatch {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "创建人姓名")
+ private String createUserName;
+
+ @Schema(description = "更新人姓名")
+ private String updateUserName;
+
+ @Schema(description = "导入状态显示名")
+ private String importStatusName;
+
+ @Schema(description = "结算类型显示名")
+ private String settlementTypeName;
+
+ @Schema(description = "结算单类型显示名")
+ private String settlementBillTypeName;
+
+ @Schema(description = "运单模式显示名")
+ private String waybillModeName;
+
+ @Schema(description = "动态费用列名")
+ private List feeItemNames;
+}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java b/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java
index 64632b22..07748388 100644
--- a/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java
@@ -3,6 +3,7 @@ package org.springblade.file.config;
import com.aliyun.oss.OSSClientBuilder;
import com.obs.services.ObsClient;
import io.minio.MinioClient;
+import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.file.service.IFileService;
@@ -10,6 +11,7 @@ import org.springblade.file.service.impl.LocalFileServiceImpl;
import org.springblade.file.service.impl.MinioFileServiceImpl;
import org.springblade.file.service.impl.OssFileServiceImpl;
import org.springblade.file.service.impl.ObsFileServiceImpl;
+import org.springblade.file.util.MinioClientFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -20,6 +22,7 @@ import org.springframework.core.env.Environment;
* @author bfhuange
* @date 2024/10/21
*/
+@Slf4j
@Configuration
public class FileConfig {
@@ -63,10 +66,22 @@ public class FileConfig {
private IFileService buildMinioFileService(FileStorageProperties.StoreProperties properties) {
validateStoreProperties(STORAGE_MINIO, properties);
- MinioClient minioClient = MinioClient.builder()
- .endpoint(properties.getEndpoint())
- .credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
- .build();
+ String endpointOrigin = MinioClientFactory.resolveOrigin(properties.getEndpoint());
+ String endpointPathPrefix = MinioClientFactory.resolvePathPrefix(properties.getEndpoint());
+ if (StringUtil.isNotBlank(endpointPathPrefix)) {
+ log.warn("MinIO endpoint 含 path,将按 origin={} pathPrefix={} 访问;建议 endpoint 使用内网地址,transform-endpoint 使用外网 HTTPS",
+ endpointOrigin, endpointPathPrefix);
+ }
+ if (StringUtil.isBlank(properties.getTransformEndpoint())
+ && properties.getEndpoint().trim().toLowerCase().startsWith("http://")) {
+ log.warn("MinIO transform-endpoint 未配置,endpoint={}。HTTPS 页面分片直传会被浏览器 Mixed Content 拦截,将回退为服务端代传",
+ properties.getEndpoint());
+ } else {
+ log.info("MinIO 文件存储已启用 endpoint={} pathPrefix={} transformEndpoint={}",
+ endpointOrigin, endpointPathPrefix, properties.getTransformEndpoint());
+ }
+ MinioClient minioClient = MinioClientFactory.create(
+ properties.getEndpoint(), properties.getAccessKeyId(), properties.getAccessKeySecret());
return new MinioFileServiceImpl(properties, minioClient);
}
@@ -95,7 +110,16 @@ public class FileConfig {
FileStorageProperties.StoreProperties mergedProperties = new FileStorageProperties.StoreProperties();
String fileStoragePrefix = "file.storage." + prefix;
mergedProperties.setEndpoint(firstNotBlank(properties.getEndpoint(), environment.getProperty(fileStoragePrefix + ".endpoint"), environment.getProperty(prefix + ".endpoint")));
- mergedProperties.setTransformEndpoint(firstNotBlank(properties.getTransformEndpoint(), environment.getProperty(fileStoragePrefix + ".transform-endpoint"), environment.getProperty(prefix + ".transform-endpoint")));
+ // 资源管理页 /resource/oss 的外网地址写在库表,不进 Spring Environment;凭证分片必须单独配 file.storage.minio.transform-endpoint
+ mergedProperties.setTransformEndpoint(firstNotBlank(
+ properties.getTransformEndpoint(),
+ environment.getProperty(fileStoragePrefix + ".transform-endpoint"),
+ environment.getProperty(fileStoragePrefix + ".transformEndpoint"),
+ environment.getProperty(prefix + ".transform-endpoint"),
+ environment.getProperty(prefix + ".transformEndpoint"),
+ environment.getProperty("oss.transform-endpoint"),
+ environment.getProperty("oss.transformEndpoint")
+ ));
mergedProperties.setAccessKeyId(firstNotBlank(properties.getAccessKeyId(), environment.getProperty(fileStoragePrefix + ".access-key-id"), environment.getProperty(fileStoragePrefix + ".access-key"), environment.getProperty(prefix + ".access-key-id"), environment.getProperty(prefix + ".access-key")));
mergedProperties.setAccessKeySecret(firstNotBlank(properties.getAccessKeySecret(), environment.getProperty(fileStoragePrefix + ".access-key-secret"), environment.getProperty(fileStoragePrefix + ".secret-key"), environment.getProperty(prefix + ".access-key-secret"), environment.getProperty(prefix + ".secret-key")));
mergedProperties.setBucketName(firstNotBlank(properties.getBucketName(), environment.getProperty(fileStoragePrefix + ".bucket-name"), environment.getProperty(prefix + ".bucket-name")));
@@ -107,7 +131,7 @@ public class FileConfig {
private String firstNotBlank(String... values) {
for (String value : values) {
if (StringUtil.isNotBlank(value)) {
- return value;
+ return value.trim();
}
}
return null;
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileTaskController.java b/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileTaskController.java
index 62e4dabf..462de6e0 100644
--- a/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileTaskController.java
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileTaskController.java
@@ -111,4 +111,26 @@ public class FileTaskController extends BladeController {
String uploadUrl = fileTaskService.getUploadUrl(id, partNumber);
return R.data(uploadUrl);
}
+
+ @ApiLog("上传任务-服务端代传分片")
+ @PostMapping(value = "/uploadPart", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
+ @Parameters({
+ @Parameter(name = "id", description = "文件任务id", in = ParameterIn.QUERY, schema = @Schema(type = "string")),
+ @Parameter(name = "partNumber", description = "分段编号", in = ParameterIn.QUERY, schema = @Schema(type = "int"))
+ })
+ @Operation(summary = "服务端代传分片", description = "HTTPS 页面无法直连 HTTP MinIO 时,由服务端转发分片到对象存储")
+ public R uploadPart(Long id, Integer partNumber, org.springframework.web.multipart.MultipartFile file) throws java.io.IOException {
+ if (file == null || file.isEmpty()) {
+ return R.fail("分片内容不能为空");
+ }
+ java.io.File tempFile = java.io.File.createTempFile("file-part-", ".bin");
+ try {
+ file.transferTo(tempFile);
+ return R.data(fileTaskService.uploadPart(id, partNumber, tempFile));
+ } finally {
+ if (tempFile.exists() && !tempFile.delete()) {
+ tempFile.deleteOnExit();
+ }
+ }
+ }
}
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskService.java
index ae394521..f47120a4 100644
--- a/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskService.java
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskService.java
@@ -74,6 +74,16 @@ public interface IFileTaskService extends IService {
*/
String getUploadUrl(Long id, Integer partNumber);
+ /**
+ * 服务端代传分片(HTTPS 页面无法直连 HTTP MinIO 时使用)
+ *
+ * @param id 文件任务id
+ * @param partNumber 分段编号
+ * @param partFile 分片临时文件
+ * @return ETag
+ */
+ String uploadPart(Long id, Integer partNumber, java.io.File partFile);
+
/**
* 取消文件任务
* @param taskIds
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java
index b92fc66e..25a46bab 100644
--- a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java
@@ -259,6 +259,33 @@ public class FileTaskServiceImpl extends ServiceImpl i
return fileService.getUploadPartUrl(fileTask.getObjectKey(), fileTask.getUploadId(), partNumber, null);
}
+ @Override
+ public String uploadPart(Long id, Integer partNumber, java.io.File partFile) {
+ FileTask fileTask = this.getById(id);
+ Func.requireNotNull(fileTask, "文件任务不存在");
+ if (FileTaskStatus.COMPLETED.getCode().equals(fileTask.getStatus())) {
+ throw new ServiceException("文件任务已完成");
+ }
+ if (FileTaskStatus.PAUSED.getCode().equals(fileTask.getStatus())) {
+ throw new ServiceException("文件任务已暂停");
+ }
+ if (partFile == null || !partFile.exists()) {
+ throw new ServiceException("分片文件不存在");
+ }
+ org.springblade.file.pojo.vo.PartEtagVO partEtag = fileService.uploadPart(
+ fileTask.getObjectKey(),
+ fileTask.getUploadId(),
+ partFile,
+ partNumber,
+ 0L,
+ partFile.length()
+ );
+ if (partEtag == null || StringUtil.isBlank(partEtag.getEtag())) {
+ throw new ServiceException("分片上传未返回 ETag");
+ }
+ return partEtag.getEtag().replace("\"", "");
+ }
+
@Transactional(rollbackFor = Exception.class)
@Override
public void cancelBatchByIds(List taskIds) {
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java
index 5214c628..c7d2ed4e 100644
--- a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java
@@ -25,11 +25,12 @@
*/
package org.springblade.file.service.impl;
+import io.minio.ComposeObjectArgs;
+import io.minio.ComposeSource;
import io.minio.GetObjectArgs;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
-import io.minio.ComposeObjectArgs;
-import io.minio.ComposeSource;
+import io.minio.ObjectWriteResponse;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.UploadObjectArgs;
@@ -39,9 +40,13 @@ import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.file.config.FileStorageProperties;
+import org.springblade.file.util.MinioClientFactory;
+import org.springblade.file.pojo.vo.PartEtagVO;
import java.io.File;
+import java.io.FileInputStream;
import java.io.InputStream;
+import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
@@ -61,9 +66,11 @@ import java.util.UUID;
public class MinioFileServiceImpl extends AbstractFileService {
private static final long PART_SIZE = 10 * 1024 * 1024L;
+ private static final String DEFAULT_REGION = MinioClientFactory.DEFAULT_REGION;
private final FileStorageProperties.StoreProperties properties;
private final MinioClient minioClient;
+ private volatile MinioClient transformPresignClient;
@Override
public String initiateMultipartUpload(String objectKey) {
@@ -73,15 +80,50 @@ public class MinioFileServiceImpl extends AbstractFileService {
@Override
public String getUploadPartUrl(String objectKey, String uploadId, Integer partNumber, Long expires) {
try {
- return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
- .method(Method.PUT).bucket(properties.getBucketName()).object(partObjectKey(objectKey, uploadId, partNumber))
- .expiry(Math.toIntExact(resolveExpires(expires))).build());
+ String presignedUrl = resolvePresignClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
+ .method(Method.PUT)
+ .bucket(properties.getBucketName())
+ .object(partObjectKey(objectKey, uploadId, partNumber))
+ .region(DEFAULT_REGION)
+ .expiry(Math.toIntExact(resolveExpires(expires)))
+ .build());
+ return applyTransformPathPrefix(presignedUrl);
} catch (Exception exception) {
log.error("MinIO获取分片上传链接异常 objectKey:{}", objectKey, exception);
throw new ServiceException("获取分片上传链接异常");
}
}
+ @Override
+ public PartEtagVO uploadPart(String objectKey, String uploadId, File file, int partNumber, Long offset, Long partSize) {
+ long start = offset == null ? 0L : offset;
+ long size = partSize == null ? Math.max(0L, file.length() - start) : partSize;
+ try (FileInputStream inputStream = new FileInputStream(file)) {
+ long skipped = inputStream.skip(start);
+ if (skipped < start) {
+ throw new ServiceException("MinIO分段上传读取文件偏移失败");
+ }
+ ObjectWriteResponse response = minioClient.putObject(PutObjectArgs.builder()
+ .bucket(properties.getBucketName())
+ .object(partObjectKey(objectKey, uploadId, partNumber))
+ .stream(inputStream, size, PART_SIZE)
+ .build());
+ String etag = response.etag() == null ? "" : response.etag().replace("\"", "");
+ return new PartEtagVO(etag, partNumber);
+ } catch (ServiceException serviceException) {
+ throw serviceException;
+ } catch (Exception exception) {
+ log.error("MinIO分段上传异常 objectKey:{} partNumber:{}", objectKey, partNumber, exception);
+ throw new ServiceException("MinIO分段上传异常");
+ }
+ }
+
+ @Override
+ public void cancelMultipartUpload(String objectKey, String uploadId) {
+ // MinIO 分片以独立对象模拟,取消时由任务层清理业务状态即可
+ log.debug("MinIO取消分段上传 objectKey:{} uploadId:{}", objectKey, uploadId);
+ }
+
@Override
public void completeMultipartUpload(String objectKey, String uploadId, List partEtags) {
try {
@@ -143,12 +185,14 @@ public class MinioFileServiceImpl extends AbstractFileService {
return null;
}
try {
- return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
+ String presignedUrl = resolvePresignClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(properties.getBucketName())
.object(resolveObjectKey(objectKey))
+ .region(DEFAULT_REGION)
.expiry(Math.toIntExact(resolveExpires(expires)))
.build());
+ return applyTransformPathPrefix(presignedUrl);
} catch (Exception exception) {
log.error("MinIO获取文件链接异常 objectKey:{}", objectKey, exception);
throw new ServiceException("MinIO获取文件链接异常");
@@ -170,6 +214,62 @@ public class MinioFileServiceImpl extends AbstractFileService {
}
}
+ /**
+ * 浏览器侧预签名优先使用外网地址,避免 HTTPS 页面访问 HTTP MinIO 被 Mixed Content 拦截。
+ * MinIO SDK 不允许 endpoint 带 path,因此外网地址中的 /oss 等前缀在签名后单独拼回。
+ */
+ private MinioClient resolvePresignClient() {
+ String transformEndpoint = properties.getTransformEndpoint();
+ if (StringUtil.isBlank(transformEndpoint)) {
+ return minioClient;
+ }
+ MinioClient cachedClient = transformPresignClient;
+ if (cachedClient != null) {
+ return cachedClient;
+ }
+ synchronized (this) {
+ if (transformPresignClient == null) {
+ transformPresignClient = MinioClientFactory.create(
+ transformEndpoint, properties.getAccessKeyId(), properties.getAccessKeySecret());
+ }
+ return transformPresignClient;
+ }
+ }
+
+ private String applyTransformPathPrefix(String presignedUrl) {
+ String transformEndpoint = properties.getTransformEndpoint();
+ if (StringUtil.isBlank(transformEndpoint) || StringUtil.isBlank(presignedUrl)) {
+ return presignedUrl;
+ }
+ String pathPrefix = normalizePathPrefix(URI.create(transformEndpoint.trim()).getPath());
+ if (StringUtil.isBlank(pathPrefix)) {
+ return presignedUrl;
+ }
+ URI url = URI.create(presignedUrl);
+ String path = url.getRawPath() == null ? "" : url.getRawPath();
+ if (path.equals(pathPrefix) || path.startsWith(pathPrefix + "/")) {
+ return presignedUrl;
+ }
+ StringBuilder result = new StringBuilder();
+ result.append(url.getScheme()).append("://").append(url.getHost());
+ if (url.getPort() > 0) {
+ result.append(':').append(url.getPort());
+ }
+ result.append(pathPrefix).append(path);
+ if (StringUtil.isNotBlank(url.getRawQuery())) {
+ result.append('?').append(url.getRawQuery());
+ }
+ return result.toString();
+ }
+
+ private String normalizePathPrefix(String path) {
+ if (StringUtil.isBlank(path) || "/".equals(path)) {
+ return "";
+ }
+ String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
+ return normalized.startsWith("/") ? normalized : "/" + normalized;
+ }
+
private String resolveObjectKey(String objectKey) {
if (StringUtil.isBlank(properties.getRootDirectory())) {
return objectKey;
diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/util/MinioClientFactory.java b/blade-service/blade-file/src/main/java/org/springblade/file/util/MinioClientFactory.java
new file mode 100644
index 00000000..384dc61e
--- /dev/null
+++ b/blade-service/blade-file/src/main/java/org/springblade/file/util/MinioClientFactory.java
@@ -0,0 +1,121 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.file.util;
+
+import io.minio.MinioClient;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.tool.utils.StringUtil;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * MinIO 客户端构建。
+ *
+ * SDK 不允许 endpoint 带 path;若配置为 {@code https://host/oss},则:
+ *
+ * endpoint 使用 origin
+ * 通过 OkHttp 拦截器把 /oss 拼回每个 API 请求
+ * 固定 region,避免 GetBucketLocation 打到业务网关拿到 HTML
+ *
+ */
+public final class MinioClientFactory {
+
+ public static final String DEFAULT_REGION = "us-east-1";
+
+ private MinioClientFactory() {
+ }
+
+ public static MinioClient create(String endpoint, String accessKey, String secretKey) {
+ EndpointParts parts = parseEndpoint(endpoint);
+ MinioClient.Builder builder = MinioClient.builder()
+ .endpoint(parts.origin())
+ .credentials(accessKey, secretKey)
+ .region(DEFAULT_REGION);
+ if (StringUtil.isNotBlank(parts.pathPrefix())) {
+ builder.httpClient(buildHttpClient(parts.pathPrefix()), true);
+ }
+ return builder.build();
+ }
+
+ public static String resolveOrigin(String endpoint) {
+ return parseEndpoint(endpoint).origin();
+ }
+
+ public static String resolvePathPrefix(String endpoint) {
+ return parseEndpoint(endpoint).pathPrefix();
+ }
+
+ private static EndpointParts parseEndpoint(String endpoint) {
+ if (StringUtil.isBlank(endpoint)) {
+ throw new ServiceException("MinIO endpoint 不能为空");
+ }
+ URI uri = URI.create(endpoint.trim());
+ if (StringUtil.isBlank(uri.getScheme()) || StringUtil.isBlank(uri.getHost())) {
+ throw new ServiceException("MinIO endpoint 配置不正确: " + endpoint);
+ }
+ StringBuilder origin = new StringBuilder();
+ origin.append(uri.getScheme()).append("://").append(uri.getHost());
+ if (uri.getPort() > 0) {
+ origin.append(':').append(uri.getPort());
+ }
+ return new EndpointParts(origin.toString(), normalizePathPrefix(uri.getPath()));
+ }
+
+ private static String normalizePathPrefix(String path) {
+ if (StringUtil.isBlank(path) || "/".equals(path)) {
+ return "";
+ }
+ String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
+ return normalized.startsWith("/") ? normalized : "/" + normalized;
+ }
+
+ private static OkHttpClient buildHttpClient(String pathPrefix) {
+ return new OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .addInterceptor(chain -> {
+ Request original = chain.request();
+ HttpUrl originalUrl = original.url();
+ String encodedPath = originalUrl.encodedPath();
+ if (encodedPath.startsWith(pathPrefix + "/") || encodedPath.equals(pathPrefix)) {
+ return chain.proceed(original);
+ }
+ HttpUrl newUrl = originalUrl.newBuilder()
+ .encodedPath(pathPrefix + encodedPath)
+ .build();
+ return chain.proceed(original.newBuilder().url(newUrl).build());
+ })
+ .build();
+ }
+
+ private record EndpointParts(String origin, String pathPrefix) {
+ }
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java b/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java
index f7c1829b..ba5bdf43 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/convert/DeptConvert.java
@@ -44,94 +44,79 @@ public interface DeptConvert {
@Mapping(source = "subcompanyname", target = "deptName")
@Mapping(source = "subcompanydesc", target = "fullName")
- // 父公司id转为父部门编码,要拼接前缀
@Mapping(source = "supsubcomid", target = "parentCode")
@Mapping(source = "created", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
- // OA modified 单独落库,用于增量比对,避免覆盖本系统 updateTime
@Mapping(source = "modified", target = "oaModified")
Dept baseConvert(OACompanyResponse company);
@Mapping(source = "departmentname", target = "deptName")
@Mapping(source = "departmentmark", target = "fullName")
- // 父部门id转为父部门编码,要拼接前缀
@Mapping(source = "supdepid", target = "parentCode")
- // 所属子公司id转为所属公司编码,要拼接前缀
@Mapping(source = "subcompanyid1", target = "belongCompanyCode")
@Mapping(source = "created", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
- // OA modified 单独落库,用于增量比对,避免覆盖本系统 updateTime
@Mapping(source = "modified", target = "oaModified")
Dept baseConvert(OADepartmentResponse department);
/**
- * oa公司转本系统部门
- * @param company
- * @return
+ * oa公司转本系统部门。组织编码由同步服务按 GWWL+三位数字 统一分配。
*/
default Dept company2dept(OACompanyResponse company) {
Dept dept = this.baseConvert(company);
- // 部门编码添加前缀
- dept.setDeptCode(OAConvertConstant.COMPANY_OA_PREFIX + company.getId());
+ // 编码稍后按规则生成;此处仅落 OA 主键,供按 oaId 匹配与重建层级
+ dept.setDeptCode(null);
String parentCode = dept.getParentCode();
if (!OAConstant.ROOT_COMPANY_ID.equals(parentCode)) {
- // 父部门编码不是0,拼接前缀
- dept.setParentCode(OAConvertConstant.COMPANY_OA_PREFIX + parentCode);
- // 父部门id后续处理
+ // 非顶级:parentCode 暂存上级 OA 主键,分配编码后再替换为正式组织编码
+ dept.setParentCode(parentCode);
+ dept.setParentId(null);
} else {
- // 父部门编码是0,父id也设置为0
- dept.setParentId(OAConvertConstant.ROOT_PARENT_ID);
+ dept.setParentCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
+ dept.setParentId(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
}
- // 公司所属公司编码就是他自己的部门编码
- dept.setBelongCompanyCode(dept.getDeptCode());
- // OA主键与上级OA主键,分批同步结束后用于重建层级
+ dept.setBelongCompanyCode(null);
dept.setOaId(company.getId());
dept.setOaSupSubComId(company.getSupsubcomid());
- // 显示顺序处理
dept.setSort(OAUtils.parseInt(company.getShoworder()));
- // 部门类型,公司
dept.setDeptCategory(DeptCategory.COMPANY.getCode());
dept.setSyncTime(new Date());
- // 是oa的
dept.setIsOa(CommonConstant.YES);
return dept;
}
/**
- * oa部门转本系统部门
- * @param oaDept
- * @param companyDeptMap 所有公司的部门编码与id map
- * @return
+ * oa部门转本系统部门。组织编码由同步服务按「所属公司编码+三位数字」统一分配。
+ *
+ * @param oaDept OA部门
+ * @param companyByOaId 已同步公司(oaId → 本地公司)
*/
- default Dept dept2dept(OADepartmentResponse oaDept, Map companyDeptMap) {
+ default Dept dept2dept(OADepartmentResponse oaDept, Map companyByOaId) {
Dept dept = this.baseConvert(oaDept);
- // 部门编码添加前缀
- dept.setDeptCode(OAConvertConstant.DEPARTMENT_OA_PREFIX + oaDept.getId());
- // OA主键;上级为根部门时记录所属公司OA主键,否则记录上级部门OA主键
+ dept.setDeptCode(null);
dept.setOaId(oaDept.getId());
if (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid())) {
dept.setOaSupSubComId(oaDept.getSubcompanyid1());
} else {
dept.setOaSupSubComId(oaDept.getSupdepid());
}
- // 部门所属公司编码就是他所属公司编码加前缀
- dept.setBelongCompanyCode(OAConvertConstant.COMPANY_OA_PREFIX + dept.getBelongCompanyCode());
- String parentCode = dept.getParentCode();
- if (!OAConstant.ROOT_COMPANY_ID.equals(parentCode)) {
- // 父部门编码不是0,拼接前缀
- dept.setParentCode(OAConvertConstant.DEPARTMENT_OA_PREFIX + parentCode);
- // 父部门id后续处理
+ Dept belongCompany = companyByOaId == null ? null : companyByOaId.get(oaDept.getSubcompanyid1());
+ if (belongCompany != null) {
+ dept.setBelongCompanyCode(belongCompany.getDeptCode());
+ dept.setParentId(null);
+ if (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid())) {
+ dept.setParentCode(belongCompany.getDeptCode());
+ dept.setParentId(belongCompany.getId());
+ } else {
+ // 上级为部门:parentCode 暂存上级部门 OA 主键,重建层级时再解析
+ dept.setParentCode(oaDept.getSupdepid());
+ }
} else {
- // 父部门编码是0,设置为公司编码
- dept.setParentCode(dept.getBelongCompanyCode());
- // 根据父部门编码获取对应的id
- dept.setParentId(companyDeptMap.get(dept.getParentCode()));
+ // 所属公司尚未解析时,暂存 OA 公司主键,后续补齐正式编码
+ dept.setBelongCompanyCode(oaDept.getSubcompanyid1());
+ dept.setParentCode(oaDept.getSupdepid());
}
-
- // 显示顺序处理
dept.setSort(OAUtils.parseInt(oaDept.getShoworder()));
- // 部门类型,部门
dept.setDeptCategory(DeptCategory.DEPT.getCode());
dept.setSyncTime(new Date());
- // 是oa的
dept.setIsOa(CommonConstant.YES);
return dept;
}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java b/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java
index 6decb305..00b2e576 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/convert/UserConvert.java
@@ -88,6 +88,8 @@ public interface UserConvert {
default User person2user(OAPersonResponse person, String defaultPassword) {
User user = baseConvert(person);
user.setAccount(resolveAccount(person));
+ // OA 可能返回多号(如 133.../137...),只保留首个号码再入库加密
+ user.setPhone(normalizeMobile(user.getPhone()));
// 密码
user.setPassword(defaultPassword);
// 性别
@@ -103,6 +105,23 @@ public interface UserConvert {
return user;
}
+ /**
+ * 规范化手机号:去空白,多号时取第一个
+ */
+ default String normalizeMobile(String mobile) {
+ if (StringUtils.isBlank(mobile)) {
+ return null;
+ }
+ String trimmed = mobile.trim();
+ String[] parts = trimmed.split("[/|,;;,\\s]+");
+ for (String part : parts) {
+ if (StringUtils.isNotBlank(part)) {
+ return part.trim();
+ }
+ }
+ return null;
+ }
+
/**
* 解析本系统登录账号:优先 OA loginid,其次工号,最后手机号
*
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java
index d08d36a6..edacf506 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java
@@ -66,6 +66,8 @@ public class SysClient implements ISysClient {
private final IFeeItemService feeItemService;
private final ICargoTypeService cargoTypeService;
+ private final IMeasurementUnitService measurementUnitService;
+ private final IInvoiceItemService invoiceItemService;
@Override
@GetMapping(MENU)
@@ -231,10 +233,27 @@ public class SysClient implements ISysClient {
.orderByAsc(CargoType::getCargoCode)));
}
+ @Override
+ @GetMapping(MEASUREMENT_UNITS)
+ public R> getMeasurementUnits() {
+ return R.data(measurementUnitService.list(Wrappers.lambdaQuery()
+ .eq(MeasurementUnit::getIsDeleted, 0)
+ .orderByAsc(MeasurementUnit::getUnitCode)));
+ }
+
@Override
@GetMapping(PERMISSIONS)
public R> getPermissions(String roleId) {
return R.data(menuService.permissionCodes(roleId));
}
+ @Override
+ @GetMapping(INVOICE_ITEMS)
+ public R> getInvoiceItems() {
+ return R.data(invoiceItemService.list(Wrappers.lambdaQuery()
+ .eq(InvoiceItem::getStatus, 1)
+ .eq(InvoiceItem::getIsDeleted, 0)
+ .orderByAsc(InvoiceItem::getCategoryName, InvoiceItem::getShortName)));
+ }
+
}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.java
index e4b98971..940bb5bb 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.java
@@ -34,6 +34,26 @@ import java.util.List;
*/
public interface MeasurementUnitMapper extends BaseMapper {
+ /**
+ * 按编码查询计量单位(包含逻辑删除记录,用于新增恢复)。
+ */
+ MeasurementUnit selectByUnitCodeIncludingDeleted(@Param("unitCode") String unitCode);
+
+ /**
+ * 按名称查询计量单位(包含逻辑删除记录,用于新增恢复)。
+ */
+ MeasurementUnit selectByUnitNameIncludingDeleted(@Param("unitName") String unitName);
+
+ /**
+ * 恢复逻辑删除计量单位。
+ */
+ int restoreById(@Param("id") Long id);
+
+ /**
+ * 释放已软删除记录占用的单位名称,避免唯一索引冲突。
+ */
+ int releaseUnitName(@Param("id") Long id, @Param("unitName") String unitName);
+
/**
* 自定义分页
*
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml
index 10fbb903..4f0bd97d 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MeasurementUnitMapper.xml
@@ -19,6 +19,34 @@
+
+ SELECT *
+ FROM blade_measurement_unit
+ WHERE unit_code = #{unitCode}
+ LIMIT 1
+
+
+
+ SELECT *
+ FROM blade_measurement_unit
+ WHERE unit_name = #{unitName}
+ LIMIT 1
+
+
+
+ UPDATE blade_measurement_unit
+ SET is_deleted = 0
+ WHERE id = #{id}
+ AND is_deleted = 1
+
+
+
+ UPDATE blade_measurement_unit
+ SET unit_name = #{unitName}
+ WHERE id = #{id}
+ AND is_deleted = 1
+
+
SELECT
mmu.id,
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.java
index 287c9e27..3d56d687 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.java
@@ -40,6 +40,13 @@ import java.util.List;
*/
public interface UserDeptMapper extends BaseMapper {
+ /**
+ * 提高当前会话 group_concat 上限,需与 queryUserDeptIds 在同一事务/连接中调用
+ *
+ * @param maxLen 最大长度
+ */
+ void setGroupConcatMaxLen(@Param("maxLen") int maxLen);
+
/**
* 根据用户id列表查询对应的部门id列表拼接的字符串
* @param userIds
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.xml
index 03182d8c..76c059bf 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.xml
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserDeptMapper.xml
@@ -2,6 +2,10 @@
+
+ SET SESSION group_concat_max_len = #{maxLen}
+
+
select
group_concat(distinct bd.id) deptIds,
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/rule/tenant/TenantDeptRule.java b/blade-service/blade-system/src/main/java/org/springblade/system/rule/tenant/TenantDeptRule.java
index ba893bfd..07a0ca8f 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/rule/tenant/TenantDeptRule.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/rule/tenant/TenantDeptRule.java
@@ -30,6 +30,7 @@ import org.springblade.core.literule.core.RuleComponent;
import org.springblade.core.tool.constant.BladeConstant;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.system.pojo.entity.Tenant;
+import org.springblade.system.pojo.enums.DeptCategory;
import org.springblade.system.rule.context.TenantContext;
import static org.springblade.system.rule.constant.TenantRuleConstant.TENANT_DEPT_RULE;
@@ -54,7 +55,7 @@ public class TenantDeptRule extends RuleComponent {
dept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
dept.setDeptName(tenant.getTenantName());
dept.setFullName(tenant.getTenantName());
- dept.setDeptCategory(1);
+ dept.setDeptCategory(DeptCategory.GROUP.getCode());
dept.setSort(2);
dept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java
index cee1baf1..d9094bf0 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java
@@ -149,12 +149,19 @@ public interface IDeptService extends IService {
boolean removeDept(String ids);
/**
- * 删除非顶级组织,顶级组织(父级为 0)保留
+ * 清除非保留顶级外的组织;保留 id=1 集团锚点,以及名称为「外部组织」的顶级
*
* @return 删除数量
*/
int clearNonTopDepts();
+ /**
+ * 确保双顶级:id=1 集团(编码 GWWL)+ 独立「外部组织」顶级
+ *
+ * @return 集团顶级组织(id=1)
+ */
+ Dept ensureOaSyncRootDept();
+
/**
* 新增或修改部门
*
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java
index 24f461de..d55f05e6 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java
@@ -27,12 +27,14 @@ package org.springblade.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springblade.common.constant.CommonConstant;
import org.springblade.common.constant.DataStatusEnum;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.log.exception.ServiceException;
@@ -389,16 +391,256 @@ public class DeptServiceImpl extends ServiceImpl implements ID
@Override
@Transactional(rollbackFor = Exception.class)
public int clearNonTopDepts() {
- Long nonTopCount = this.count(Wrappers.lambdaQuery()
- .isNotNull(Dept::getParentId)
- .ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID));
- if (nonTopCount == null || nonTopCount == 0L) {
+ // 同步前清理:保留集团锚点 id=1,以及「外部组织」顶级;其余删除
+ Dept groupRoot = this.ensureOaSyncRootDept();
+ Dept externalRoot = this.ensureExternalOrgTopDept();
+ Set keepIds = new HashSet<>();
+ if (groupRoot != null && groupRoot.getId() != null) {
+ keepIds.add(groupRoot.getId());
+ }
+ if (externalRoot != null && externalRoot.getId() != null) {
+ keepIds.add(externalRoot.getId());
+ }
+ if (keepIds.isEmpty()) {
return 0;
}
- this.remove(Wrappers.lambdaQuery()
- .isNotNull(Dept::getParentId)
- .ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID));
- return nonTopCount.intValue();
+ Long clearCount = this.count(Wrappers.lambdaQuery().notIn(Dept::getId, keepIds));
+ if (clearCount == null || clearCount == 0L) {
+ return 0;
+ }
+ this.remove(Wrappers.lambdaQuery().notIn(Dept::getId, keepIds));
+ return clearCount.intValue();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Dept ensureOaSyncRootDept() {
+ this.migrateLegacyOaDeptCategory();
+ this.removeLegacyOaSyncRoots();
+ this.migrateLegacyGroupRootToIdOne();
+ Dept rootDept = this.getById(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
+ String tenantId = AuthUtil.getTenantId();
+ if (StringUtil.isBlank(tenantId)) {
+ tenantId = BladeConstant.ADMIN_TENANT_ID;
+ }
+ if (rootDept == null) {
+ rootDept = new Dept();
+ rootDept.setId(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
+ rootDept.setTenantId(tenantId);
+ rootDept.setParentId(BladeConstant.TOP_PARENT_ID);
+ rootDept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
+ rootDept.setDeptName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ rootDept.setFullName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ rootDept.setShortName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ rootDept.setDeptCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
+ rootDept.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
+ rootDept.setBelongCompanyCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
+ rootDept.setDeptCategory(DeptCategory.GROUP.getCode());
+ rootDept.setSort(0);
+ rootDept.setStatus(DataStatusEnum.ENABLE.getCode());
+ rootDept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
+ rootDept.setIsOa(0);
+ rootDept.setIsPlatformCompany(0);
+ rootDept.setSyncTime(new Date());
+ this.save(rootDept);
+ log.info("已创建 OA 同步集团顶级 id={} name={} code={}",
+ OAConvertConstant.OA_SYNC_ROOT_DEPT_ID,
+ OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME,
+ OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
+ } else {
+ boolean wronglyNamedAsExternal = OAConvertConstant.EXTERNAL_ORG_NAME.equals(rootDept.getDeptName());
+ boolean legacyNamed = OAConvertConstant.LEGACY_ROOT_DEPT_NAME.equals(rootDept.getDeptName());
+ boolean needRename = wronglyNamedAsExternal || legacyNamed || StringUtil.isBlank(rootDept.getDeptName())
+ || !OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME.equals(rootDept.getDeptName());
+ boolean needUpdate = needRename
+ || !OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE.equals(rootDept.getDeptCode())
+ || !DeptCategory.GROUP.getCode().equals(rootDept.getDeptCategory())
+ || !Objects.equals(BladeConstant.TOP_PARENT_ID, rootDept.getParentId());
+ if (needUpdate) {
+ Dept updateParam = new Dept();
+ updateParam.setId(rootDept.getId());
+ if (needRename) {
+ updateParam.setDeptName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ updateParam.setFullName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ updateParam.setShortName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ rootDept.setDeptName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ rootDept.setFullName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ rootDept.setShortName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ }
+ updateParam.setDeptCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
+ updateParam.setBelongCompanyCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
+ updateParam.setDeptCategory(DeptCategory.GROUP.getCode());
+ updateParam.setParentId(BladeConstant.TOP_PARENT_ID);
+ updateParam.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
+ updateParam.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
+ updateParam.setIsOa(0);
+ this.updateById(updateParam);
+ rootDept.setDeptCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
+ rootDept.setBelongCompanyCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
+ rootDept.setDeptCategory(DeptCategory.GROUP.getCode());
+ rootDept.setParentId(BladeConstant.TOP_PARENT_ID);
+ rootDept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
+ rootDept.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
+ rootDept.setIsOa(0);
+ log.info("已校正 OA 同步集团顶级 id={} name={} code={} category={}",
+ rootDept.getId(), rootDept.getDeptName(), rootDept.getDeptCode(), rootDept.getDeptCategory());
+ }
+ }
+ this.ensureExternalOrgTopDept();
+ return rootDept;
+ }
+
+ /**
+ * 将历史集团伪顶级 id=135 迁移到 id=1(子节点改挂,再删除旧锚点)
+ */
+ private void migrateLegacyGroupRootToIdOne() {
+ Long legacyRootId = OAConvertConstant.LEGACY_OA_SYNC_ROOT_DEPT_ID;
+ Long newRootId = OAConvertConstant.OA_SYNC_ROOT_DEPT_ID;
+ if (Objects.equals(legacyRootId, newRootId)) {
+ return;
+ }
+ Dept legacyRoot = this.getById(legacyRootId);
+ if (legacyRoot == null) {
+ return;
+ }
+ // 仅迁移历史伪顶级;若 id=135 已是真实 OA 公司记录则跳过删除
+ boolean isOaCompanyRecord = StringUtil.isNotBlank(legacyRoot.getOaId())
+ && DeptCategory.COMPANY.getCode().equals(legacyRoot.getDeptCategory())
+ && (Objects.equals(legacyRoot.getIsOa(), CommonConstant.YES) || Objects.equals(legacyRoot.getIsOa(), 1));
+ if (isOaCompanyRecord) {
+ log.warn("历史 id={} 已是 OA 同步公司(oaId={}),跳过伪顶级迁移", legacyRootId, legacyRoot.getOaId());
+ return;
+ }
+ Dept newRoot = this.getById(newRootId);
+ String tenantId = AuthUtil.getTenantId();
+ if (StringUtil.isBlank(tenantId)) {
+ tenantId = BladeConstant.ADMIN_TENANT_ID;
+ }
+ if (newRoot == null) {
+ newRoot = new Dept();
+ newRoot.setId(newRootId);
+ newRoot.setTenantId(StringUtil.isNotBlank(legacyRoot.getTenantId()) ? legacyRoot.getTenantId() : tenantId);
+ newRoot.setParentId(BladeConstant.TOP_PARENT_ID);
+ newRoot.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
+ newRoot.setDeptName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ newRoot.setFullName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ newRoot.setShortName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
+ newRoot.setDeptCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
+ newRoot.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
+ newRoot.setBelongCompanyCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
+ newRoot.setDeptCategory(DeptCategory.GROUP.getCode());
+ newRoot.setSort(0);
+ newRoot.setStatus(DataStatusEnum.ENABLE.getCode());
+ newRoot.setIsDeleted(BladeConstant.DB_NOT_DELETED);
+ newRoot.setIsOa(0);
+ newRoot.setIsPlatformCompany(0);
+ newRoot.setSyncTime(new Date());
+ this.save(newRoot);
+ log.info("迁移创建新集团顶级 id={}(来自历史 id={})", newRootId, legacyRootId);
+ }
+ this.update(Wrappers.lambdaUpdate()
+ .set(Dept::getParentId, newRootId)
+ .set(Dept::getParentCode, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE)
+ .eq(Dept::getParentId, legacyRootId));
+ this.removeById(legacyRootId);
+ log.info("已将历史集团顶级 id={} 迁移到 id={},并删除旧锚点", legacyRootId, newRootId);
+ }
+
+ /**
+ * 确保存在独立的「外部组织」顶级(与 id=1 集团并存,仅保留一个同名顶级)
+ */
+ private Dept ensureExternalOrgTopDept() {
+ List externalTops = this.list(Wrappers.lambdaQuery()
+ .eq(Dept::getDeptName, OAConvertConstant.EXTERNAL_ORG_NAME)
+ .and(wrapper -> wrapper
+ .isNull(Dept::getParentId)
+ .or()
+ .eq(Dept::getParentId, BladeConstant.TOP_PARENT_ID))
+ .ne(Dept::getId, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID)
+ .orderByAsc(Dept::getId));
+ if (CollectionUtil.isNotEmpty(externalTops)) {
+ Dept keep = externalTops.get(0);
+ if (externalTops.size() > 1) {
+ List duplicateIds = externalTops.stream()
+ .skip(1)
+ .map(Dept::getId)
+ .filter(Objects::nonNull)
+ .toList();
+ if (CollectionUtil.isNotEmpty(duplicateIds)) {
+ this.update(Wrappers.lambdaUpdate()
+ .set(Dept::getParentId, keep.getId())
+ .in(Dept::getParentId, duplicateIds));
+ this.removeByIds(duplicateIds);
+ log.info("已合并重复的外部组织顶级,保留 id={},删除{}", keep.getId(), duplicateIds);
+ }
+ }
+ return keep;
+ }
+ String tenantId = AuthUtil.getTenantId();
+ if (StringUtil.isBlank(tenantId)) {
+ tenantId = BladeConstant.ADMIN_TENANT_ID;
+ }
+ Dept externalDept = new Dept();
+ externalDept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
+ externalDept.setTenantId(tenantId);
+ externalDept.setParentId(BladeConstant.TOP_PARENT_ID);
+ externalDept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
+ externalDept.setDeptName(OAConvertConstant.EXTERNAL_ORG_NAME);
+ externalDept.setFullName(OAConvertConstant.EXTERNAL_ORG_NAME);
+ externalDept.setShortName(OAConvertConstant.EXTERNAL_ORG_NAME);
+ externalDept.setDeptCode(null);
+ externalDept.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
+ externalDept.setDeptCategory(DeptCategory.GROUP.getCode());
+ externalDept.setSort(0);
+ externalDept.setStatus(DataStatusEnum.ENABLE.getCode());
+ externalDept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
+ externalDept.setIsOa(0);
+ externalDept.setIsPlatformCompany(0);
+ externalDept.setSyncTime(new Date());
+ this.save(externalDept);
+ log.info("已创建外部组织顶级 id={}", externalDept.getId());
+ return externalDept;
+ }
+
+ /**
+ * 清理历史伪顶级(OACROOT);同名旧集团仅清理非 id=1 的顶级
+ */
+ private void removeLegacyOaSyncRoots() {
+ List legacyRoots = this.list(Wrappers.lambdaQuery()
+ .ne(Dept::getId, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID)
+ .ne(Dept::getId, OAConvertConstant.LEGACY_OA_SYNC_ROOT_DEPT_ID)
+ .and(wrapper -> wrapper
+ .eq(Dept::getDeptCode, OAConvertConstant.LEGACY_ROOT_DEPT_CODE)
+ .or(nameWrapper -> nameWrapper
+ .eq(Dept::getDeptName, OAConvertConstant.LEGACY_ROOT_DEPT_NAME)
+ .eq(Dept::getDeptCode, OAConvertConstant.LEGACY_ROOT_DEPT_CODE)
+ .and(parentWrapper -> parentWrapper
+ .isNull(Dept::getParentId)
+ .or()
+ .eq(Dept::getParentId, BladeConstant.TOP_PARENT_ID)))));
+ if (CollectionUtil.isEmpty(legacyRoots)) {
+ return;
+ }
+ Set legacyIds = legacyRoots.stream().map(Dept::getId).filter(Objects::nonNull).collect(Collectors.toSet());
+ this.update(Wrappers.lambdaUpdate()
+ .set(Dept::getParentId, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID)
+ .set(Dept::getParentCode, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE)
+ .in(Dept::getParentId, legacyIds));
+ this.removeByIds(legacyIds);
+ log.info("已清理历史伪顶级组织{}条:{}", legacyIds.size(), legacyIds);
+ }
+
+ /**
+ * 按编码前缀对齐组织类型:OAC→公司、OAD→部门(覆盖历史 1/2 映射)
+ */
+ private void migrateLegacyOaDeptCategory() {
+ this.update(Wrappers.lambdaUpdate()
+ .set(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
+ .likeRight(Dept::getDeptCode, OAConvertConstant.DEPARTMENT_OA_PREFIX));
+ this.update(Wrappers.lambdaUpdate()
+ .set(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
+ .likeRight(Dept::getDeptCode, OAConvertConstant.COMPANY_OA_PREFIX)
+ .ne(Dept::getDeptCode, OAConvertConstant.LEGACY_ROOT_DEPT_CODE));
}
@Override
@@ -650,9 +892,16 @@ public class DeptServiceImpl extends ServiceImpl implements ID
if (StringUtil.isBlank(parent.getDeptCode())) {
throw new ServiceException("请选择已配置组织编码的上级组织");
}
- String pattern = Pattern.quote(parent.getDeptCode()) + "-\\d+";
- if (!deptCode.matches(pattern)) {
- throw new ServiceException("组织编码格式应为:上级编码-分段数字");
+ // 公司:GWWL+三位数字;部门:所属公司编码+三位数字;其他:上级编码+三位数字
+ String expectedParentCode = parent.getDeptCode();
+ if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
+ expectedParentCode = OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE;
+ } else if (DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())
+ && StringUtil.isNotBlank(dept.getBelongCompanyCode())) {
+ expectedParentCode = dept.getBelongCompanyCode();
+ }
+ if (!org.springblade.system.util.DeptCodeUtil.matchesChildCode(expectedParentCode, deptCode)) {
+ throw new ServiceException("组织编码格式应为:" + expectedParentCode + "三位数字(不足补零,中间不加-)");
}
this.ensureDeptCodeUnique(dept, deptCode);
dept.setDeptCode(deptCode);
@@ -724,8 +973,8 @@ public class DeptServiceImpl extends ServiceImpl implements ID
log.info("没有记录OA主键的组织,跳过层级重建");
return;
}
- Map companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
- Map departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
+ Map companyByOaId = this.indexOaCompanies(oaDeptList);
+ Map departmentByOaId = this.indexOaDepartments(oaDeptList);
Map desiredParentIdMap = new LinkedHashMap<>();
Map desiredParentCodeMap = new HashMap<>();
for (Dept dept : oaDeptList) {
@@ -778,21 +1027,20 @@ public class DeptServiceImpl extends ServiceImpl implements ID
if (CollectionUtil.isEmpty(oaDeptList)) {
return 0;
}
- Map companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
- Map departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
+ Map companyByOaId = this.indexOaCompanies(oaDeptList);
+ Map departmentByOaId = this.indexOaDepartments(oaDeptList);
Set deleteIds = new HashSet<>();
for (Dept dept : oaDeptList) {
String parentOaId = dept.getOaSupSubComId();
- // 上级为空或为 0:视为本地锚点/根级,不作为孤儿删除
+ // 上级为空或为 0:OA 顶级公司挂本地锚点,不作为孤儿
if (StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId)) {
continue;
}
- boolean parentExists;
- if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
- parentExists = companyByOaId.containsKey(parentOaId);
- } else {
- parentExists = departmentByOaId.containsKey(parentOaId) || companyByOaId.containsKey(parentOaId);
- }
+ boolean isCompany = this.isOaCompany(dept);
+ boolean parentExists = isCompany
+ ? companyByOaId.containsKey(parentOaId)
+ : departmentByOaId.containsKey(parentOaId) || companyByOaId.containsKey(parentOaId);
+ // 全量数据整理后仍找不到上级:公司、部门一律删除,不改挂保留
if (!parentExists) {
deleteIds.add(dept.getId());
}
@@ -813,7 +1061,7 @@ public class DeptServiceImpl extends ServiceImpl implements ID
if (StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId)) {
continue;
}
- Dept parent = DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())
+ Dept parent = this.isOaCompany(dept)
? companyByOaId.get(parentOaId)
: Optional.ofNullable(departmentByOaId.get(parentOaId)).orElse(companyByOaId.get(parentOaId));
if (parent != null && deleteIds.contains(parent.getId())) {
@@ -842,6 +1090,72 @@ public class DeptServiceImpl extends ServiceImpl implements ID
return deleteIds.size();
}
+ /**
+ * 索引 OA 公司:同时兼容类型迁移前后的公司记录
+ */
+ private Map indexOaCompanies(List oaDeptList) {
+ Map oaIdMap = new HashMap<>();
+ for (Dept dept : oaDeptList) {
+ if (!this.isOaCompany(dept) || StringUtil.isBlank(dept.getOaId())) {
+ continue;
+ }
+ Dept previous = oaIdMap.put(dept.getOaId(), dept);
+ if (previous != null) {
+ log.warn("OA公司主键重复,层级重建使用后一条 oaId={} id={} previousId={}",
+ dept.getOaId(), dept.getId(), previous.getId());
+ }
+ }
+ return oaIdMap;
+ }
+
+ private boolean isOaCompany(Dept dept) {
+ if (dept == null) {
+ return false;
+ }
+ if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
+ return true;
+ }
+ if (org.springblade.system.util.DeptCodeUtil.isCompanyCode(dept.getDeptCode())) {
+ return true;
+ }
+ return StringUtil.isNotBlank(dept.getDeptCode())
+ && dept.getDeptCode().startsWith(OAConvertConstant.COMPANY_OA_PREFIX)
+ && !OAConvertConstant.LEGACY_ROOT_DEPT_CODE.equals(dept.getDeptCode());
+ }
+
+ private boolean isOaDepartment(Dept dept) {
+ if (dept == null) {
+ return false;
+ }
+ if (DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())) {
+ return true;
+ }
+ if (StringUtil.isNotBlank(dept.getBelongCompanyCode())
+ && org.springblade.system.util.DeptCodeUtil.matchesChildCode(dept.getBelongCompanyCode(), dept.getDeptCode())) {
+ return true;
+ }
+ return StringUtil.isNotBlank(dept.getDeptCode())
+ && dept.getDeptCode().startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX);
+ }
+
+ /**
+ * 索引 OA 部门:兼容类型迁移前后
+ */
+ private Map indexOaDepartments(List oaDeptList) {
+ Map oaIdMap = new HashMap<>();
+ for (Dept dept : oaDeptList) {
+ if (!this.isOaDepartment(dept) || StringUtil.isBlank(dept.getOaId())) {
+ continue;
+ }
+ Dept previous = oaIdMap.put(dept.getOaId(), dept);
+ if (previous != null) {
+ log.warn("OA部门主键重复,层级重建使用后一条 oaId={} id={} previousId={}",
+ dept.getOaId(), dept.getId(), previous.getId());
+ }
+ }
+ return oaIdMap;
+ }
+
/**
* 按机构类型建立 OA 主键索引
*/
@@ -866,19 +1180,22 @@ public class DeptServiceImpl extends ServiceImpl implements ID
private OaParentLink resolveOaParentLink(Dept dept, Map companyByOaId, Map departmentByOaId) {
String parentOaId = dept.getOaSupSubComId();
boolean rootParent = StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId);
- if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
+ if (this.isOaCompany(dept)) {
if (rootParent) {
- return new OaParentLink(OAConvertConstant.ROOT_PARENT_ID, OAConstant.ROOT_COMPANY_ID);
+ // OA 顶级公司统一挂到本地顶级锚点下
+ return new OaParentLink(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
}
Dept parent = companyByOaId.get(parentOaId);
if (parent == null || parent.getId() == null || Objects.equals(parent.getId(), dept.getId())) {
+ // 全量整理后仍无上级:跳过重建,交由孤儿清理直接删除
log.warn("未找到上级公司,跳过层级重建 oaId={} oaSupSubComId={} deptId={}",
dept.getOaId(), parentOaId, dept.getId());
return null;
}
return new OaParentLink(parent.getId(), this.resolveParentCode(parent, OAConvertConstant.COMPANY_OA_PREFIX, parentOaId));
}
- if (!DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())) {
+ if (!DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())
+ && !(StringUtil.isNotBlank(dept.getDeptCode()) && dept.getDeptCode().startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX))) {
return null;
}
if (rootParent) {
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java
index 4c43c474..ab0d639a 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MeasurementUnitServiceImpl.java
@@ -62,10 +62,42 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl {
+ // 0. 确保双顶级:id=1 集团(GWWL) + 独立「外部组织」
+ deptService.ensureOaSyncRootDept();
// 1. 同步公司
this.syncCompany(queryStartTime);
- // 公司分批入库后,重建层级并清理无法挂载的公司
+ // 公司分批入库后:清理白名单外公司、重建层级并清理无法挂载的公司
+ this.removeOutsideWhitelistOrgs();
deptService.rebuildOaHierarchy();
deptService.removeOaOrphansWithoutParent();
// 2. 同步部门
this.syncDept(queryStartTime);
// 3. 分批写入完成后,按 OA 主键重建组织、部门层级
+ this.removeOutsideWhitelistOrgs();
deptService.rebuildOaHierarchy();
deptService.removeOaOrphansWithoutParent();
// 4. 更新部门祖级列表
@@ -290,6 +296,10 @@ public class OASyncServiceImpl implements IOASyncService {
private OaPersonSyncPageVO syncPersonFromOaPage(int current, int size, boolean incremental, String modifiedSince) {
int pageNo = current < 1 ? 1 : current;
int pageSize = size < 1 ? 20 : Math.min(size, 200);
+ // 全量同步首页开启原始数据收集,结束后按父级 id 写入 JSON
+ if (!incremental && pageNo == 1) {
+ oaSyncRawDumpHelper.beginFullDump();
+ }
IncrementalStart incrementalStart = resolvePersonIncrementalStart(incremental, modifiedSince);
OAPersonSearch personSearch = buildPersonSearch(incrementalStart.getStartTime());
personSearch.setCurPage(pageNo);
@@ -303,6 +313,9 @@ public class OASyncServiceImpl implements IOASyncService {
List oaPersons = responseData.getDataList() == null
? Collections.emptyList() : responseData.getDataList();
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
+ if (!incremental) {
+ oaSyncRawDumpHelper.collectPersons(oaPersons);
+ }
OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons);
OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex);
CacheUtil.clear(USER_CACHE);
@@ -320,6 +333,9 @@ public class OASyncServiceImpl implements IOASyncService {
|| oaPersons.size() < pageSize
|| (long) pageNo * pageSize >= totalSize;
pageVO.setFinished(finished);
+ if (!incremental && finished) {
+ pageVO.setDumpFilePath(oaSyncRawDumpHelper.finishFullDump());
+ }
ComposeLogUtil.getLastLog().info("OA人员分页同步完成 mode={} {}/{},成功{},跳过{}",
incremental ? "incremental" : "full", pageNo, totalSize,
personSyncCount.getSyncedCount(), personSyncCount.getSkippedCount());
@@ -338,6 +354,13 @@ public class OASyncServiceImpl implements IOASyncService {
private OaOrgSyncPageVO syncCompanyFromOaPage(int current, int size, boolean incremental, String modifiedSince) {
int pageNo = current < 1 ? 1 : current;
int pageSize = size < 1 ? 20 : Math.min(size, 200);
+ // 首屏确保双顶级:id=1 集团(GWWL) + 独立「外部组织」
+ if (pageNo == 1) {
+ deptService.ensureOaSyncRootDept();
+ if (!incremental) {
+ oaSyncRawDumpHelper.beginOrgFullDump();
+ }
+ }
IncrementalStart incrementalStart = resolveIncrementalStart(incremental, modifiedSince, DeptCategory.COMPANY);
OACompanySearch companySearch = buildCompanySearch(incrementalStart.getStartTime());
companySearch.setCurPage(pageNo);
@@ -351,6 +374,9 @@ public class OASyncServiceImpl implements IOASyncService {
List oaCompanies = responseData.getDataList() == null
? Collections.emptyList() : responseData.getDataList();
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
+ if (!incremental) {
+ oaSyncRawDumpHelper.collectCompanies(oaCompanies);
+ }
OrgSyncCount syncCount = handleCompany(oaCompanies, true);
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
ComposeLogUtil.getLastLog().warn("同步公司,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
@@ -358,7 +384,8 @@ public class OASyncServiceImpl implements IOASyncService {
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("company", pageNo, pageSize, totalSize, oaCompanies.size(), syncCount);
pageVO.setModifiedSince(incrementalStart.getModifiedSince());
if (Boolean.TRUE.equals(pageVO.getFinished())) {
- // 公司全部分批入库后:重建层级,并删除上级不存在的数据
+ // 公司全部分批入库后:清理白名单外公司、重建层级,并删除上级不存在的数据
+ this.removeOutsideWhitelistOrgs();
deptService.rebuildOaHierarchy();
deptService.removeOaOrphansWithoutParent();
}
@@ -387,6 +414,9 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.getLastLog().warn("未查询到已同步的OA公司,跳过部门同步");
OaOrgSyncPageVO emptyPageVO = buildOrgSyncPageVO("department", pageNo, pageSize, 0L, 0, OrgSyncCount.empty());
emptyPageVO.setFinished(true);
+ if (!incremental) {
+ emptyPageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump());
+ }
return emptyPageVO;
}
IncrementalStart incrementalStart = resolveIncrementalStart(incremental, modifiedSince, DeptCategory.DEPT);
@@ -402,6 +432,9 @@ public class OASyncServiceImpl implements IOASyncService {
List oaDepartments = responseData.getDataList() == null
? Collections.emptyList() : responseData.getDataList();
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
+ if (!incremental) {
+ oaSyncRawDumpHelper.collectDepartments(oaDepartments);
+ }
OrgSyncCount syncCount = handleDept(oaDepartments, true);
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
@@ -409,10 +442,14 @@ public class OASyncServiceImpl implements IOASyncService {
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("department", pageNo, pageSize, totalSize, oaDepartments.size(), syncCount);
pageVO.setModifiedSince(incrementalStart.getModifiedSince());
if (Boolean.TRUE.equals(pageVO.getFinished())) {
- // 部门分批同步完成后,重建层级、清理无法挂载数据并刷新祖级列表
+ // 部门分批同步完成后,清理白名单外组织、重建层级、清理无法挂载数据并刷新祖级列表
+ this.removeOutsideWhitelistOrgs();
deptService.rebuildOaHierarchy();
deptService.removeOaOrphansWithoutParent();
deptService.updateAncestors(null);
+ if (!incremental) {
+ pageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump());
+ }
}
CacheUtil.clear(SYS_CACHE);
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
@@ -437,10 +474,16 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.getLastLog().warn("解析回传 modifiedSince 失败:{},重新计算", modifiedSince);
}
}
+ String codePrefix = DeptCategory.COMPANY.equals(deptCategory)
+ ? OAConvertConstant.COMPANY_OA_PREFIX
+ : OAConvertConstant.DEPARTMENT_OA_PREFIX;
Dept latest = deptService.getOne(Wrappers.lambdaQuery()
.select(Dept::getOaModified)
.eq(Dept::getIsOa, YES)
- .eq(Dept::getDeptCategory, deptCategory.getCode())
+ .and(wrapper -> wrapper
+ .eq(Dept::getDeptCategory, deptCategory.getCode())
+ .or()
+ .likeRight(Dept::getDeptCode, codePrefix))
.isNotNull(Dept::getOaModified)
.ne(Dept::getOaModified, "")
.orderByDesc(Dept::getOaModified)
@@ -608,13 +651,9 @@ public class OASyncServiceImpl implements IOASyncService {
if (CollectionUtil.isEmpty(oaCompanies)) {
return OrgSyncCount.empty();
}
- // 获取需要的公司名称
Set companyNames = syncAll ? Collections.emptySet() : getCompanyNames();
- // 转换数据
List allParam = oaCompanies.stream()
- // 强制跳过上级公司为 0 的数据
- .filter(company -> !OAConstant.ROOT_COMPANY_ID.equals(company.getSupsubcomid()))
- // 非全量同步时,筛选只要配置的公司
+ .filter(this::isAllowedOaCompany)
.filter(company -> syncAll || companyNames.contains(company.getSubcompanyname()))
.map(deptConvert::company2dept)
.toList();
@@ -623,10 +662,41 @@ public class OASyncServiceImpl implements IOASyncService {
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
}
- // 查询已存在公司(含 oaModified),按编码索引
- Map existingDeptMap = loadExistingDeptMap(DeptCategory.COMPANY, null);
- Map deptIdByCode = toDeptIdMap(existingDeptMap);
- return persistSyncedDepts(allParam, existingDeptMap, deptIdByCode, DeptCategory.COMPANY, filteredSkipCount);
+ Map existingByOaId = loadExistingByOaId(DeptCategory.COMPANY);
+ Map codeOwnerMap = loadCompanyCodeOwnerMap();
+ Set usedCodes = new HashSet<>(codeOwnerMap.keySet());
+ Map batchByOaId = new LinkedHashMap<>();
+ for (Dept dept : allParam) {
+ this.assignCompanyCode(dept, existingByOaId, usedCodes);
+ batchByOaId.put(dept.getOaId(), dept);
+ }
+ // 上级公司编码:OA 上级为 0 → GWWL;否则取上级公司已分配编码
+ for (Dept dept : allParam) {
+ if (OAConstant.ROOT_COMPANY_ID.equals(dept.getOaSupSubComId()) || StringUtils.isBlank(dept.getOaSupSubComId())) {
+ dept.setParentCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
+ dept.setParentId(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
+ continue;
+ }
+ Dept parent = batchByOaId.get(dept.getOaSupSubComId());
+ if (parent == null) {
+ parent = existingByOaId.get(dept.getOaSupSubComId());
+ }
+ if (parent != null && StringUtils.isNotBlank(parent.getDeptCode())) {
+ dept.setParentCode(parent.getDeptCode());
+ dept.setParentId(parent.getId());
+ } else {
+ dept.setParentCode(null);
+ dept.setParentId(null);
+ }
+ }
+ Map deptIdByCode = new HashMap<>();
+ existingByOaId.values().forEach(dept -> {
+ if (StringUtils.isNotBlank(dept.getDeptCode()) && dept.getId() != null) {
+ deptIdByCode.put(dept.getDeptCode(), dept.getId());
+ }
+ });
+ deptIdByCode.put(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
+ return persistSyncedDepts(allParam, existingByOaId, deptIdByCode, DeptCategory.COMPANY, filteredSkipCount);
}
/**
@@ -639,103 +709,241 @@ public class OASyncServiceImpl implements IOASyncService {
if (CollectionUtil.isEmpty(oaDepts)) {
return OrgSyncCount.empty();
}
- // 查询所有公司的编码和id的map
- Map companyDeptMap = getAllCompanyDeptMap();
- // 根公司id
+ Map companyByOaId = loadCompanyByOaId();
+ Set allowedCompanyOaIds = resolveAllowedCompanyOaIds(companyByOaId);
String rootCompanyId = getRootCompanyId();
if (!syncAll && rootCompanyId == null) {
return new OrgSyncCount(0, oaDepts.size(), Collections.emptyList());
}
- // 根公司下要同步的部门名称
Set rootCompanyDeptNames = syncAll ? Collections.emptySet() : getRootCompanyDeptNames();
- // 转换数据
List allParam = oaDepts.stream()
- // 全量同步时不过滤;否则:部门的公司不是根公司,或者部门名称在根公司下需要同步的部门列表中,且是根部门
+ .filter(oaDept -> allowedCompanyOaIds.contains(oaDept.getSubcompanyid1()))
.filter(oaDept -> syncAll
|| rootCompanyId == null
|| !rootCompanyId.equals(oaDept.getSubcompanyid1())
|| (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentname())))
- .map(dept -> deptConvert.dept2dept(dept, companyDeptMap))
+ .map(dept -> deptConvert.dept2dept(dept, companyByOaId))
.toList();
int filteredSkipCount = oaDepts.size() - allParam.size();
if (CollectionUtil.isEmpty(allParam)) {
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
}
- Set deptCodes = new HashSet<>();
- allParam.forEach(dept -> {
- if (StringUtils.isNotEmpty(dept.getDeptCode())) {
- deptCodes.add(dept.getDeptCode());
+
+ Map existingByOaId = loadExistingByOaId(DeptCategory.DEPT);
+ Map nextSerialByCompany = new HashMap<>();
+ Map> usedCodesByCompany = new HashMap<>();
+ // 预载各公司下已有部门编码,保证分段连续
+ for (Dept existing : existingByOaId.values()) {
+ if (StringUtils.isBlank(existing.getBelongCompanyCode()) || StringUtils.isBlank(existing.getDeptCode())) {
+ continue;
}
- if (StringUtils.isNotEmpty(dept.getParentCode())) {
- deptCodes.add(dept.getParentCode());
+ usedCodesByCompany
+ .computeIfAbsent(existing.getBelongCompanyCode(), key -> new HashSet<>())
+ .add(existing.getDeptCode());
+ }
+ Map batchByOaId = new LinkedHashMap<>();
+ for (Dept dept : allParam) {
+ this.assignDepartmentCode(dept, existingByOaId, companyByOaId, usedCodesByCompany, nextSerialByCompany);
+ batchByOaId.put(dept.getOaId(), dept);
+ }
+ // 解析上级:根部门挂公司;子部门挂上级部门(本批或已存在)
+ Map deptIdByCode = new HashMap<>();
+ companyByOaId.values().forEach(company -> {
+ if (StringUtils.isNotBlank(company.getDeptCode()) && company.getId() != null) {
+ deptIdByCode.put(company.getDeptCode(), company.getId());
}
});
- // 查询数据库已有部门(含 oaModified)
- Map existingDeptMap = loadExistingDeptMap(DeptCategory.DEPT, deptCodes);
- Map deptIdByCode = toDeptIdMap(existingDeptMap);
- // 公司编码也放入 map,根部门挂到公司时才能解析父级
- companyDeptMap.forEach(deptIdByCode::putIfAbsent);
- return persistSyncedDepts(allParam, existingDeptMap, deptIdByCode, DeptCategory.DEPT, filteredSkipCount);
+ existingByOaId.values().forEach(dept -> {
+ if (StringUtils.isNotBlank(dept.getDeptCode()) && dept.getId() != null) {
+ deptIdByCode.put(dept.getDeptCode(), dept.getId());
+ }
+ });
+ for (Dept dept : allParam) {
+ if (dept.getParentId() != null && StringUtils.isNotBlank(dept.getParentCode())
+ && DeptCodeUtil.matchesChildCode(dept.getBelongCompanyCode(), dept.getDeptCode())
+ && dept.getBelongCompanyCode().equals(dept.getParentCode())) {
+ continue;
+ }
+ String parentOaId = dept.getOaSupSubComId();
+ Dept belongCompany = companyByOaId.values().stream()
+ .filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode()))
+ .findFirst()
+ .orElse(null);
+ if (belongCompany != null && (OAConstant.ROOT_COMPANY_ID.equals(parentOaId)
+ || StringUtils.equals(parentOaId, belongCompany.getOaId()))) {
+ dept.setParentCode(belongCompany.getDeptCode());
+ dept.setParentId(belongCompany.getId());
+ continue;
+ }
+ Dept parentDept = batchByOaId.get(parentOaId);
+ if (parentDept == null) {
+ parentDept = existingByOaId.get(parentOaId);
+ }
+ if (parentDept != null && StringUtils.isNotBlank(parentDept.getDeptCode())) {
+ dept.setParentCode(parentDept.getDeptCode());
+ dept.setParentId(parentDept.getId());
+ } else if (belongCompany != null) {
+ // 上级部门尚未就绪时先挂所属公司,分批结束后按 OA 主键重建
+ dept.setParentCode(belongCompany.getDeptCode());
+ dept.setParentId(belongCompany.getId());
+ } else {
+ dept.setParentCode(null);
+ dept.setParentId(null);
+ }
+ }
+ return persistSyncedDepts(allParam, existingByOaId, deptIdByCode, DeptCategory.DEPT, filteredSkipCount);
}
/**
- * 加载已存在组织,按部门编码索引
- *
- * @param deptCategory 组织类型
- * @param deptCodes 限定编码;为空则按类型全量加载
+ * 公司编码:GWWL+三位数字;已有合规编码则复用
*/
- private Map loadExistingDeptMap(DeptCategory deptCategory, Set deptCodes) {
- if (deptCodes != null && deptCodes.isEmpty()) {
- return new HashMap<>();
+ private void assignCompanyCode(Dept dept, Map existingByOaId, Set usedCodes) {
+ Dept existing = existingByOaId.get(dept.getOaId());
+ if (existing != null && DeptCodeUtil.isCompanyCode(existing.getDeptCode())) {
+ dept.setDeptCode(existing.getDeptCode());
+ dept.setBelongCompanyCode(existing.getDeptCode());
+ usedCodes.add(existing.getDeptCode());
+ return;
}
+ int serial = DeptCodeUtil.nextSerial(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, usedCodes);
+ String deptCode = DeptCodeUtil.formatChildCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, serial);
+ while (usedCodes.contains(deptCode)) {
+ serial++;
+ deptCode = DeptCodeUtil.formatChildCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, serial);
+ }
+ usedCodes.add(deptCode);
+ dept.setDeptCode(deptCode);
+ dept.setBelongCompanyCode(deptCode);
+ }
+
+ /**
+ * 部门编码:所属公司编码+三位数字;已有合规编码则复用
+ */
+ private void assignDepartmentCode(Dept dept, Map existingByOaId, Map companyByOaId,
+ Map> usedCodesByCompany, Map nextSerialByCompany) {
+ Dept existing = existingByOaId.get(dept.getOaId());
+ Dept belongCompany = null;
+ if (StringUtils.isNotBlank(dept.getBelongCompanyCode())) {
+ belongCompany = companyByOaId.values().stream()
+ .filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode())
+ || StringUtils.equals(company.getOaId(), dept.getBelongCompanyCode()))
+ .findFirst()
+ .orElse(null);
+ }
+ if (belongCompany == null && existing != null && StringUtils.isNotBlank(existing.getBelongCompanyCode())) {
+ String belongCode = existing.getBelongCompanyCode();
+ belongCompany = companyByOaId.values().stream()
+ .filter(company -> StringUtils.equals(company.getDeptCode(), belongCode))
+ .findFirst()
+ .orElse(null);
+ }
+ if (belongCompany == null) {
+ return;
+ }
+ final Dept resolvedCompany = belongCompany;
+ final String companyCode = resolvedCompany.getDeptCode();
+ dept.setBelongCompanyCode(companyCode);
+ if (existing != null && DeptCodeUtil.matchesChildCode(companyCode, existing.getDeptCode())) {
+ dept.setDeptCode(existing.getDeptCode());
+ usedCodesByCompany.computeIfAbsent(companyCode, key -> new HashSet<>()).add(existing.getDeptCode());
+ return;
+ }
+ Set usedCodes = usedCodesByCompany.computeIfAbsent(companyCode, key -> new HashSet<>());
+ int serial = nextSerialByCompany.computeIfAbsent(companyCode,
+ key -> DeptCodeUtil.nextSerial(companyCode, usedCodes));
+ String deptCode = DeptCodeUtil.formatChildCode(companyCode, serial);
+ while (usedCodes.contains(deptCode)) {
+ serial++;
+ deptCode = DeptCodeUtil.formatChildCode(companyCode, serial);
+ }
+ nextSerialByCompany.put(companyCode, serial + 1);
+ usedCodes.add(deptCode);
+ dept.setDeptCode(deptCode);
+ }
+
+ /**
+ * 按 oaId 加载已存在组织
+ */
+ private Map loadExistingByOaId(DeptCategory deptCategory) {
List deptList = deptService.list(Wrappers.lambdaQuery()
.eq(Dept::getDeptCategory, deptCategory.getCode())
- .in(deptCodes != null, Dept::getDeptCode, deptCodes)
- .select(Dept::getId, Dept::getDeptCode, Dept::getOaModified, Dept::getParentId));
- return deptList.stream()
- .filter(dept -> StringUtils.isNotEmpty(dept.getDeptCode()))
- .collect(Collectors.toMap(Dept::getDeptCode, dept -> dept, (a, b) -> b));
- }
-
- private Map toDeptIdMap(Map existingDeptMap) {
- Map deptIdByCode = new HashMap<>();
- existingDeptMap.forEach((code, dept) -> deptIdByCode.put(code, dept.getId()));
- return deptIdByCode;
+ .isNotNull(Dept::getOaId)
+ .ne(Dept::getOaId, "")
+ .select(Dept::getId, Dept::getDeptCode, Dept::getOaModified, Dept::getParentId,
+ Dept::getOaId, Dept::getBelongCompanyCode, Dept::getParentCode, Dept::getOaSupSubComId));
+ Map byOaId = new HashMap<>();
+ for (Dept dept : deptList) {
+ if (StringUtils.isNotBlank(dept.getOaId())) {
+ byOaId.put(dept.getOaId(), dept);
+ }
+ }
+ return byOaId;
}
/**
- * 持久化同步数据:不存在则新增;存在且 oaModified 不一致才修改
- *
- * @param allParam 待处理数据
- * @param existingDeptMap 已存在组织(同类型)
- * @param deptIdByCode 编码→id(含上级公司等)
- * @param deptCategory 组织类型
- * @param filteredSkipCount 上游过滤跳过数
- * @return 同步统计
+ * 已占用的公司编码(GWWLxxx)
*/
- private OrgSyncCount persistSyncedDepts(List allParam, Map existingDeptMap,
+ private Map loadCompanyCodeOwnerMap() {
+ List companyList = deptService.list(Wrappers.lambdaQuery()
+ .eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
+ .likeRight(Dept::getDeptCode, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE)
+ .select(Dept::getId, Dept::getDeptCode, Dept::getOaId));
+ Map codeMap = new HashMap<>();
+ for (Dept dept : companyList) {
+ if (DeptCodeUtil.isCompanyCode(dept.getDeptCode())) {
+ codeMap.put(dept.getDeptCode(), dept);
+ }
+ }
+ return codeMap;
+ }
+
+ /**
+ * 已同步公司:oaId → 公司
+ */
+ private Map loadCompanyByOaId() {
+ List companyList = deptService.list(Wrappers.lambdaQuery()
+ .eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
+ .isNotNull(Dept::getOaId)
+ .ne(Dept::getOaId, ""));
+ Map byOaId = new HashMap<>();
+ for (Dept dept : companyList) {
+ if (StringUtils.isNotBlank(dept.getOaId())) {
+ byOaId.put(dept.getOaId(), dept);
+ }
+ }
+ return byOaId;
+ }
+
+ /**
+ * 持久化同步数据:按 oaId 匹配;不存在则新增;存在且 oaModified 不一致才修改
+ */
+ private OrgSyncCount persistSyncedDepts(List allParam, Map existingByOaId,
Map deptIdByCode, DeptCategory deptCategory,
int filteredSkipCount) {
- Set existsDeptIds = existingDeptMap.values().stream()
+ Set existsDeptIds = existingByOaId.values().stream()
.map(Dept::getId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
allParam.forEach(dept -> {
- if (deptIdByCode.containsKey(dept.getDeptCode())) {
+ Dept existing = existingByOaId.get(dept.getOaId());
+ if (existing != null && existing.getId() != null) {
+ dept.setId(existing.getId());
+ } else if (StringUtils.isNotBlank(dept.getDeptCode()) && deptIdByCode.containsKey(dept.getDeptCode())) {
dept.setId(deptIdByCode.get(dept.getDeptCode()));
} else {
dept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
+ }
+ if (StringUtils.isNotBlank(dept.getDeptCode())) {
deptIdByCode.put(dept.getDeptCode(), dept.getId());
}
});
- // 重新把没有父id的数据设置一下父id;上级尚未入库时,新增记录先挂到根节点,全部分批完成后重建层级并清理无法挂载的数据
int unresolvedParentCount = 0;
for (Dept dept : allParam) {
if (dept.getParentId() != null) {
continue;
}
- Long parentId = deptIdByCode.get(dept.getParentCode());
+ Long parentId = StringUtils.isNotBlank(dept.getParentCode()) ? deptIdByCode.get(dept.getParentCode()) : null;
if (parentId != null) {
dept.setParentId(parentId);
continue;
@@ -750,8 +958,8 @@ public class OASyncServiceImpl implements IOASyncService {
deptCategory.getName(), unresolvedParentCount);
}
- // 父id不为空才新增;已存在的记录即使父id未解析也允许更新(空父id不会覆盖原值)
List list = allParam.stream()
+ .filter(dept -> StringUtils.isNotBlank(dept.getDeptCode()))
.filter(dept -> dept.getParentId() != null || existsDeptIds.contains(dept.getId()))
.toList();
List addList = list.stream()
@@ -761,10 +969,9 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.getLastLog().info("批量新增{}:{}", deptCategory.getName(), addList.size());
deptService.saveBatch(addList);
}
- // 已存在且 oaModified 不一致(或不存在可比对值)才修改
List updateList = list.stream()
.filter(dept -> existsDeptIds.contains(dept.getId()))
- .filter(dept -> !shouldSkipOaUpdate(existingDeptMap.get(dept.getDeptCode()), dept))
+ .filter(dept -> !shouldSkipOaUpdate(existingByOaId.get(dept.getOaId()), dept))
.toList();
if (CollectionUtil.isNotEmpty(updateList)) {
ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size());
@@ -772,10 +979,11 @@ public class OASyncServiceImpl implements IOASyncService {
}
int unchangedCount = (int) list.stream()
.filter(dept -> existsDeptIds.contains(dept.getId()))
- .filter(dept -> shouldSkipOaUpdate(existingDeptMap.get(dept.getDeptCode()), dept))
+ .filter(dept -> shouldSkipOaUpdate(existingByOaId.get(dept.getOaId()), dept))
.count();
List notHandledList = allParam.stream()
- .filter(dept -> dept.getParentId() == null && !existsDeptIds.contains(dept.getId()))
+ .filter(dept -> StringUtils.isBlank(dept.getDeptCode())
+ || (dept.getParentId() == null && !existsDeptIds.contains(dept.getId())))
.toList();
int syncedCount = addList.size() + updateList.size();
int skippedCount = filteredSkipCount + unchangedCount + notHandledList.size();
@@ -786,12 +994,17 @@ public class OASyncServiceImpl implements IOASyncService {
}
/**
- * oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段
+ * oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。
+ * 组织编码已按新规则重算且与库中不一致时,即使 modified 相同也要更新。
*/
private boolean shouldSkipOaUpdate(Dept existing, Dept incoming) {
if (existing == null) {
return false;
}
+ if (incoming != null && StringUtils.isNotBlank(incoming.getDeptCode())
+ && !StringUtils.equals(existing.getDeptCode(), incoming.getDeptCode())) {
+ return false;
+ }
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
if (localModified.isEmpty() || remoteModified.isEmpty()) {
@@ -831,6 +1044,106 @@ public class OASyncServiceImpl implements IOASyncService {
}
}
+ /**
+ * 是否允许同步的 OA 公司:二级白名单,或其上级为二级白名单(三级子公司)
+ */
+ private boolean isAllowedOaCompany(OACompanyResponse company) {
+ if (company == null || StringUtils.isBlank(company.getId())) {
+ return false;
+ }
+ if (OAConvertConstant.OA_SYNC_KEEP_SECOND_LEVEL_COMPANY_IDS.contains(company.getId())) {
+ return true;
+ }
+ return OAConvertConstant.OA_SYNC_KEEP_SECOND_LEVEL_COMPANY_IDS.contains(company.getSupsubcomid());
+ }
+
+ /**
+ * 解析允许同步部门的所属公司 OA 主键:二级白名单 ∪ 已同步的三级子公司
+ */
+ private Set resolveAllowedCompanyOaIds(Map companyByOaId) {
+ Set allowed = new HashSet<>(OAConvertConstant.OA_SYNC_KEEP_SECOND_LEVEL_COMPANY_IDS);
+ if (companyByOaId == null || companyByOaId.isEmpty()) {
+ return allowed;
+ }
+ for (Dept company : companyByOaId.values()) {
+ if (company == null || StringUtils.isBlank(company.getOaId())) {
+ continue;
+ }
+ if (OAConvertConstant.OA_SYNC_KEEP_SECOND_LEVEL_COMPANY_IDS.contains(company.getOaId())
+ || OAConvertConstant.OA_SYNC_KEEP_SECOND_LEVEL_COMPANY_IDS.contains(company.getOaSupSubComId())) {
+ allowed.add(company.getOaId());
+ }
+ }
+ return allowed;
+ }
+
+ /**
+ * 清理不在白名单树内的 OA 公司及其子树(含部门)
+ */
+ private int removeOutsideWhitelistOrgs() {
+ List oaCompanyList = deptService.list(Wrappers.lambdaQuery()
+ .eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
+ .isNotNull(Dept::getOaId)
+ .ne(Dept::getOaId, ""));
+ Map companyByOaId = new HashMap<>();
+ for (Dept company : oaCompanyList) {
+ if (StringUtils.isNotBlank(company.getOaId())) {
+ companyByOaId.put(company.getOaId(), company);
+ }
+ }
+ Set allowedCompanyOaIds = resolveAllowedCompanyOaIds(companyByOaId);
+ Set deleteIds = new HashSet<>();
+ for (Dept company : oaCompanyList) {
+ if (!allowedCompanyOaIds.contains(company.getOaId())) {
+ deleteIds.add(company.getId());
+ }
+ }
+ Map companyCodeToOaId = new HashMap<>();
+ for (Dept company : oaCompanyList) {
+ if (StringUtils.isNotBlank(company.getDeptCode()) && StringUtils.isNotBlank(company.getOaId())) {
+ companyCodeToOaId.put(company.getDeptCode(), company.getOaId());
+ }
+ }
+ List oaDeptList = deptService.list(Wrappers.lambdaQuery()
+ .eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
+ .isNotNull(Dept::getOaId)
+ .ne(Dept::getOaId, ""));
+ for (Dept dept : oaDeptList) {
+ String belongOaId = null;
+ if (StringUtils.isNotBlank(dept.getBelongCompanyCode())) {
+ belongOaId = companyCodeToOaId.get(dept.getBelongCompanyCode());
+ }
+ if (StringUtils.isBlank(belongOaId) && StringUtils.isNotBlank(dept.getOaSupSubComId())
+ && companyByOaId.containsKey(dept.getOaSupSubComId())) {
+ // 根部门上级可能直接是公司 OA 主键
+ belongOaId = dept.getOaSupSubComId();
+ }
+ if (StringUtils.isBlank(belongOaId) || !allowedCompanyOaIds.contains(belongOaId)) {
+ deleteIds.add(dept.getId());
+ }
+ }
+ if (CollectionUtil.isEmpty(deleteIds)) {
+ return 0;
+ }
+ List allDeptList = deptService.list();
+ boolean cascadeChanged = true;
+ while (cascadeChanged) {
+ cascadeChanged = false;
+ for (Dept dept : allDeptList) {
+ if (deleteIds.contains(dept.getId())) {
+ continue;
+ }
+ if (dept.getParentId() != null && deleteIds.contains(dept.getParentId())) {
+ deleteIds.add(dept.getId());
+ cascadeChanged = true;
+ }
+ }
+ }
+ deptService.removeByIds(deleteIds);
+ ComposeLogUtil.getLastLog().info("清理白名单外 OA 组织{}条", deleteIds.size());
+ return deleteIds.size();
+ }
+
/**
* 获取 OA 部门查询使用的子公司 id
*
@@ -842,6 +1155,8 @@ public class OASyncServiceImpl implements IOASyncService {
if (syncAll) {
companyList = deptService.list(Wrappers.lambdaQuery()
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
+ .isNotNull(Dept::getOaId)
+ .ne(Dept::getOaId, "")
);
} else {
Set companyNames = this.getCompanyNames();
@@ -851,11 +1166,22 @@ public class OASyncServiceImpl implements IOASyncService {
companyList = deptService.list(Wrappers.lambdaQuery()
.in(Dept::getDeptName, companyNames)
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
+ .isNotNull(Dept::getOaId)
+ .ne(Dept::getOaId, "")
);
}
+ Map companyByOaId = new HashMap<>();
+ for (Dept company : companyList) {
+ String oaId = this.resolveOaCompanyId(company);
+ if (StringUtils.isNotBlank(oaId)) {
+ companyByOaId.put(oaId, company);
+ }
+ }
+ Set allowedCompanyOaIds = resolveAllowedCompanyOaIds(companyByOaId);
return companyList.stream()
.map(this::resolveOaCompanyId)
.filter(StringUtils::isNotBlank)
+ .filter(allowedCompanyOaIds::contains)
.distinct()
.collect(Collectors.joining(","));
}
@@ -951,13 +1277,20 @@ public class OASyncServiceImpl implements IOASyncService {
return Collections.emptySet();
}
// 查询父部门编码是根公司的,部门类型的,根公司下部门名称的部门
+ Dept rootCompany = deptService.getOne(Wrappers.lambdaQuery()
+ .eq(Dept::getOaId, rootCompanyId)
+ .eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
+ .last("limit 1"), false);
+ if (rootCompany == null || StringUtils.isBlank(rootCompany.getDeptCode())) {
+ return Collections.emptySet();
+ }
return deptService.list(Wrappers.lambdaQuery()
.in(Dept::getDeptName, rootCompanyDeptNames)
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
- .eq(Dept::getParentCode, OAConvertConstant.COMPANY_OA_PREFIX + rootCompanyId)
+ .eq(Dept::getParentCode, rootCompany.getDeptCode())
).stream()
- .filter(dept -> StringUtils.isNotBlank(dept.getDeptCode()))
- .map(dept -> dept.getDeptCode().replace(OAConvertConstant.DEPARTMENT_OA_PREFIX, ""))
+ .map(Dept::getOaId)
+ .filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
}
@@ -980,11 +1313,11 @@ public class OASyncServiceImpl implements IOASyncService {
ComposeLogUtil.getLastLog().warn("未查询到根公司 名称:{}", companyName);
return null;
}
- if (StringUtils.isBlank(dept.getDeptCode())) {
+ if (StringUtils.isBlank(dept.getOaId()) && StringUtils.isBlank(dept.getDeptCode())) {
ComposeLogUtil.getLastLog().warn("根公司编码为空:{}", companyName);
return null;
}
- // 部门编码去掉前缀,就是oa的id
- return dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, "");
+ return StringUtils.isNotBlank(dept.getOaId()) ? dept.getOaId()
+ : dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, "");
}
}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaSyncRawDumpHelper.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaSyncRawDumpHelper.java
new file mode 100644
index 00000000..0674f00f
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaSyncRawDumpHelper.java
@@ -0,0 +1,379 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONWriter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springblade.system.log.ComposeLogUtil;
+import org.springblade.thirdparty.oa.feign.IOAClient;
+import org.springblade.thirdparty.oa.pojo.response.OACompanyResponse;
+import org.springblade.thirdparty.oa.pojo.response.OADepartmentResponse;
+import org.springblade.thirdparty.oa.pojo.response.OAPersonResponse;
+import org.springblade.thirdparty.oa.pojo.search.OACompanySearch;
+import org.springblade.thirdparty.oa.pojo.search.OADepartmentSearch;
+import org.springblade.thirdparty.oa.pojo.search.OASearch;
+import org.springblade.thirdparty.oa.util.OAUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * OA 全量同步原始数据落盘:按父级 id 组装层级 JSON
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class OaSyncRawDumpHelper {
+
+ private static final String ORPHAN_COMPANY_ID = "_orphan_company";
+ private static final String ORPHAN_DEPT_ID = "_orphan_dept";
+
+ private final IOAClient oaClient;
+
+ @Value("${third-party.oa.sync-dump-dir:./data/oa-sync}")
+ private String syncDumpDir;
+
+ /**
+ * 当前全量同步会话收集到的人员原始数据
+ */
+ private final List collectingPersons = Collections.synchronizedList(new ArrayList<>());
+ private volatile boolean collecting;
+
+ /**
+ * 机构全量同步会话收集的公司/部门原始数据
+ */
+ private final List collectingCompanies = Collections.synchronizedList(new ArrayList<>());
+ private final List collectingDepartments = Collections.synchronizedList(new ArrayList<>());
+ private volatile boolean collectingOrg;
+
+ /**
+ * 全量同步首页启动收集
+ */
+ public synchronized void beginFullDump() {
+ collectingPersons.clear();
+ collecting = true;
+ ComposeLogUtil.getLastLog().info("已开启 OA 全量同步原始数据收集,目录={}", resolveDumpDir());
+ }
+
+ /**
+ * 机构全量同步首页启动收集公司/部门原始数据
+ */
+ public synchronized void beginOrgFullDump() {
+ collectingCompanies.clear();
+ collectingDepartments.clear();
+ collectingOrg = true;
+ ComposeLogUtil.getLastLog().info("已开启 OA 机构全量同步原始数据收集,目录={}", resolveDumpDir());
+ }
+
+ /**
+ * 收集本页人员原始数据
+ */
+ public void collectPersons(List oaPersons) {
+ if (!collecting || oaPersons == null || oaPersons.isEmpty()) {
+ return;
+ }
+ collectingPersons.addAll(oaPersons);
+ }
+
+ /**
+ * 收集本页公司原始数据
+ */
+ public void collectCompanies(List oaCompanies) {
+ if (!collectingOrg || oaCompanies == null || oaCompanies.isEmpty()) {
+ return;
+ }
+ collectingCompanies.addAll(oaCompanies);
+ }
+
+ /**
+ * 收集本页部门原始数据
+ */
+ public void collectDepartments(List oaDepartments) {
+ if (!collectingOrg || oaDepartments == null || oaDepartments.isEmpty()) {
+ return;
+ }
+ collectingDepartments.addAll(oaDepartments);
+ }
+
+ /**
+ * 全量同步结束:先写入人员层级 JSON,再异步补齐公司/部门接口原始层级数据
+ *
+ * @return 人员原始层级 JSON 文件路径,失败时返回 null
+ */
+ public synchronized String finishFullDump() {
+ if (!collecting) {
+ return null;
+ }
+ List persons = new ArrayList<>(collectingPersons);
+ collecting = false;
+ collectingPersons.clear();
+ try {
+ String stamp = DateUtil.format(new Date(), "yyyyMMddHHmmss");
+ Map personPayload = new LinkedHashMap<>();
+ personPayload.put("exportTime", DateUtil.format(new Date(), DateUtil.PATTERN_DATETIME));
+ personPayload.put("personCount", persons.size());
+ personPayload.put("personTree", buildPersonTree(persons));
+ personPayload.put("personsRaw", persons);
+ Path personFile = writeDumpFile("oa-full-sync-person-raw-" + stamp + ".json", personPayload);
+ ComposeLogUtil.getLastLog().info("OA 人员原始层级数据已写入 {}", personFile.toAbsolutePath());
+ CompletableFuture.runAsync(() -> dumpOrgRawAsync(stamp));
+ return personFile.toAbsolutePath().toString();
+ } catch (Exception exception) {
+ log.error("写入 OA 全量同步人员原始 JSON 失败", exception);
+ ComposeLogUtil.getLastLog().error("写入 OA 全量同步人员原始 JSON 失败:{}", exception.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * 机构全量同步结束:写入公司/部门接口原始数据(按父级 id 层级)
+ *
+ * @return JSON 文件路径,失败时返回 null
+ */
+ public synchronized String finishOrgFullDump() {
+ if (!collectingOrg) {
+ return null;
+ }
+ List companies = new ArrayList<>(collectingCompanies);
+ List departments = new ArrayList<>(collectingDepartments);
+ collectingOrg = false;
+ collectingCompanies.clear();
+ collectingDepartments.clear();
+ try {
+ String stamp = DateUtil.format(new Date(), "yyyyMMddHHmmss");
+ Path orgFile = writeOrgDumpFile(stamp, companies, departments);
+ ComposeLogUtil.getLastLog().info("OA 机构原始层级数据已写入 {}", orgFile.toAbsolutePath());
+ return orgFile.toAbsolutePath().toString();
+ } catch (Exception exception) {
+ log.error("写入 OA 机构全量同步原始 JSON 失败", exception);
+ ComposeLogUtil.getLastLog().error("写入 OA 机构全量同步原始 JSON 失败:{}", exception.getMessage());
+ return null;
+ }
+ }
+
+ private void dumpOrgRawAsync(String stamp) {
+ try {
+ List companies = fetchAllCompanies();
+ List departments = fetchAllDepartments(companies);
+ Path orgFile = writeOrgDumpFile(stamp, companies, departments);
+ log.info("OA 公司/部门原始层级数据已写入 {}", orgFile.toAbsolutePath());
+ } catch (Exception exception) {
+ log.error("异步写入 OA 公司/部门原始 JSON 失败", exception);
+ }
+ }
+
+ private Path writeOrgDumpFile(String stamp, List companies,
+ List departments) throws IOException {
+ Map orgPayload = new LinkedHashMap<>();
+ orgPayload.put("exportTime", DateUtil.format(new Date(), DateUtil.PATTERN_DATETIME));
+ orgPayload.put("companyCount", companies.size());
+ orgPayload.put("departmentCount", departments.size());
+ orgPayload.put("companyTree", buildCompanyTree(companies));
+ orgPayload.put("departmentTree", buildDepartmentTree(departments));
+ orgPayload.put("companiesRaw", companies);
+ orgPayload.put("departmentsRaw", departments);
+ return writeDumpFile("oa-full-sync-org-raw-" + stamp + ".json", orgPayload);
+ }
+
+ private List fetchAllCompanies() {
+ List companies = new ArrayList<>();
+ OACompanySearch companySearch = new OACompanySearch();
+ companySearch.setCurPage(1);
+ companySearch.setPageSize(100);
+ OAUtils.pageSyncHandler(companySearch, param -> oaClient.queryCompanyPage(new OASearch<>(param)), response -> {
+ log.error("导出公司原始数据失败 {}", JSON.toJSONString(response));
+ return new ServiceException("导出公司原始数据失败");
+ }, 10000, log::info).accept(companies::addAll);
+ return companies;
+ }
+
+ private List fetchAllDepartments(List companies) {
+ List departments = new ArrayList<>();
+ String subCompanyIds = companies.stream()
+ .map(OACompanyResponse::getId)
+ .filter(StringUtils::isNotBlank)
+ .distinct()
+ .reduce((left, right) -> left + "," + right)
+ .orElse("");
+ OADepartmentSearch departmentSearch = new OADepartmentSearch();
+ departmentSearch.setCurPage(1);
+ departmentSearch.setPageSize(100);
+ departmentSearch.setSubcompanyid1(subCompanyIds);
+ OAUtils.pageSyncHandler(departmentSearch, param -> oaClient.queryDepartmentPage(new OASearch<>(param)), response -> {
+ log.error("导出部门原始数据失败 {}", JSON.toJSONString(response));
+ return new ServiceException("导出部门原始数据失败");
+ }, 10000, log::info).accept(departments::addAll);
+ return departments;
+ }
+
+ private List> buildCompanyTree(List companies) {
+ Map> nodeMap = new LinkedHashMap<>();
+ for (OACompanyResponse company : companies) {
+ if (company == null || StringUtils.isBlank(company.getId())) {
+ continue;
+ }
+ Map node = new LinkedHashMap<>();
+ node.put("id", company.getId());
+ node.put("parentId", blankToRoot(company.getSupsubcomid()));
+ node.put("type", "company");
+ node.put("name", company.getSubcompanyname());
+ node.put("raw", company);
+ node.put("children", new ArrayList>());
+ nodeMap.put(company.getId(), node);
+ }
+ return linkChildren(nodeMap);
+ }
+
+ private List> buildDepartmentTree(List departments) {
+ Map> nodeMap = new LinkedHashMap<>();
+ for (OADepartmentResponse department : departments) {
+ if (department == null || StringUtils.isBlank(department.getId())) {
+ continue;
+ }
+ Map node = new LinkedHashMap<>();
+ node.put("id", department.getId());
+ node.put("parentId", blankToRoot(department.getSupdepid()));
+ node.put("companyId", department.getSubcompanyid1());
+ node.put("type", "department");
+ node.put("name", department.getDepartmentname());
+ node.put("raw", department);
+ node.put("children", new ArrayList>());
+ nodeMap.put(department.getId(), node);
+ }
+ return linkChildren(nodeMap);
+ }
+
+ private List> buildPersonTree(List persons) {
+ Map> companyMap = new LinkedHashMap<>();
+ Map> deptMap = new LinkedHashMap<>();
+ for (OAPersonResponse person : persons) {
+ if (person == null) {
+ continue;
+ }
+ String companyId = StringUtils.defaultIfBlank(person.getSubcompanyid1(), ORPHAN_COMPANY_ID);
+ String deptId = StringUtils.defaultIfBlank(person.getDepartmentid(), ORPHAN_DEPT_ID + "@" + companyId);
+ Map companyNode = companyMap.computeIfAbsent(companyId, key -> {
+ Map node = new LinkedHashMap<>();
+ node.put("id", key);
+ node.put("parentId", "0");
+ node.put("type", "company");
+ node.put("name", StringUtils.defaultIfBlank(person.getSubcompanyname(), key));
+ Map raw = new LinkedHashMap<>();
+ raw.put("subcompanyid1", person.getSubcompanyid1());
+ raw.put("subcompanyname", person.getSubcompanyname());
+ raw.put("subcompanycode", person.getSubcompanycode());
+ node.put("raw", raw);
+ node.put("children", new ArrayList>());
+ return node;
+ });
+ @SuppressWarnings("unchecked")
+ List> companyChildren = (List>) companyNode.get("children");
+ Map deptNode = deptMap.computeIfAbsent(deptId, key -> {
+ Map node = new LinkedHashMap<>();
+ node.put("id", key);
+ node.put("parentId", companyId);
+ node.put("type", "department");
+ node.put("name", StringUtils.defaultIfBlank(person.getDepartmentname(), key));
+ Map raw = new LinkedHashMap<>();
+ raw.put("departmentid", person.getDepartmentid());
+ raw.put("departmentname", person.getDepartmentname());
+ raw.put("departmentcode", person.getDepartmentcode());
+ raw.put("subcompanyid1", person.getSubcompanyid1());
+ node.put("raw", raw);
+ node.put("children", new ArrayList>());
+ companyChildren.add(node);
+ return node;
+ });
+ @SuppressWarnings("unchecked")
+ List> deptChildren = (List>) deptNode.get("children");
+ Map personNode = new LinkedHashMap<>();
+ personNode.put("id", StringUtils.defaultIfBlank(person.getId(), person.getLoginid()));
+ personNode.put("parentId", deptId);
+ personNode.put("type", "person");
+ personNode.put("name", person.getLastname());
+ personNode.put("raw", person);
+ personNode.put("children", Collections.emptyList());
+ deptChildren.add(personNode);
+ }
+ return new ArrayList<>(companyMap.values());
+ }
+
+ @SuppressWarnings("unchecked")
+ private List> linkChildren(Map> nodeMap) {
+ List> roots = new ArrayList<>();
+ for (Map node : nodeMap.values()) {
+ String parentId = Objects.toString(node.get("parentId"), "0");
+ Map parent = nodeMap.get(parentId);
+ if (parent != null && !Objects.equals(parent.get("id"), node.get("id"))) {
+ ((List>) parent.get("children")).add(node);
+ } else {
+ roots.add(node);
+ }
+ }
+ return roots;
+ }
+
+ private String blankToRoot(String parentId) {
+ if (StringUtils.isBlank(parentId) || "0".equals(parentId) || "-1".equals(parentId)) {
+ return "0";
+ }
+ return parentId;
+ }
+
+ private Path writeDumpFile(String fileName, Map payload) throws IOException {
+ Path dir = resolveDumpDir();
+ Files.createDirectories(dir);
+ Path dumpFile = dir.resolve(fileName);
+ String json = JSON.toJSONString(payload, JSONWriter.Feature.PrettyFormat, JSONWriter.Feature.WriteMapNullValue);
+ Files.writeString(dumpFile, json, StandardCharsets.UTF_8);
+ return dumpFile;
+ }
+
+ private Path resolveDumpDir() {
+ return Paths.get(StringUtils.defaultIfBlank(syncDumpDir, "./data/oa-sync")).toAbsolutePath().normalize();
+ }
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java
index eba6b8cd..77ba09d0 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OaUserListSyncHelper.java
@@ -86,7 +86,7 @@ public class OaUserListSyncHelper {
private final IRoleService roleService;
/**
- * 从人员数据提取二级公司、三级部门,并挂到「桂物物流集团」下
+ * 从人员数据提取二级公司、三级部门,并挂到集团顶级(id=1/GWWL) 下
*
* @param oaPersons OA人员
* @return 组织索引
@@ -350,7 +350,20 @@ public class OaUserListSyncHelper {
String defaultRoleId = getDefaultRoleId();
List updateUserParams = list.stream()
.filter(userDeptIds -> !unchangedUserIds.contains(userDeptIds.getUserId()))
- .filter(userDeptIds -> StringUtils.isNotBlank(userDeptIds.getDeptIds()) && userDeptIds.getDeptIds().length() <= MAX_DEPT_ID_LENGTH)
+ .filter(userDeptIds -> {
+ if (StringUtils.isBlank(userDeptIds.getDeptIds())) {
+ return false;
+ }
+ int deptIdLength = userDeptIds.getDeptIds().length();
+ int deptCodesLength = StringUtils.length(userDeptIds.getDeptCodes());
+ if (deptIdLength > MAX_DEPT_ID_LENGTH || deptCodesLength > MAX_DEPT_ID_LENGTH) {
+ ComposeLogUtil.getLastLog().warn(
+ "用户部门字段超长已跳过回写 userId={} deptIdLen={} deptCodesLen={} max={}",
+ userDeptIds.getUserId(), deptIdLength, deptCodesLength, MAX_DEPT_ID_LENGTH);
+ return false;
+ }
+ return true;
+ })
.map(userDeptIds -> {
User user = new User();
user.setId(userDeptIds.getUserId());
@@ -383,33 +396,7 @@ public class OaUserListSyncHelper {
}
private Dept getOrCreateRootCompany() {
- Dept rootCompany = deptService.getOne(Wrappers.lambdaQuery()
- .eq(Dept::getDeptName, OAConvertConstant.ROOT_COMPANY_NAME)
- .last("limit 1"), false);
- if (rootCompany != null) {
- return rootCompany;
- }
- rootCompany = new Dept();
- rootCompany.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
- rootCompany.setTenantId(resolveTenantId());
- rootCompany.setParentId(BladeConstant.TOP_PARENT_ID);
- rootCompany.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
- rootCompany.setDeptName(OAConvertConstant.ROOT_COMPANY_NAME);
- rootCompany.setFullName(OAConvertConstant.ROOT_COMPANY_NAME);
- rootCompany.setShortName(OAConvertConstant.ROOT_COMPANY_NAME);
- rootCompany.setDeptCode("OACROOT");
- rootCompany.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
- rootCompany.setBelongCompanyCode("OACROOT");
- rootCompany.setDeptCategory(DeptCategory.COMPANY.getCode());
- rootCompany.setSort(0);
- rootCompany.setStatus(DataStatusEnum.ENABLE.getCode());
- rootCompany.setIsDeleted(BladeConstant.DB_NOT_DELETED);
- rootCompany.setIsOa(YES);
- rootCompany.setIsPlatformCompany(0);
- rootCompany.setSyncTime(new Date());
- deptService.save(rootCompany);
- ComposeLogUtil.getLastLog().info("已创建顶级组织:{}", OAConvertConstant.ROOT_COMPANY_NAME);
- return rootCompany;
+ return deptService.ensureOaSyncRootDept();
}
private Dept upsertOrg(String name, String oaCode, Dept existing, Dept parent, String tenantId,
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserDeptServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserDeptServiceImpl.java
index 88b17735..6fc40f70 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserDeptServiceImpl.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserDeptServiceImpl.java
@@ -46,11 +46,18 @@ import java.util.List;
@Service
public class UserDeptServiceImpl extends ServiceImpl implements IUserDeptService {
+ /**
+ * 与 blade_user.dept_id / dept_codes 扩容后的字段上限一致
+ */
+ private static final int GROUP_CONCAT_MAX_LEN = 2000;
+
@Override
public List queryUserDeptIds(Collection userIds) {
if (CollectionUtil.isEmpty(userIds)) {
return Collections.emptyList();
}
+ // 需与后续查询在同一连接中生效(外层事务内调用)
+ baseMapper.setGroupConcatMaxLen(GROUP_CONCAT_MAX_LEN);
return baseMapper.queryUserDeptIds(userIds);
}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java
index 3a463c4d..055daa42 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java
@@ -1206,9 +1206,36 @@ public class UserServiceImpl extends BaseServiceImpl implement
if (user == null) {
return;
}
- if (user.getPhone() != null && user.getPhone().length() > 20) {
- user.setPhone(SM2Util.decrypt(user.getPhone(), properties.getPublicKey(), properties.getPrivateKey()));
+ String phone = user.getPhone();
+ // 密文 Hex 通常远长于普通手机号;含 / 等非 Hex 字符的明文(如双号)直接跳过
+ if (StringUtil.isBlank(phone) || phone.length() <= 20 || !isSm2HexCipher(phone)) {
+ return;
}
+ try {
+ user.setPhone(SM2Util.decrypt(phone, properties.getPublicKey(), properties.getPrivateKey()));
+ } catch (Exception exception) {
+ log.warn("手机号 SM2 解密失败,保留原值 length={}", phone.length(), exception);
+ }
+ }
+
+ /**
+ * 判断是否为 SM2 密文 Hex(仅 0-9a-f,且偶数长度)
+ */
+ private boolean isSm2HexCipher(String value) {
+ int length = value.length();
+ if ((length & 1) != 0) {
+ return false;
+ }
+ for (int index = 0; index < length; index++) {
+ char character = value.charAt(index);
+ boolean isDigit = character >= '0' && character <= '9';
+ boolean isLowerHex = character >= 'a' && character <= 'f';
+ boolean isUpperHex = character >= 'A' && character <= 'F';
+ if (!isDigit && !isLowerHex && !isUpperHex) {
+ return false;
+ }
+ }
+ return true;
}
@Override
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/util/DeptCodeUtil.java b/blade-service/blade-system/src/main/java/org/springblade/system/util/DeptCodeUtil.java
new file mode 100644
index 00000000..687a3a46
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/util/DeptCodeUtil.java
@@ -0,0 +1,97 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springblade.thirdparty.oa.constant.OAConvertConstant;
+
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 组织编码工具:上级编码直接拼接固定 3 位分段数字(不足补零,中间不加 "-")
+ *
+ * @author Chill
+ */
+public final class DeptCodeUtil {
+
+ /**
+ * 分段数字位数
+ */
+ public static final int SEGMENT_WIDTH = 3;
+
+ private DeptCodeUtil() {
+ }
+
+ /**
+ * 生成子级编码:上级编码 + 三位数字
+ */
+ public static String formatChildCode(String parentCode, int serial) {
+ return parentCode + String.format("%0" + SEGMENT_WIDTH + "d", serial);
+ }
+
+ /**
+ * 是否符合「上级编码 + 三位数字」格式(恰好多出 3 位数字)
+ */
+ public static boolean matchesChildCode(String parentCode, String deptCode) {
+ if (StringUtils.isBlank(parentCode) || StringUtils.isBlank(deptCode)) {
+ return false;
+ }
+ return deptCode.matches("^" + Pattern.quote(parentCode) + "\\d{" + SEGMENT_WIDTH + "}$");
+ }
+
+ /**
+ * 是否为公司编码(GWWL + 三位数字)
+ */
+ public static boolean isCompanyCode(String deptCode) {
+ return matchesChildCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, deptCode);
+ }
+
+ /**
+ * 在已有编码中取下一分段序号(从 1 起)
+ */
+ public static int nextSerial(String parentCode, Collection