Merge remote-tracking branch 'origin/feature/20261019-bfi-all' into feature/hcs-api-p0
# Conflicts: # blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
This commit is contained in:
@@ -36,3 +36,5 @@ Thumbs.db
|
||||
/.codex-work/
|
||||
.serena/
|
||||
.workbuddy/
|
||||
/data/
|
||||
/output/
|
||||
|
||||
+8
-7
@@ -84,16 +84,11 @@ public class BladeAuthorizationHandler extends AbstractAuthorizationHandler {
|
||||
if (bladeProperties.isProd() && isWeakPassword(request.getPassword())) {
|
||||
return buildValidationFailure(ExceptionCode.INVALID_USER_PASSWORD);
|
||||
}
|
||||
// 判断账号是否锁定
|
||||
// 仅按账号锁定;不做 IP 锁定,避免同出口/网关 IP 下一人失败牵连全部未登录用户
|
||||
OAuth2Validation accountValidation = lockHandler.validateAccountLock(request.getTenantId(), request.getUsername());
|
||||
if (!accountValidation.isSuccess()) {
|
||||
return accountValidation;
|
||||
}
|
||||
// 判断IP是否锁定
|
||||
OAuth2Validation ipValidation = lockHandler.validateIpLock(request.getTenantId());
|
||||
if (!ipValidation.isSuccess()) {
|
||||
return ipValidation;
|
||||
}
|
||||
}
|
||||
return super.preValidation(request);
|
||||
}
|
||||
@@ -105,7 +100,13 @@ public class BladeAuthorizationHandler extends AbstractAuthorizationHandler {
|
||||
*/
|
||||
@Override
|
||||
public void preFailure(OAuth2Request request, OAuth2Validation validation) {
|
||||
// 处理认证失败,增加错误次数
|
||||
// 已因锁定失败时不再累加失败次数,避免其他账号重试时被误计并连带锁定
|
||||
if (validation != null && !validation.isSuccess()
|
||||
&& ExceptionCode.USER_TOO_MANY_FAILS.getCode() == validation.getCode()) {
|
||||
log.error("用户:{},认证失败,失败原因:{}", request.getUsername(), validation.getMessage());
|
||||
return;
|
||||
}
|
||||
// 处理认证失败,增加错误次数(仅账号维度)
|
||||
lockHandler.handleAuthFailure(request.getTenantId(), request.getUsername());
|
||||
|
||||
log.error("用户:{},认证失败,失败原因:{}", request.getUsername(), validation.getMessage());
|
||||
|
||||
@@ -90,7 +90,7 @@ public class BladeLockHandler {
|
||||
|
||||
/**
|
||||
* 处理认证失败
|
||||
* 同时增加账号和IP错误次数
|
||||
* 仅增加账号错误次数;不再累计 IP 失败次数,避免共享出口 IP 误伤其他用户。
|
||||
*
|
||||
* @param tenantId 租户id
|
||||
* @param account 账号
|
||||
@@ -102,7 +102,6 @@ public class BladeLockHandler {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
authLockClient.addAccountFailCount(tenantId, account, userId, ip, userAgent);
|
||||
authLockClient.addIpFailCount(tenantId, ip, userAgent);
|
||||
} catch (Exception e) {
|
||||
log.warn("认证失败计数异常: {}", e.getMessage());
|
||||
}
|
||||
|
||||
+3
-2
@@ -309,12 +309,13 @@ public interface ISysClient {
|
||||
R<Region> getRegion(@RequestParam("code") String code);
|
||||
|
||||
/**
|
||||
* 获取启用的费用项
|
||||
* 获取费用项。默认仅返回启用档案;includeDisabled 为 true 时同时返回停用档案。
|
||||
*
|
||||
* @param includeDisabled 是否包含停用费用项
|
||||
* @return 费用项集合
|
||||
*/
|
||||
@GetMapping(FEE_ITEMS)
|
||||
R<List<FeeItem>> getFeeItems();
|
||||
R<List<FeeItem>> getFeeItems(@RequestParam(value = "includeDisabled", required = false) Boolean includeDisabled);
|
||||
|
||||
@GetMapping(CARGO_TYPES)
|
||||
R<List<CargoType>> getCargoTypes();
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ public class ISysClientFallback implements ISysClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<List<FeeItem>> getFeeItems() {
|
||||
public R<List<FeeItem>> getFeeItems(Boolean includeDisabled) {
|
||||
return R.fail("获取数据失败");
|
||||
}
|
||||
|
||||
|
||||
+7
@@ -150,6 +150,13 @@ public class Dept extends TenantEntity {
|
||||
@Schema(description = "合同编码前缀")
|
||||
private String contractCodePrefix;
|
||||
|
||||
/**
|
||||
* 合同初始流水号(该部门当年首个合同时从此号起编,范围1-999)
|
||||
*/
|
||||
@Schema(description = "合同初始流水号")
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private Integer contractInitialSerial;
|
||||
|
||||
/**
|
||||
* 组织简称
|
||||
*/
|
||||
|
||||
+18
@@ -69,4 +69,22 @@ public class OaOrgSyncPageVO implements Serializable {
|
||||
|
||||
@Schema(description = "增量同步使用的 OA modified 起始时间,后续分页需原样回传")
|
||||
private String modifiedSince;
|
||||
|
||||
@Schema(description = "全量同步结束后原始数据层级 JSON 文件路径")
|
||||
private String dumpFilePath;
|
||||
|
||||
@Schema(description = "当前同步的公司 OA 主键(部门阶段按公司轮询时使用)")
|
||||
private String companyOaId;
|
||||
|
||||
@Schema(description = "当前公司序号,从 1 开始")
|
||||
private Integer companyIndex;
|
||||
|
||||
@Schema(description = "待同步公司总数")
|
||||
private Integer companyTotal;
|
||||
|
||||
@Schema(description = "当前公司的部门是否已同步完")
|
||||
private Boolean companyFinished;
|
||||
|
||||
@Schema(description = "下一公司 OA 主键;当前公司完成后回传,前端据此切换并重置页码")
|
||||
private String nextCompanyOaId;
|
||||
}
|
||||
|
||||
+3
@@ -66,4 +66,7 @@ public class OaPersonSyncPageVO implements Serializable {
|
||||
|
||||
@Schema(description = "增量同步使用的 OA modified 起始时间,后续分页需原样回传")
|
||||
private String modifiedSince;
|
||||
|
||||
@Schema(description = "全量同步结束后原始数据层级 JSON 文件路径")
|
||||
private String dumpFilePath;
|
||||
}
|
||||
|
||||
+2
@@ -32,6 +32,8 @@ public class FormalSettlementSaveRequest implements Serializable {
|
||||
private String remark;
|
||||
private List<SummaryFee> summaryFees;
|
||||
private List<Invoice> invoices;
|
||||
/** 项目预付核销 */
|
||||
private List<SettlementProjectAdvanceWriteOffRequest> projectAdvanceWriteOffs;
|
||||
|
||||
@Data
|
||||
public static class DetailAdjustment implements Serializable {
|
||||
|
||||
+5
@@ -35,6 +35,9 @@ public class PaymentApplicationSaveRequest implements Serializable {
|
||||
private Long settlementId;
|
||||
private List<Long> settlementIds;
|
||||
private Long preSettlementId;
|
||||
private List<Long> preSettlementIds;
|
||||
/** 预结算单号。驳回重提时用于在主键失真后找回预结算单 */
|
||||
private String preSettlementNo;
|
||||
private Long projectId;
|
||||
private String projectName;
|
||||
private Long deptId;
|
||||
@@ -60,4 +63,6 @@ public class PaymentApplicationSaveRequest implements Serializable {
|
||||
private String remark;
|
||||
private List<PaymentApplicationInvoiceRequest> invoices;
|
||||
private List<PaymentApplicationRecordRequest> paymentRecords;
|
||||
/** 多张结算单时按行提交的付款金额 */
|
||||
private List<PaymentApplicationSettlementLineRequest> settlementLines;
|
||||
}
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 付款申请多结算单付款明细。 @author Chill */
|
||||
@Data
|
||||
public class PaymentApplicationSettlementLineRequest implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
private Long formalSettlementId;
|
||||
private Long preSettlementId;
|
||||
private BigDecimal paymentRatio;
|
||||
private BigDecimal appliedAmount;
|
||||
}
|
||||
+6
@@ -51,6 +51,12 @@ public class PersonCertSubmitRequest implements Serializable {
|
||||
private String realName;
|
||||
@Schema(description = "身份证号")
|
||||
private String idCardNo;
|
||||
@Schema(description = "出生年月")
|
||||
private LocalDate birthday;
|
||||
@Schema(description = "住址-区划:省市区名称拼接串")
|
||||
private String addressRegion;
|
||||
@Schema(description = "住址-详细地址")
|
||||
private String address;
|
||||
@Schema(description = "身份证正面照")
|
||||
private String idCardFrontUrl;
|
||||
@Schema(description = "身份证反面照")
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ public class PreSettlementDetailAdjustRequest implements Serializable {
|
||||
@Schema(description = "预结算明细ID")
|
||||
private Long detailId;
|
||||
|
||||
@Schema(description = "调整原因")
|
||||
@Schema(description = "备注(写入结算明细备注,非必填)")
|
||||
private String changeReason;
|
||||
|
||||
@Schema(description = "明细费用行")
|
||||
|
||||
+3
@@ -76,6 +76,9 @@ public class PreSettlementSaveRequest implements Serializable {
|
||||
@Schema(description = "结算合计费用")
|
||||
private List<SummaryFee> summaryFees;
|
||||
|
||||
@Schema(description = "项目预付核销")
|
||||
private List<SettlementProjectAdvanceWriteOffRequest> projectAdvanceWriteOffs;
|
||||
|
||||
@Data
|
||||
@Schema(description = "结算合计费用")
|
||||
public static class SummaryFee implements Serializable {
|
||||
|
||||
+6
@@ -63,4 +63,10 @@ public class ReceivablePayableFeeCalculateRequest implements Serializable {
|
||||
@Schema(description = "动态费用项目")
|
||||
private Map<String, BigDecimal> feeItems;
|
||||
|
||||
@Schema(description = "运输单价")
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
@Schema(description = "本次调整字段:transportQuantity、mileage、unitPrice 或费用项名称")
|
||||
private String changedField;
|
||||
|
||||
}
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 导入结算单请求(列表筛选 + 确认/草稿入参)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "导入结算单请求")
|
||||
public class SettlementImportBatchRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "主键")
|
||||
private Long id;
|
||||
|
||||
@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,应收仅 formal")
|
||||
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=新运单,history=历史运单")
|
||||
private String waybillMode;
|
||||
|
||||
@Schema(description = "导入状态")
|
||||
private String importStatus;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "动态费用列名")
|
||||
private List<String> feeItemNames;
|
||||
|
||||
@Schema(description = "导入明细行")
|
||||
private List<Map<String, Object>> rows;
|
||||
|
||||
@Schema(description = "分批追加")
|
||||
private Boolean append;
|
||||
|
||||
@Schema(description = "列表筛选:项目名称")
|
||||
private String projectNameLike;
|
||||
|
||||
@Schema(description = "列表筛选:创建人")
|
||||
private Long createUser;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
}
|
||||
+8
@@ -172,4 +172,12 @@ public class FormalSettlement extends TenantEntity {
|
||||
* 作废原因,驳回时记录驳回原因
|
||||
*/
|
||||
private String voidReason;
|
||||
/**
|
||||
* 来源原正式结算单号:结算调整重推生成的新单,指向被调整的原单
|
||||
*/
|
||||
private String originFormalSettlementNo;
|
||||
/**
|
||||
* 调整状态:adjusted-已被结算调整(原单标记)
|
||||
*/
|
||||
private String adjustStatus;
|
||||
}
|
||||
|
||||
+4
@@ -25,6 +25,10 @@ public class FormalSettlementChangeRecord extends TenantEntity {
|
||||
private Integer lineNo;
|
||||
private String operationType;
|
||||
private String changeContent;
|
||||
/** 变更前数据JSON */
|
||||
private String beforeData;
|
||||
/** 变更后数据JSON */
|
||||
private String afterData;
|
||||
private String changeReason;
|
||||
private String operatorName;
|
||||
private LocalDateTime changeTime;
|
||||
|
||||
+8
@@ -8,7 +8,9 @@
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
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;
|
||||
@@ -123,6 +125,12 @@ public class FormalSettlementDetail extends TenantEntity {
|
||||
* 批次号
|
||||
*/
|
||||
private String batchNo;
|
||||
/**
|
||||
* 计费方式(来源应收应付货物费用计费类型,非本表字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "计费方式")
|
||||
private String billingType;
|
||||
/**
|
||||
* 运输单价
|
||||
*/
|
||||
|
||||
+8
@@ -8,7 +8,9 @@
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
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;
|
||||
@@ -70,6 +72,12 @@ public class FormalSettlementDetailFee extends TenantEntity {
|
||||
* 费用项JSON
|
||||
*/
|
||||
private String feeItemsJson;
|
||||
/**
|
||||
* 命中计费规则JSON(来源费用带回,非本表字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "命中计费规则JSON")
|
||||
private String billingRulesJson;
|
||||
/**
|
||||
* 原金额
|
||||
*/
|
||||
|
||||
+6
@@ -1,5 +1,6 @@
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -17,7 +18,12 @@ public class PaymentApplicationSettlement extends TenantEntity {
|
||||
private Long paymentApplicationId;
|
||||
private Long formalSettlementId;
|
||||
private String formalSettlementNo;
|
||||
private Long preSettlementId;
|
||||
private String preSettlementNo;
|
||||
private BigDecimal settlementAmount;
|
||||
private BigDecimal appliedAmount;
|
||||
private BigDecimal paidAmount;
|
||||
/** 查看时回填的可付款金额,不落库 */
|
||||
@TableField(exist = false)
|
||||
private BigDecimal payableAmount;
|
||||
}
|
||||
|
||||
+5
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -99,6 +100,10 @@ public class PreSettlementDetail extends TenantEntity {
|
||||
@Schema(description = "批次号")
|
||||
private String batchNo;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "计费方式")
|
||||
private String billingType;
|
||||
|
||||
@Schema(description = "运输单价")
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
|
||||
+4
@@ -30,6 +30,10 @@ public class SettlementAdjustment extends TenantEntity {
|
||||
private String currentNode;
|
||||
private String currentProcessor;
|
||||
private String kingdeeSyncStatus;
|
||||
/**
|
||||
* 金蝶单据号(未推送场景回填原结算单金蝶单号;重推场景回填新单金蝶单号)
|
||||
*/
|
||||
private String kingdeeBillNo;
|
||||
private String attachmentsJson;
|
||||
private String remark;
|
||||
private LocalDateTime approvedTime;
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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 = "导入明细行JSON")
|
||||
private String rowsJson;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "关联运单导入批次ID")
|
||||
private Long waybillImportBatchId;
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
}
|
||||
+3
@@ -6,6 +6,8 @@
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -41,6 +43,7 @@ public class TransportReconciliationExternal extends TenantEntity {
|
||||
private BigDecimal freightAmount;
|
||||
private String feeItemsJson;
|
||||
private BigDecimal settlementAmount;
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private Boolean suspectedDuplicate;
|
||||
private String matchStatus;
|
||||
private Long matchedInternalId;
|
||||
|
||||
+3
@@ -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;
|
||||
|
||||
|
||||
+26
@@ -92,4 +92,30 @@ public class AdminWaybillCardVO implements Serializable {
|
||||
@Schema(description = "采购方是否已付款(预留)")
|
||||
private Boolean buyerPaid;
|
||||
|
||||
/* ===== 运次(一趟车)字段:列表按运次下发,卡片的展示单号与路线取这些 ===== */
|
||||
|
||||
@Schema(description = "运次主键:配载单主键或运单主键,按字符串下发(19 位雪花号按数字下发会被前端取整)")
|
||||
private String tripId;
|
||||
|
||||
@Schema(description = "配载单号,未配载为空")
|
||||
private String loadingNo;
|
||||
|
||||
@Schema(description = "展示单号:有配载取配载单号,否则取运单号")
|
||||
private String tripNo;
|
||||
|
||||
@Schema(description = "途经站点:整趟车中间各站按顺序以「 → 」连接,无中间站为空")
|
||||
private String viaName;
|
||||
|
||||
@Schema(description = "站点数(整趟车,含起终点)")
|
||||
private Integer stationCount;
|
||||
|
||||
@Schema(description = "货物汇总(整趟车,同名货物合并且单位一致时取数量合计)")
|
||||
private String cargoSummary;
|
||||
|
||||
@Schema(description = "合计重量(整趟车同单位时才有值,单位不一致为空)")
|
||||
private String totalWeight;
|
||||
|
||||
@Schema(description = "子运单数(可见口径;配载运次即整趟车的子单数)")
|
||||
private Integer waybillCount;
|
||||
|
||||
}
|
||||
|
||||
+27
-1
@@ -66,9 +66,12 @@ public class AdminWaybillDetailVO implements Serializable {
|
||||
@Schema(description = "卸货地址")
|
||||
private String unloadAddress;
|
||||
|
||||
@Schema(description = "货物名称")
|
||||
@Schema(description = "货物名称:运单上录入的全部货名以「、」连接")
|
||||
private String cargoName;
|
||||
|
||||
@Schema(description = "货物名称清单:运单上录入的全部货物,同名的只留一次")
|
||||
private List<String> cargoNames;
|
||||
|
||||
@Schema(description = "货物数量(带单位)")
|
||||
private String cargoQuantity;
|
||||
|
||||
@@ -135,6 +138,29 @@ public class AdminWaybillDetailVO implements Serializable {
|
||||
@Schema(description = "途打卡记录")
|
||||
private List<DriverEnrouteRecordVO> enrouteRecords;
|
||||
|
||||
/* ===== 运次(一趟车)字段:详情按运次下发,与司机端接单页同一口径 ===== */
|
||||
|
||||
@Schema(description = "运次主键:配载单主键或运单主键,按字符串下发(19 位雪花号按数字下发会被前端取整)")
|
||||
private String tripId;
|
||||
|
||||
@Schema(description = "配载单号,未配载为空")
|
||||
private String loadingNo;
|
||||
|
||||
@Schema(description = "展示单号:有配载取配载单号,否则取运单号")
|
||||
private String tripNo;
|
||||
|
||||
@Schema(description = "途经站点:整趟车中间各站按顺序以「 → 」连接,无中间站为空")
|
||||
private String viaName;
|
||||
|
||||
@Schema(description = "站点数(整趟车,含起终点)")
|
||||
private Integer stationCount;
|
||||
|
||||
@Schema(description = "货物汇总(整趟车,同名货物合并且单位一致时取数量合计)")
|
||||
private String cargoSummary;
|
||||
|
||||
@Schema(description = "站点任务安排(整趟车按实际途经顺序,第 1 站即 ①;配载运次含每站装卸与货物条目)")
|
||||
private List<TripStationVO> stations;
|
||||
|
||||
@Data
|
||||
@Schema(description = "装卸点")
|
||||
public static class AdminRoutePointVO implements Serializable {
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 货物明细行:一条货物一行
|
||||
* <p>
|
||||
* 详情页逐行展示「货物名称 + 数量单位」,名称与数量分开给,前端不需要再从汇总串里拆。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "货物明细行")
|
||||
public class CargoLineVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "货物名称")
|
||||
private String cargoName;
|
||||
|
||||
@Schema(description = "数量(含单位,如 1000吨);录入时只填了名称则为空")
|
||||
private String quantity;
|
||||
}
|
||||
+6
@@ -67,9 +67,15 @@ public class DriverPunchNodeVO implements Serializable {
|
||||
@Schema(description = "不可打卡的原因,供司机端展示")
|
||||
private String lockReason;
|
||||
|
||||
@Schema(description = "运单已结束(完成/取消):卡片仅展示、不可提交")
|
||||
private Boolean ended;
|
||||
|
||||
@Schema(description = "是否展示该卡(在途可能因频次/时段隐藏)")
|
||||
private Boolean visible;
|
||||
|
||||
@Schema(description = "打这张卡即完成运单:该运单的末位可打卡节点,且过程配置不要求业务员确认回单")
|
||||
private Boolean finishOnPunch;
|
||||
|
||||
@Schema(description = "是否默认展开(仅第一个可打卡节点)")
|
||||
private Boolean defaultExpanded;
|
||||
|
||||
|
||||
+29
@@ -67,6 +67,9 @@ public class DriverWaybillCardVO implements Serializable {
|
||||
@Schema(description = "货物类别")
|
||||
private String cargoCategory;
|
||||
|
||||
@Schema(description = "货物明细(详情返回):一条一行,同名同单位的货物已合并数量")
|
||||
private List<CargoLineVO> cargos;
|
||||
|
||||
@Schema(description = "重量(带单位)")
|
||||
private String weight;
|
||||
|
||||
@@ -150,6 +153,29 @@ public class DriverWaybillCardVO implements Serializable {
|
||||
@Schema(description = "运输方式文案:公路运输等")
|
||||
private String transportType;
|
||||
|
||||
@Schema(description = "运输组织:common普通 / load配载")
|
||||
private String transportOrgType;
|
||||
|
||||
/* ===== 运次(一趟车)字段:运次接口返回,旧运单接口不填 ===== */
|
||||
|
||||
@Schema(description = "运次主键:配载单主键或运单主键,按字符串下发(19 位雪花号按数字下发会被前端取整)")
|
||||
private String tripId;
|
||||
|
||||
@Schema(description = "配载单号,未配载为空")
|
||||
private String loadingNo;
|
||||
|
||||
@Schema(description = "展示单号:有配载取配载单号,否则取运单号")
|
||||
private String tripNo;
|
||||
|
||||
@Schema(description = "途经站点:整趟车中间各站按顺序以「 → 」连接")
|
||||
private String viaName;
|
||||
|
||||
@Schema(description = "站点数(整趟车,含起终点)")
|
||||
private Integer stationCount;
|
||||
|
||||
@Schema(description = "货物汇总(整趟车,同名货物合并且单位一致时取数量合计)")
|
||||
private String cargoSummary;
|
||||
|
||||
@Schema(description = "计划发货时间")
|
||||
private String planShipTime;
|
||||
|
||||
@@ -165,6 +191,9 @@ public class DriverWaybillCardVO implements Serializable {
|
||||
@Schema(description = "装卸点(详情返回)")
|
||||
private List<DriverRoutePointVO> routePoints;
|
||||
|
||||
@Schema(description = "站点任务安排(详情返回,按实际途经顺序,第 1 站即 ①;有配载时含每站装卸与货物条目)")
|
||||
private List<TripStationVO> stations;
|
||||
|
||||
@Schema(description = "过程配置 JSON(详情返回,供前端兜底推导打卡节点)")
|
||||
private String processJson;
|
||||
|
||||
|
||||
+14
-1
@@ -20,10 +20,12 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -33,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;
|
||||
@@ -50,6 +52,14 @@ public class FormalSettlementVO extends FormalSettlement {
|
||||
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) @Schema(description = "调整状态名称:adjusted-已调整")
|
||||
private String adjustStatusName;
|
||||
@TableField(exist = false) private String preSettlementNos;
|
||||
@TableField(exist = false) private String preSettlementNo;
|
||||
@TableField(exist = false) private List<FormalSettlementSource> sources;
|
||||
@@ -57,9 +67,12 @@ public class FormalSettlementVO extends FormalSettlement {
|
||||
@TableField(exist = false) private List<FormalSettlementSummaryFee> summaryFees;
|
||||
@TableField(exist = false) private List<FormalSettlementPayment> payments;
|
||||
@TableField(exist = false) private List<FormalSettlementInvoice> invoices;
|
||||
@TableField(exist = false) @Schema(description = "项目预付核销")
|
||||
private List<SettlementProjectAdvanceWriteOff> projectAdvanceWriteOffs;
|
||||
@TableField(exist = false) @Schema(description = "开票申请经金蝶回写的发票结果(含文件地址,只读展示)")
|
||||
private List<InvoiceApplicationInvoice> issuedInvoices;
|
||||
@TableField(exist = false) private List<PaymentApplicationVO> paymentApplications;
|
||||
@TableField(exist = false) private List<SettlementAdvancePaymentVO> advancePayments;
|
||||
@TableField(exist = false) private List<SettlementAdjustmentVO> adjustments;
|
||||
@TableField(exist = false) private List<FormalSettlementChangeRecord> changeRecords;
|
||||
}
|
||||
|
||||
+8
-1
@@ -35,6 +35,7 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -44,7 +45,7 @@ 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;
|
||||
@@ -55,6 +56,12 @@ public class InvoiceApplicationVO extends InvoiceApplication {
|
||||
@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<InvoiceApplicationSettlement> settlements;
|
||||
@TableField(exist = false) private List<InvoiceApplicationSheetVO> sheets;
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
+2
@@ -27,4 +27,6 @@ public class PaymentApplicationReferenceAmountVO implements Serializable {
|
||||
@Schema(description = "结算金额") private BigDecimal settlementAmount;
|
||||
@Schema(description = "累计申请付款金额") private BigDecimal cumulativeAppliedAmount;
|
||||
@Schema(description = "可付款金额") private BigDecimal payableAmount;
|
||||
/** 该结算单当前为第几次付款(含本次,已作废不计) */
|
||||
@Schema(description = "付款次序") private Integer paymentSequence;
|
||||
}
|
||||
|
||||
+8
-1
@@ -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;
|
||||
@@ -49,6 +50,12 @@ public class PaymentApplicationVO extends PaymentApplication {
|
||||
@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<PaymentApplicationInvoice> invoices;
|
||||
@TableField(exist = false) private List<PaymentApplicationRecord> paymentRecords;
|
||||
@TableField(exist = false) private List<PaymentApplicationSettlement> settlements;
|
||||
|
||||
+5
@@ -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<PreSettlementAdvance> advances;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "项目预付核销")
|
||||
private List<SettlementProjectAdvanceWriteOff> projectAdvanceWriteOffs;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "变更记录")
|
||||
private List<PreSettlementChangeRecord> changeRecords;
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
}
|
||||
+6
@@ -39,6 +39,7 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 收款流水视图实体类
|
||||
@@ -91,4 +92,9 @@ public class ReceiptFlowVO extends KingdeeReceiptFlow {
|
||||
private BigDecimal receiptAmountEnd;
|
||||
@TableField(exist = false)
|
||||
private List<ReceiptFlowRecord> claimRecords = new ArrayList<>();
|
||||
/**
|
||||
* 本流水历史关联且仍有剩余可认领金额的应收正式结算单(继续认领时预填)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private List<Map<String, Object>> relatedSettlements = new ArrayList<>();
|
||||
}
|
||||
|
||||
+4
@@ -73,6 +73,10 @@ public class ReceivablePayableDetailVO extends ReceivablePayableDetail {
|
||||
@Schema(description = "状态名称")
|
||||
private String settlementStatusName;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "计费方式")
|
||||
private String billingType;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "费用项目")
|
||||
private Map<String, Object> feeItems;
|
||||
|
||||
-1
@@ -17,7 +17,6 @@ public class SettlementAdjustmentVO extends SettlementAdjustment {
|
||||
private String approvalStatusName;
|
||||
private String settlementTypeName;
|
||||
private String createUserName;
|
||||
private String kingdeeBillNo;
|
||||
private List<SettlementAdjustmentDetail> details;
|
||||
private List<FormalSettlementDetail> formalDetails;
|
||||
}
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
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 SettlementAdvancePaymentVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "记录类型:progress_advance 进度预付 / project_advance 项目预付核销")
|
||||
private String recordType;
|
||||
|
||||
@Schema(description = "付款单号")
|
||||
private String paymentNo;
|
||||
|
||||
@Schema(description = "申请金额(进度预付为申请预付金额,项目预付为本次核销金额)")
|
||||
private BigDecimal appliedAmount;
|
||||
|
||||
@Schema(description = "已付款金额(底层付款申请实付)")
|
||||
private BigDecimal paidAmount;
|
||||
|
||||
@Schema(description = "单据状态")
|
||||
private String billStatus;
|
||||
|
||||
@Schema(description = "金蝶单据号")
|
||||
private String kingdeeBillNo;
|
||||
|
||||
@Schema(description = "创建人姓名")
|
||||
private String createUserName;
|
||||
|
||||
@Schema(description = "创建时间(统一取底层付款申请创建时间)")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 导入结算单批次视图
|
||||
*
|
||||
* @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<String> feeItemNames;
|
||||
|
||||
@Schema(description = "导入明细行")
|
||||
private List<Map<String, Object>> rows;
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 运次查询条件
|
||||
* <p>
|
||||
* 司机端用 {@link #forDriver(String)} 按司机手机号收口;管理端用 {@link #forAdmin()} 不按司机收口。
|
||||
* 两者都不给时收口为空集——收口一旦缺省就等于把别人的运次发出去,故缺省不放行。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "运次查询条件")
|
||||
public class TripQueryVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "运次主键(详情按它取单个运次)")
|
||||
private String tripId;
|
||||
|
||||
@Schema(description = "司机手机号")
|
||||
private String driverPhone;
|
||||
|
||||
@Schema(description = "是否不按司机收口(管理端)")
|
||||
private boolean allDrivers;
|
||||
|
||||
@Schema(description = "可见组织 ID 集合:非空时只出这些组织的运单行,空集合表示不按组织过滤")
|
||||
private List<Long> deptIds;
|
||||
|
||||
@Schema(description = "状态:0待接单/1进行中/2已完成/3已取消,不传为全部(不含已取消)")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "多档状态:同上取值,非空时优先于 status(待处理列表要待接单与进行中两档)")
|
||||
private List<Integer> statuses;
|
||||
|
||||
@Schema(description = "关键字:运单号 / 配载单号 / 起终点 / 车牌")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "运输组织类型:load 公路配载 / common 普通运输,不传为全部(按子单行筛选)")
|
||||
private String transportOrgType;
|
||||
|
||||
@Schema(description = "异常筛选:exception 只看命中异常的子单 / normal 只看未命中的,不传为全部(按子单行筛选)")
|
||||
private String exception;
|
||||
|
||||
@Schema(description = "有未完成异常的子运单主键集合,配合 exception 使用(按子单行筛选)")
|
||||
private Set<Long> exceptionWaybillIds;
|
||||
|
||||
@Schema(description = "创建时间下界,按子单行筛选")
|
||||
private Date createdFrom;
|
||||
|
||||
@Schema(description = "创建时间上界,按子单行筛选")
|
||||
private Date createdTo;
|
||||
|
||||
@Schema(description = "是否排除「有子单被拒且这趟车还没结束」的运次(待处理列表用)")
|
||||
private boolean excludeRejectedTrips;
|
||||
|
||||
@Schema(description = "当前页,从 1 开始")
|
||||
private Integer current;
|
||||
|
||||
@Schema(description = "每页条数(按运次,不是运单)")
|
||||
private Integer size;
|
||||
|
||||
public static TripQueryVO forDriver(String driverPhone) {
|
||||
TripQueryVO query = new TripQueryVO();
|
||||
query.setDriverPhone(driverPhone);
|
||||
return query;
|
||||
}
|
||||
|
||||
public static TripQueryVO forAdmin() {
|
||||
TripQueryVO query = new TripQueryVO();
|
||||
query.setAllDrivers(true);
|
||||
return query;
|
||||
}
|
||||
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
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.List;
|
||||
|
||||
/**
|
||||
* 运次站点(任务安排)
|
||||
* <p>
|
||||
* 有配载单时按配载单的路线信息给出实际途经顺序,第 1 站即 ①;站点上的「装N / 卸N」指向配载单运单集合里的第 N 张子运单,
|
||||
* 同一站可以既卸又装(装卸)。没有路线信息时回退到子运单的起讫点去重成站。
|
||||
* <p>
|
||||
* 站点上的货物条目取子运单,不取配载单的货物信息:配载单的货物条数与子运单不一定同数,行序也对不上站点编号。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "运次站点")
|
||||
public class TripStationVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "站序,从 1 开始(展示为 ①②③)")
|
||||
private Integer index;
|
||||
|
||||
@Schema(description = "站名:展示地址优先,回退详细地址")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "详细地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "站点标记原文:装N / 卸N")
|
||||
private List<String> tags;
|
||||
|
||||
@Schema(description = "操作类型:load 装货 / unload 卸货 / load_unload 装卸,无从判定时为空")
|
||||
private String operationType;
|
||||
|
||||
@Schema(description = "操作类型文案:装货 / 卸货 / 装卸")
|
||||
private String operationTypeLabel;
|
||||
|
||||
@Schema(description = "该站涉及的货物条目")
|
||||
private List<TripCargoVO> cargos;
|
||||
|
||||
/**
|
||||
* 站点货物条目
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "运次站点货物条目")
|
||||
public static class TripCargoVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "所属子运单主键,按字符串下发")
|
||||
private String waybillId;
|
||||
|
||||
@Schema(description = "所属运单号")
|
||||
private String waybillNo;
|
||||
|
||||
@Schema(description = "货物名称")
|
||||
private String cargoName;
|
||||
|
||||
@Schema(description = "货主名称")
|
||||
private String customerName;
|
||||
|
||||
@Schema(description = "数量")
|
||||
private BigDecimal quantity;
|
||||
|
||||
@Schema(description = "数量单位")
|
||||
private String quantityUnit;
|
||||
|
||||
@Schema(description = "该站的操作:load 装货 / unload 卸货")
|
||||
private String operation;
|
||||
|
||||
@Schema(description = "项目名称")
|
||||
private String projectName;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 运次 Tab 统计
|
||||
* <p>
|
||||
* 与司机端运单统计字段同名同义,但计数单位是运次(一卡一趟车),不是运单。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "运次 Tab 统计")
|
||||
public class TripTabCountsVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "全部(待接单+进行中+已完成,不含已取消)")
|
||||
private long all;
|
||||
|
||||
@Schema(description = "待接单")
|
||||
private long pending;
|
||||
|
||||
@Schema(description = "进行中")
|
||||
private long doing;
|
||||
|
||||
@Schema(description = "已完成")
|
||||
private long done;
|
||||
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
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.time.LocalDate;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 运次:一趟车的展示与操作单位
|
||||
* <p>
|
||||
* 有配载单时就是那张配载单连同它的全部子运单,没配载时就是一张运单;它没有自己的表。
|
||||
* 状态与接单需求都由子运单读时聚合,配载单上不落这两个字段。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "运次")
|
||||
public class TripVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "运次主键:配载单主键或运单主键,按字符串下发(19 位雪花号按数字下发会被前端取整)")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "展示单号:有配载取配载单号,否则取运单号")
|
||||
private String tripNo;
|
||||
|
||||
@Schema(description = "配载单号,未配载为空")
|
||||
private String loadingNo;
|
||||
|
||||
@Schema(description = "运输组织类型:load 公路配载 / common 普通运输")
|
||||
private String transportOrgType;
|
||||
|
||||
@Schema(description = "状态:0待接单/1运输中/2已完成/3已取消,子单集合为空时为空")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "排序键:运次内子运单 create_time 的最大值")
|
||||
private Date sortTime;
|
||||
|
||||
@Schema(description = "是否需要接单:任一子运单的项目有接单节点即为是")
|
||||
private Boolean requireAccept;
|
||||
|
||||
@Schema(description = "接单态:accepted 已接单 / pending 待接单,无从判定时为空")
|
||||
private String acceptStatus;
|
||||
|
||||
@Schema(description = "子运单数(可见口径:已排除本司机拒单的子单)")
|
||||
private int waybillCount;
|
||||
|
||||
@Schema(description = "司机姓名")
|
||||
private String driverName;
|
||||
|
||||
@Schema(description = "司机手机号")
|
||||
private String driverPhone;
|
||||
|
||||
@Schema(description = "车牌号")
|
||||
private String vehicleNo;
|
||||
|
||||
@Schema(description = "挂车车牌号")
|
||||
private String trailerVehicleNo;
|
||||
|
||||
@Schema(description = "项目名称")
|
||||
private String projectName;
|
||||
|
||||
@Schema(description = "客户名称")
|
||||
private String customerName;
|
||||
|
||||
@Schema(description = "运输类型")
|
||||
private String transportType;
|
||||
|
||||
@Schema(description = "货物名称:整趟车全部货名以「、」连接,一格多名的历史写法已摊平")
|
||||
private String cargoName;
|
||||
|
||||
@Schema(description = "货物名称清单:整趟车全部货物,同名的只留一次,保持子单与录入顺序")
|
||||
private List<String> cargoNames;
|
||||
|
||||
@Schema(description = "货物类型:整趟车全部类型以「、」连接")
|
||||
private String cargoType;
|
||||
|
||||
@Schema(description = "货物类型清单:整趟车全部货物类型,同类型的只留一次")
|
||||
private List<String> cargoTypes;
|
||||
|
||||
@Schema(description = "发货地址")
|
||||
private String departureAddress;
|
||||
|
||||
@Schema(description = "途经地")
|
||||
private String transitAddress;
|
||||
|
||||
@Schema(description = "到货地")
|
||||
private String arrivalAddress;
|
||||
|
||||
@Schema(description = "起点名称:整趟车首站,展示地址优先、回退详细地址")
|
||||
private String departureName;
|
||||
|
||||
@Schema(description = "途经站点:中间各站按顺序以「 → 」连接,无中间站为空")
|
||||
private String transitName;
|
||||
|
||||
@Schema(description = "终点名称:整趟车末站,展示地址优先、回退详细地址")
|
||||
private String arrivalName;
|
||||
|
||||
@Schema(description = "站点数(整趟车,含起终点)")
|
||||
private int stationCount;
|
||||
|
||||
@Schema(description = "货物汇总:按货物名合并数量,同名同单位取合计")
|
||||
private String cargoSummary;
|
||||
|
||||
@Schema(description = "货物明细:一条货物一行,同名同单位的货物已合并数量")
|
||||
private List<CargoLineVO> cargos;
|
||||
|
||||
@Schema(description = "合计重量:全部子运单同单位时为数量合计,单位不一致时为空")
|
||||
private BigDecimal totalQuantity;
|
||||
|
||||
@Schema(description = "合计重量的单位,与 totalQuantity 同时为空或同时有值")
|
||||
private String quantityUnit;
|
||||
|
||||
@Schema(description = "计重合计:全部货物同单位时取该单位合计,单位不一致时只合计「吨」项(其余单位不参与);无可用合计时为空")
|
||||
private BigDecimal weighingQuantity;
|
||||
|
||||
@Schema(description = "计重合计的单位,与 weighingQuantity 同时为空或同时有值")
|
||||
private String weighingUnit;
|
||||
|
||||
@Schema(description = "计划发货日期(配载单,未配载为空)")
|
||||
private LocalDate estimatedStartDate;
|
||||
|
||||
@Schema(description = "计划完成日期(配载单,未配载为空)")
|
||||
private LocalDate estimatedEndDate;
|
||||
|
||||
@Schema(description = "任务备注(配载单的运输说明)")
|
||||
private String taskRemark;
|
||||
|
||||
@Schema(description = "路线信息,仅单个运次详情返回(列表不带,省流量)")
|
||||
private String routeJson;
|
||||
|
||||
@Schema(description = "货物信息,仅单个运次详情返回(列表不带,省流量)")
|
||||
private String goodsJson;
|
||||
|
||||
@Schema(description = "站点(任务安排,按实际途经顺序),仅单个运次详情返回;列表用起/经/终与站点数")
|
||||
private List<TripStationVO> stations;
|
||||
|
||||
@Schema(description = "子运单(可见子单,按创建时间升序)")
|
||||
private List<TripWaybillVO> waybills;
|
||||
|
||||
/**
|
||||
* 运次下的子运单
|
||||
* <p>
|
||||
* 接单事实仍落在每张子运单上,运次只是把它们的需要接单与接单态聚合起来。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "运次下的子运单")
|
||||
public static class TripWaybillVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "运单主键,按字符串下发")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "运单号")
|
||||
private String waybillNo;
|
||||
|
||||
@Schema(description = "业务状态")
|
||||
private String businessStatus;
|
||||
|
||||
@Schema(description = "接单状态:pending待接单/accepted已接单/rejected已拒绝")
|
||||
private String acceptStatus;
|
||||
|
||||
@Schema(description = "该运单是否需要接单确认")
|
||||
private Boolean requireAccept;
|
||||
|
||||
@Schema(description = "项目ID")
|
||||
private Long projectId;
|
||||
|
||||
@Schema(description = "项目名称")
|
||||
private String projectName;
|
||||
|
||||
@Schema(description = "货物名称:运单上录入的全部货名以「、」连接,一格多名的历史写法已摊平")
|
||||
private String cargoName;
|
||||
|
||||
@Schema(description = "货物名称清单:运单上录入的全部货物,同名的只留一次")
|
||||
private List<String> cargoNames;
|
||||
|
||||
@Schema(description = "货物类型:主表首行的类型,完整清单见 cargoTypes")
|
||||
private String cargoType;
|
||||
|
||||
@Schema(description = "货物类型清单:运单上录入的全部货物类型,同类型的只留一次")
|
||||
private List<String> cargoTypes;
|
||||
|
||||
@Schema(description = "数量")
|
||||
private BigDecimal quantity;
|
||||
|
||||
@Schema(description = "数量单位")
|
||||
private String quantityUnit;
|
||||
|
||||
@Schema(description = "发货地")
|
||||
private String departureName;
|
||||
|
||||
@Schema(description = "发货地址")
|
||||
private String departureAddress;
|
||||
|
||||
@Schema(description = "收货地")
|
||||
private String arrivalName;
|
||||
|
||||
@Schema(description = "收货地址")
|
||||
private String arrivalAddress;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+6
@@ -20,6 +20,12 @@ public class WaybillImportBatchVO extends WaybillImportBatch {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String importTypeName;
|
||||
private String statusName;
|
||||
/** 结算状态:unsettled/partial/settled */
|
||||
@Schema(description = "结算状态")
|
||||
private String settlementStatus;
|
||||
/** 结算状态名称:未结算/部分结算/已结算 */
|
||||
@Schema(description = "结算状态名称")
|
||||
private String settlementStatusName;
|
||||
private String createUserName;
|
||||
private String updateUserName;
|
||||
}
|
||||
|
||||
+9
@@ -29,6 +29,7 @@ import lombok.EqualsAndHashCode;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 运单管理视图实体类
|
||||
@@ -78,4 +79,12 @@ public class WaybillVO extends Waybill {
|
||||
@Schema(description = "是否仅查询未配载运单")
|
||||
private Integer onlyUnassignedLoading;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "是否仅查询可配载运单")
|
||||
private Integer loadableOnly;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "运单主键集合")
|
||||
private List<Long> ids;
|
||||
|
||||
}
|
||||
|
||||
+34
-1
@@ -17,7 +17,40 @@ public interface ISettlementAdjustmentService {
|
||||
Long saveDraft(SettlementAdjustmentSaveRequest request);
|
||||
void removeDraft(Long id);
|
||||
void submit(SettlementAdjustmentStatusRequest request);
|
||||
void approve(SettlementAdjustmentStatusRequest request);
|
||||
/**
|
||||
* 审批通过:未推送场景回写原单并发布金蝶自动推送事件;已推送场景原单零改动仅追加变更记录。
|
||||
*
|
||||
* @param request 审批请求
|
||||
* @return 已推送场景的手工冲销提示文案,未推送场景返回 null
|
||||
*/
|
||||
String approve(SettlementAdjustmentStatusRequest request);
|
||||
void returnBill(SettlementAdjustmentStatusRequest request);
|
||||
String repush(Long adjustmentId);
|
||||
|
||||
/**
|
||||
* 选单后金蝶冲销预检(软提示,不阻断建单):未推送 / 已推送已冲销 / 已推送未冲销;
|
||||
* 金蝶查询异常降级返回「未校验」,由审批分流与重推闸门兜底
|
||||
*
|
||||
* @param formalSettlementId 选定正式结算单id
|
||||
* @return syncStatus / writtenOff / message
|
||||
*/
|
||||
Map<String, Object> kingdeeWriteOffPrecheck(Long formalSettlementId);
|
||||
|
||||
/**
|
||||
* 推送金蝶(未推送/生成失败场景):推送关联正式结算单,成功后由正式结算单回填调整单金蝶单号与同步状态
|
||||
*
|
||||
* @param adjustmentId 结算调整单id
|
||||
* @return 金蝶单据编号
|
||||
*/
|
||||
String syncKingdee(Long adjustmentId);
|
||||
|
||||
/**
|
||||
* 重推克隆+应用(独立事务,由 repush 经自代理调用):克隆原单为新正式结算单并应用调整,
|
||||
* 供推送失败重试时幂等复用
|
||||
*
|
||||
* @param originalSettlementId 原正式结算单id
|
||||
* @param adjustmentId 结算调整单id
|
||||
* @return 新正式结算单id
|
||||
*/
|
||||
Long cloneAndApplyForRepush(Long originalSettlementId, Long adjustmentId);
|
||||
}
|
||||
|
||||
+4
@@ -132,6 +132,10 @@ public class User extends TenantEntity implements IPhone {
|
||||
* 是否是oa的账号
|
||||
*/
|
||||
private Integer isOa;
|
||||
/**
|
||||
* OA登录名
|
||||
*/
|
||||
private String oaLoginId;
|
||||
/**
|
||||
* OA上次更新时间(OA 的 modified 字段),仅同步比对使用
|
||||
*/
|
||||
|
||||
-2
@@ -11,7 +11,6 @@ import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.process.pojo.dto.ApprovalDTO;
|
||||
@@ -49,7 +48,6 @@ public class BusinessProcessController extends BladeController {
|
||||
if (param == null) {
|
||||
param = new ApprovalDTO();
|
||||
}
|
||||
param.setLoginName(AuthUtil.getUserAccount());
|
||||
IPage<ApprovalVO> pages = businessProcessService.queryMkApprovalList(Condition.getPage(query), param);
|
||||
return R.data(pages);
|
||||
}
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
|
||||
* 获取流程当前节点详情
|
||||
*
|
||||
* @param processInstanceId 流程实例id
|
||||
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析
|
||||
* @param loginName MK登录名(OA登录名),可为空,为空时按流程发起人解析
|
||||
* @return 当前节点列表
|
||||
*/
|
||||
List<?> getCurrentNodes(String processInstanceId, String loginName);
|
||||
@@ -80,7 +80,7 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
|
||||
* 获取流程实例详情
|
||||
*
|
||||
* @param processInstanceId 流程实例id
|
||||
* @param loginName MK登录名(手机号),可为空,为空时按流程发起人解析
|
||||
* @param loginName MK登录名(OA登录名),可为空,为空时按流程发起人解析
|
||||
* @return 流程实例详情
|
||||
*/
|
||||
Object getProcessInfo(String processInstanceId, String loginName);
|
||||
|
||||
+21
-27
@@ -132,9 +132,9 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
public String processSubmit(MKProcessCreateDTO param) {
|
||||
AssertUtils.notNull(param, "提交流程参数不能为空");
|
||||
AssertUtils.notBlank(param.getTemplateCode(), "模板编码不能为空");
|
||||
// 前端拿到的手机号可能经 @Sensitive 脱敏(如 137****8880),这里从库取真实手机号覆盖
|
||||
String loginName = resolveCurrentUserPhone();
|
||||
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法提交审核流");
|
||||
// MK loginName 使用当前用户 OA 登录名,未配置则不允许提交
|
||||
String loginName = resolveCurrentUserOaLoginId();
|
||||
AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法提交审核流");
|
||||
param.setSubmitIdentity(loginName);
|
||||
param.setLoginName(loginName);
|
||||
String bizType = param.getBizType();
|
||||
@@ -258,7 +258,7 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
resolvedLoginName = businessProcess.getPromoterLoginName();
|
||||
}
|
||||
if (StringUtil.isBlank(resolvedLoginName)) {
|
||||
resolvedLoginName = resolveCurrentUserPhone();
|
||||
resolvedLoginName = resolveCurrentUserOaLoginId();
|
||||
}
|
||||
if (StringUtil.isBlank(resolvedLoginName)) {
|
||||
return false;
|
||||
@@ -291,10 +291,10 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
}
|
||||
String loginName = businessProcess.getPromoterLoginName();
|
||||
if (StringUtil.isBlank(loginName)) {
|
||||
loginName = resolveCurrentUserPhone();
|
||||
loginName = resolveCurrentUserOaLoginId();
|
||||
}
|
||||
if (StringUtil.isBlank(loginName)) {
|
||||
log.warn("查询MK流程实例跳过,登录名为空 bizId={} processInstanceId={}",
|
||||
log.warn("查询MK流程实例跳过,OA登录名为空 bizId={} processInstanceId={}",
|
||||
bizId, businessProcess.getProcessInstanceId());
|
||||
return false;
|
||||
}
|
||||
@@ -357,9 +357,9 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
}
|
||||
String loginName = businessProcess.getPromoterLoginName();
|
||||
if (StringUtil.isBlank(loginName)) {
|
||||
loginName = resolveCurrentUserPhone();
|
||||
loginName = resolveCurrentUserOaLoginId();
|
||||
}
|
||||
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法删除审核流");
|
||||
AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法删除审核流");
|
||||
log.info("调用mk processDelete processInstanceId={} loginName={} bizId={} formInstanceId={}",
|
||||
businessProcess.getProcessInstanceId(), loginName, bizId, businessProcess.getFormInstanceId());
|
||||
boolean deleted = mkService.processDelete(businessProcess.getProcessInstanceId(), loginName);
|
||||
@@ -563,27 +563,18 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
}
|
||||
|
||||
/**
|
||||
* 从当前登录用户实体读取真实手机号(库内 SM2 密文先解密,绕过接口脱敏)
|
||||
* 从当前登录用户实体读取 OA 登录名,作为 MK 接口 loginName
|
||||
*/
|
||||
private String resolveCurrentUserPhone() {
|
||||
private String resolveCurrentUserOaLoginId() {
|
||||
Long userId = AuthUtil.getUserId();
|
||||
if (userId != null) {
|
||||
User user = userService.getById(userId);
|
||||
if (user != null) {
|
||||
userService.decryptPhone(user);
|
||||
if (StringUtil.isNotBlank(user.getPhone()) && !user.getPhone().contains("*")) {
|
||||
return user.getPhone().trim();
|
||||
}
|
||||
if (StringUtil.isNotBlank(user.getAccount()) && user.getAccount().matches("^1\\d{10}$")) {
|
||||
return user.getAccount().trim();
|
||||
}
|
||||
}
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
String account = AuthUtil.getUserAccount();
|
||||
if (StringUtil.isNotBlank(account) && account.matches("^1\\d{10}$")) {
|
||||
return account.trim();
|
||||
User user = userService.getById(userId);
|
||||
if (user == null || StringUtil.isBlank(user.getOaLoginId())) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
return user.getOaLoginId().trim();
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -703,8 +694,8 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
processInstanceId = businessProcess.getProcessInstanceId();
|
||||
}
|
||||
try {
|
||||
String loginName = resolveCurrentUserPhone();
|
||||
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法获取审核跳转地址");
|
||||
String loginName = resolveCurrentUserOaLoginId();
|
||||
AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法获取审核跳转地址");
|
||||
String mkApprovalUrl = mkService.getMKApprovalUrl(processInstanceId, loginName);
|
||||
AssertUtils.notBlank(mkApprovalUrl, "获取mk审批页面链接异常");
|
||||
return mkApprovalUrl;
|
||||
@@ -921,6 +912,9 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
|
||||
@Override
|
||||
public IPage<ApprovalVO> queryMkApprovalList(IPage<ApprovalVO> page, ApprovalDTO param) {
|
||||
String loginName = resolveCurrentUserOaLoginId();
|
||||
AssertUtils.notBlank(loginName, "当前用户OA登录名为空,无法查询审核列表");
|
||||
param.setLoginName(loginName);
|
||||
if (MKDoc.RELATED.getCode().equals(param.getDocType())) {
|
||||
// 我参与的,调用流程列表接口
|
||||
MKProcessDTO processParam = approvalConvert.dto2mk(param);
|
||||
|
||||
+4
-3
@@ -204,7 +204,7 @@ public class DeptController extends BladeController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从OA按页同步部门(需先完成公司同步)
|
||||
* 从OA按页同步部门(需先完成公司同步;按公司 oaId 轮询查询)
|
||||
*/
|
||||
@IsAdmin
|
||||
@PostMapping("/sync-oa-department")
|
||||
@@ -214,8 +214,9 @@ public class DeptController extends BladeController {
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "20") Integer size,
|
||||
@RequestParam(defaultValue = "false") Boolean incremental,
|
||||
@RequestParam(required = false) String modifiedSince) {
|
||||
return R.data(oaSyncService.syncDepartmentPage(current, size, Boolean.TRUE.equals(incremental), modifiedSince));
|
||||
@RequestParam(required = false) String modifiedSince,
|
||||
@RequestParam(required = false) String companyOaId) {
|
||||
return R.data(oaSyncService.syncDepartmentPage(current, size, Boolean.TRUE.equals(incremental), modifiedSince, companyOaId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -49,8 +49,8 @@ public interface DeptConvert {
|
||||
@Mapping(source = "modified", target = "oaModified")
|
||||
Dept baseConvert(OACompanyResponse company);
|
||||
|
||||
@Mapping(source = "departmentname", target = "deptName")
|
||||
@Mapping(source = "departmentmark", target = "fullName")
|
||||
@Mapping(source = "departmentmark", target = "deptName")
|
||||
@Mapping(source = "departmentname", target = "fullName")
|
||||
@Mapping(source = "supdepid", target = "parentCode")
|
||||
@Mapping(source = "subcompanyid1", target = "belongCompanyCode")
|
||||
@Mapping(source = "created", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
+5
-3
@@ -70,6 +70,7 @@ public interface UserConvert {
|
||||
@Mapping(source = "lastname", target = "realName")
|
||||
@Mapping(source = "mobile", target = "phone")
|
||||
@Mapping(source = "email", target = "email")
|
||||
@Mapping(source = "loginid", target = "oaLoginId")
|
||||
@Mapping(source = "modified", target = "oaModified")
|
||||
User baseConvert(OAPersonResponse person);
|
||||
|
||||
@@ -147,7 +148,7 @@ public interface UserConvert {
|
||||
/**
|
||||
* oa人员转本系统用户部门
|
||||
* @param person
|
||||
* @param userMap 所有用户手机号与id map
|
||||
* @param userMap OA登录名与用户id
|
||||
* @return
|
||||
*/
|
||||
default UserDept person2userDept(OAPersonResponse person, Map<String, Long> userMap) {
|
||||
@@ -161,8 +162,9 @@ public interface UserConvert {
|
||||
userDept.setStatus(status);
|
||||
// 排序
|
||||
userDept.setSort(OAUtils.parseInt(person.getDsporder()));
|
||||
// 用户id
|
||||
userDept.setUserId(userMap.get(resolveAccount(person)));
|
||||
// 用户id,按 OA 登录名关联本批已新增或更新的用户
|
||||
String oaLoginId = person == null ? null : StringUtils.trimToNull(person.getLoginid());
|
||||
userDept.setUserId(oaLoginId == null ? null : userMap.get(oaLoginId));
|
||||
userDept.setSyncTime(new Date());
|
||||
return userDept;
|
||||
}
|
||||
|
||||
+3
-2
@@ -32,6 +32,7 @@ import org.springblade.core.tool.api.R;
|
||||
import org.springblade.system.pojo.entity.*;
|
||||
import org.springblade.system.service.*;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
@@ -218,9 +219,9 @@ public class SysClient implements ISysClient {
|
||||
|
||||
@Override
|
||||
@GetMapping(FEE_ITEMS)
|
||||
public R<List<FeeItem>> getFeeItems() {
|
||||
public R<List<FeeItem>> getFeeItems(@RequestParam(value = "includeDisabled", required = false) Boolean includeDisabled) {
|
||||
return R.data(feeItemService.list(Wrappers.<FeeItem>lambdaQuery()
|
||||
.eq(FeeItem::getStatus, 1)
|
||||
.eq(!Boolean.TRUE.equals(includeDisabled), FeeItem::getStatus, 1)
|
||||
.eq(FeeItem::getIsDeleted, 0)
|
||||
.orderByAsc(FeeItem::getFeeCategory, FeeItem::getName)));
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<result column="dept_code" property="deptCode"/>
|
||||
<result column="kingdee_dept_code" property="kingdeeDeptCode"/>
|
||||
<result column="contract_code_prefix" property="contractCodePrefix"/>
|
||||
<result column="contract_initial_serial" property="contractInitialSerial"/>
|
||||
<result column="short_name" property="shortName"/>
|
||||
<result column="pinyin_mnemonic" property="pinyinMnemonic"/>
|
||||
<result column="mnemonic_code" property="mnemonicCode"/>
|
||||
@@ -36,6 +37,7 @@
|
||||
<result column="dept_code" property="deptCode"/>
|
||||
<result column="kingdee_dept_code" property="kingdeeDeptCode"/>
|
||||
<result column="contract_code_prefix" property="contractCodePrefix"/>
|
||||
<result column="contract_initial_serial" property="contractInitialSerial"/>
|
||||
<result column="short_name" property="shortName"/>
|
||||
<result column="pinyin_mnemonic" property="pinyinMnemonic"/>
|
||||
<result column="mnemonic_code" property="mnemonicCode"/>
|
||||
|
||||
+20
@@ -34,6 +34,26 @@ import java.util.List;
|
||||
*/
|
||||
public interface MeasurementUnitMapper extends BaseMapper<MeasurementUnit> {
|
||||
|
||||
/**
|
||||
* 按编码查询计量单位(包含逻辑删除记录,用于新增恢复)。
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* 自定义分页
|
||||
*
|
||||
|
||||
+28
@@ -19,6 +19,34 @@
|
||||
<result column="remark" property="remark"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByUnitCodeIncludingDeleted" resultType="org.springblade.system.pojo.entity.MeasurementUnit">
|
||||
SELECT *
|
||||
FROM blade_measurement_unit
|
||||
WHERE unit_code = #{unitCode}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectByUnitNameIncludingDeleted" resultType="org.springblade.system.pojo.entity.MeasurementUnit">
|
||||
SELECT *
|
||||
FROM blade_measurement_unit
|
||||
WHERE unit_name = #{unitName}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<update id="restoreById">
|
||||
UPDATE blade_measurement_unit
|
||||
SET is_deleted = 0
|
||||
WHERE id = #{id}
|
||||
AND is_deleted = 1
|
||||
</update>
|
||||
|
||||
<update id="releaseUnitName">
|
||||
UPDATE blade_measurement_unit
|
||||
SET unit_name = #{unitName}
|
||||
WHERE id = #{id}
|
||||
AND is_deleted = 1
|
||||
</update>
|
||||
|
||||
<select id="selectMeasurementUnitPage" resultMap="measurementUnitResultMap">
|
||||
SELECT
|
||||
mmu.id,
|
||||
|
||||
+6
-6
@@ -45,13 +45,13 @@ public interface UserMapper extends BaseMapper<User> {
|
||||
/**
|
||||
* 自定义分页
|
||||
*
|
||||
* @param page
|
||||
* @param user
|
||||
* @param deptIdList
|
||||
* @param tenantId
|
||||
* @return
|
||||
* @param page 分页
|
||||
* @param user 查询条件
|
||||
* @param deptId 部门 ID(含自身及子孙,由 SQL FIND_IN_SET 过滤;为空则不过滤部门)
|
||||
* @param tenantId 租户 ID
|
||||
* @return 用户列表
|
||||
*/
|
||||
List<User> selectUserPage(IPage<User> page, @Param("user") User user, @Param("deptIdList") List<Long> deptIdList, @Param("tenantId") String tenantId);
|
||||
List<User> selectUserPage(IPage<User> page, @Param("user") User user, @Param("deptId") Long deptId, @Param("tenantId") String tenantId);
|
||||
|
||||
/**
|
||||
* 获取用户
|
||||
|
||||
+13
-11
@@ -26,6 +26,7 @@
|
||||
<result column="dept_id" property="deptId"/>
|
||||
<result column="post_id" property="postId"/>
|
||||
<result column="remark" property="remark"/>
|
||||
<result column="oa_login_id" property="oaLoginId"/>
|
||||
<result column="oa_person_id" property="oaPersonId"/>
|
||||
</resultMap>
|
||||
|
||||
@@ -61,20 +62,21 @@
|
||||
<if test="user.status!=null and user.status>=0">
|
||||
and bu.status = #{user.status}
|
||||
</if>
|
||||
<if test="deptIdList!=null and deptIdList.size>0">
|
||||
<if test="deptId != null">
|
||||
and bu.id in (
|
||||
SELECT
|
||||
user_id
|
||||
FROM
|
||||
blade_user_dept
|
||||
WHERE
|
||||
status = 1 and is_deleted = 0 and dept_id IN
|
||||
<foreach collection="deptIdList" index="index" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
select distinct bud.user_id
|
||||
from blade_user_dept bud
|
||||
where bud.status = 1
|
||||
and bud.is_deleted = 0
|
||||
and bud.dept_id in (
|
||||
select bd.id
|
||||
from blade_dept bd
|
||||
where bd.is_deleted = 0
|
||||
and (bd.id = #{deptId} or find_in_set(#{deptId}, bd.ancestors) > 0)
|
||||
)
|
||||
)
|
||||
</if>
|
||||
ORDER BY bu.id
|
||||
ORDER BY bu.create_time DESC, bu.id DESC
|
||||
</select>
|
||||
|
||||
<select id="getUser" resultMap="userResultMap">
|
||||
|
||||
+3
-3
@@ -149,16 +149,16 @@ public interface IDeptService extends IService<Dept> {
|
||||
boolean removeDept(String ids);
|
||||
|
||||
/**
|
||||
* 清除非保留顶级外的组织;保留 id=135 集团锚点,以及名称为「外部组织」的顶级
|
||||
* 清除非保留顶级外的组织;保留 id=1 集团锚点,以及名称为「外部组织」的顶级
|
||||
*
|
||||
* @return 删除数量
|
||||
*/
|
||||
int clearNonTopDepts();
|
||||
|
||||
/**
|
||||
* 确保双顶级:id=135 集团(编码 GWWL)+ 独立「外部组织」顶级
|
||||
* 确保双顶级:id=1 集团(编码 GWWL)+ 独立「外部组织」顶级
|
||||
*
|
||||
* @return 集团顶级组织(id=135)
|
||||
* @return 集团顶级组织(id=1)
|
||||
*/
|
||||
Dept ensureOaSyncRootDept();
|
||||
|
||||
|
||||
+4
-3
@@ -52,13 +52,14 @@ public interface IOASyncService {
|
||||
OaOrgSyncPageVO syncCompanyPage(int current, int size, boolean incremental, String modifiedSince);
|
||||
|
||||
/**
|
||||
* 按页从 OA 部门接口同步部门;最后一页完成后更新祖级列表
|
||||
* 按页从 OA 部门接口同步部门;须先完成公司同步,再按公司 oaId 轮询查询部门
|
||||
*
|
||||
* @param current 当前页,从 1 开始
|
||||
* @param current 当前页,从 1 开始(相对当前公司)
|
||||
* @param size 每页条数
|
||||
* @param incremental 是否增量同步(按本地最大 oaModified 过滤 OA)
|
||||
* @param modifiedSince 增量起始时间;首屏可空,后续分页回传首屏返回值
|
||||
* @param companyOaId 当前公司 OA 主键;首屏可空(自动取第一家),后续回传
|
||||
* @return 本页同步结果
|
||||
*/
|
||||
OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince);
|
||||
OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince, String companyOaId);
|
||||
}
|
||||
|
||||
+146
-24
@@ -34,6 +34,7 @@ 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;
|
||||
@@ -366,7 +367,9 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
|
||||
@Override
|
||||
public List<Dept> getDeptChild(Long deptId) {
|
||||
return baseMapper.selectList(Wrappers.<Dept>query().lambda().like(Dept::getAncestors, deptId));
|
||||
// 必须用 FIND_IN_SET:LIKE '%1%' 会误匹配雪花 ID 中的数字片段,导致子部门集合爆炸
|
||||
return baseMapper.selectList(Wrappers.<Dept>query().lambda()
|
||||
.apply("FIND_IN_SET({0}, ancestors)", String.valueOf(deptId)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -390,7 +393,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int clearNonTopDepts() {
|
||||
// 同步前清理:保留集团锚点 id=135,以及「外部组织」顶级;其余删除
|
||||
// 同步前清理:保留集团锚点 id=1,以及「外部组织」顶级;其余删除
|
||||
Dept groupRoot = this.ensureOaSyncRootDept();
|
||||
Dept externalRoot = this.ensureExternalOrgTopDept();
|
||||
Set<Long> keepIds = new HashSet<>();
|
||||
@@ -416,6 +419,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
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)) {
|
||||
@@ -447,14 +451,17 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
} else {
|
||||
boolean wronglyNamedAsExternal = OAConvertConstant.EXTERNAL_ORG_NAME.equals(rootDept.getDeptName());
|
||||
boolean needUpdate = wronglyNamedAsExternal
|
||||
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 (wronglyNamedAsExternal || StringUtil.isBlank(rootDept.getDeptName())) {
|
||||
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);
|
||||
@@ -468,6 +475,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
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);
|
||||
@@ -475,6 +483,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
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());
|
||||
}
|
||||
@@ -484,7 +493,63 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保存在独立的「外部组织」顶级(与 id=135 集团并存,仅保留一个同名顶级)
|
||||
* 将历史集团伪顶级 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.<Dept>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<Dept> externalTops = this.list(Wrappers.<Dept>lambdaQuery()
|
||||
@@ -540,11 +605,12 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理历史伪顶级(OACROOT);同名「桂物物流集团」仅清理非 id=135 的顶级
|
||||
* 清理历史伪顶级(OACROOT);同名旧集团仅清理非 id=1 的顶级
|
||||
*/
|
||||
private void removeLegacyOaSyncRoots() {
|
||||
List<Dept> legacyRoots = this.list(Wrappers.<Dept>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
|
||||
@@ -621,6 +687,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
}
|
||||
validateDeptCategory(dept);
|
||||
validateDeptCode(dept, parent);
|
||||
validateContractInitialSerial(dept);
|
||||
return saveOrUpdate(dept);
|
||||
}
|
||||
|
||||
@@ -808,6 +875,16 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
}
|
||||
}
|
||||
|
||||
private void validateContractInitialSerial(Dept dept) {
|
||||
Integer serial = dept.getContractInitialSerial();
|
||||
if (serial == null) {
|
||||
return;
|
||||
}
|
||||
if (serial < 1 || serial > 999) {
|
||||
throw new ServiceException("合同初始流水号范围为1到999");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDeptCode(Dept dept, Dept parent) {
|
||||
String deptCode = dept.getDeptCode();
|
||||
// 组织编码非必填,为空时存 null,避免唯一索引冲突
|
||||
@@ -975,7 +1052,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
boolean isCompany = this.isOaCompany(dept);
|
||||
boolean parentExists = isCompany
|
||||
? companyByOaId.containsKey(parentOaId)
|
||||
: departmentByOaId.containsKey(parentOaId) || companyByOaId.containsKey(parentOaId);
|
||||
: this.resolveOaDepartmentParent(dept, parentOaId, companyByOaId, departmentByOaId) != null;
|
||||
// 全量数据整理后仍找不到上级:公司、部门一律删除,不改挂保留
|
||||
if (!parentExists) {
|
||||
deleteIds.add(dept.getId());
|
||||
@@ -999,7 +1076,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
}
|
||||
Dept parent = this.isOaCompany(dept)
|
||||
? companyByOaId.get(parentOaId)
|
||||
: Optional.ofNullable(departmentByOaId.get(parentOaId)).orElse(companyByOaId.get(parentOaId));
|
||||
: this.resolveOaDepartmentParent(dept, parentOaId, companyByOaId, departmentByOaId);
|
||||
if (parent != null && deleteIds.contains(parent.getId())) {
|
||||
deleteIds.add(dept.getId());
|
||||
oaCascadeChanged = true;
|
||||
@@ -1111,7 +1188,10 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 OA 上级主键解析本系统父级
|
||||
* 根据 OA 上级主键解析本系统父级。
|
||||
* <p>
|
||||
* OA 公司与部门主键同属数字空间(如公司 id=19「运输事业部」与部门 id=19「投资发展部」),
|
||||
* 解析部门上级时必须按「同属公司」消歧,禁止跨公司误挂。
|
||||
*/
|
||||
private OaParentLink resolveOaParentLink(Dept dept, Map<String, Dept> companyByOaId, Map<String, Dept> departmentByOaId) {
|
||||
String parentOaId = dept.getOaSupSubComId();
|
||||
@@ -1138,21 +1218,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
log.warn("部门缺少上级OA主键,跳过层级重建 oaId={} deptId={}", dept.getOaId(), dept.getId());
|
||||
return null;
|
||||
}
|
||||
boolean parentIsDepartment = StringUtil.isNotBlank(dept.getParentCode())
|
||||
&& dept.getParentCode().startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX);
|
||||
boolean parentIsCompany = StringUtil.isNotBlank(dept.getParentCode())
|
||||
&& dept.getParentCode().startsWith(OAConvertConstant.COMPANY_OA_PREFIX);
|
||||
Dept parent;
|
||||
if (parentIsDepartment) {
|
||||
parent = departmentByOaId.get(parentOaId);
|
||||
} else if (parentIsCompany) {
|
||||
parent = companyByOaId.get(parentOaId);
|
||||
} else {
|
||||
parent = departmentByOaId.get(parentOaId);
|
||||
if (parent == null) {
|
||||
parent = companyByOaId.get(parentOaId);
|
||||
}
|
||||
}
|
||||
Dept parent = this.resolveOaDepartmentParent(dept, parentOaId, companyByOaId, departmentByOaId);
|
||||
if (parent == null || parent.getId() == null || Objects.equals(parent.getId(), dept.getId())) {
|
||||
log.warn("未找到上级组织,跳过层级重建 oaId={} oaSupSubComId={} deptId={}",
|
||||
dept.getOaId(), parentOaId, dept.getId());
|
||||
@@ -1163,6 +1229,62 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
return new OaParentLink(parent.getId(), this.resolveParentCode(parent, prefix, parentOaId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析部门的 OA 上级:优先同公司下的上级部门,其次所属/同号公司;避免公司与部门 oaId 撞号误挂。
|
||||
*/
|
||||
private Dept resolveOaDepartmentParent(Dept dept, String parentOaId,
|
||||
Map<String, Dept> companyByOaId, Map<String, Dept> departmentByOaId) {
|
||||
Dept belongCompany = this.findBelongCompany(dept, companyByOaId);
|
||||
// 根部门:oaSupSubComId 存的是所属公司 OA 主键
|
||||
if (belongCompany != null && StringUtil.equals(parentOaId, belongCompany.getOaId())) {
|
||||
return belongCompany;
|
||||
}
|
||||
Dept parentDept = departmentByOaId.get(parentOaId);
|
||||
if (parentDept != null && this.isSameOaCompanyTree(dept, parentDept, companyByOaId)) {
|
||||
return parentDept;
|
||||
}
|
||||
return companyByOaId.get(parentOaId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按所属公司编码或 OA 主键定位本地公司
|
||||
*/
|
||||
private Dept findBelongCompany(Dept dept, Map<String, Dept> companyByOaId) {
|
||||
if (dept == null || companyByOaId == null || companyByOaId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtil.isNotBlank(dept.getBelongCompanyCode())) {
|
||||
for (Dept company : companyByOaId.values()) {
|
||||
if (company != null && StringUtil.equals(company.getDeptCode(), dept.getBelongCompanyCode())) {
|
||||
return company;
|
||||
}
|
||||
}
|
||||
Dept byOaId = companyByOaId.get(dept.getBelongCompanyCode());
|
||||
if (byOaId != null) {
|
||||
return byOaId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个组织是否同属一家 OA 公司(用于部门 oaId 与公司 oaId 撞号消歧)
|
||||
*/
|
||||
private boolean isSameOaCompanyTree(Dept left, Dept right, Map<String, Dept> companyByOaId) {
|
||||
if (left == null || right == null) {
|
||||
return false;
|
||||
}
|
||||
if (StringUtil.isNotBlank(left.getBelongCompanyCode()) && StringUtil.isNotBlank(right.getBelongCompanyCode())) {
|
||||
return StringUtil.equals(left.getBelongCompanyCode(), right.getBelongCompanyCode());
|
||||
}
|
||||
Dept leftCompany = this.findBelongCompany(left, companyByOaId);
|
||||
Dept rightCompany = this.isOaCompany(right) ? right : this.findBelongCompany(right, companyByOaId);
|
||||
if (leftCompany == null || rightCompany == null || leftCompany.getId() == null || rightCompany.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(leftCompany.getId(), rightCompany.getId());
|
||||
}
|
||||
|
||||
private String resolveParentCode(Dept parent, String prefix, String parentOaId) {
|
||||
if (StringUtil.isNotBlank(parent.getDeptCode())) {
|
||||
return parent.getDeptCode();
|
||||
|
||||
+32
@@ -62,10 +62,42 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl<MeasurementUnitM
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submit(MeasurementUnit measurementUnit) {
|
||||
prepare(measurementUnit);
|
||||
prepareSubmitTarget(measurementUnit);
|
||||
validate(measurementUnit);
|
||||
return saveOrUpdate(measurementUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码或名称已存在且为软删除时:取消软删除并复用原主键,后续按更新落库;
|
||||
* 存在且未删除时仍由唯一校验提示「已存在」。
|
||||
*/
|
||||
private void prepareSubmitTarget(MeasurementUnit measurementUnit) {
|
||||
if (Func.isNotEmpty(measurementUnit.getId())) {
|
||||
return;
|
||||
}
|
||||
MeasurementUnit existingByCode = baseMapper.selectByUnitCodeIncludingDeleted(measurementUnit.getUnitCode());
|
||||
MeasurementUnit existingByName = baseMapper.selectByUnitNameIncludingDeleted(measurementUnit.getUnitName());
|
||||
MeasurementUnit toRestore = null;
|
||||
if (existingByCode != null && Objects.equals(existingByCode.getIsDeleted(), 1)) {
|
||||
toRestore = existingByCode;
|
||||
} else if (existingByName != null && Objects.equals(existingByName.getIsDeleted(), 1)
|
||||
&& (existingByCode == null || Objects.equals(existingByCode.getId(), existingByName.getId()))) {
|
||||
toRestore = existingByName;
|
||||
}
|
||||
if (toRestore == null) {
|
||||
return;
|
||||
}
|
||||
// 复用编码恢复时,若另一条软删除记录占用了目标名称,先释放名称唯一索引
|
||||
if (existingByName != null && Objects.equals(existingByName.getIsDeleted(), 1)
|
||||
&& !Objects.equals(existingByName.getId(), toRestore.getId())) {
|
||||
String releasedName = existingByName.getUnitName() + "_DEL_" + existingByName.getId();
|
||||
baseMapper.releaseUnitName(existingByName.getId(), releasedName);
|
||||
}
|
||||
baseMapper.restoreById(toRestore.getId());
|
||||
measurementUnit.setId(toRestore.getId());
|
||||
measurementUnit.setIsDeleted(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean changeStatus(Long id, Integer status) {
|
||||
|
||||
+266
-57
@@ -66,6 +66,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
private final IMKPushService mkPushService;
|
||||
private final IDataSyncRecordService dataSyncRecordService;
|
||||
private final OaUserListSyncHelper oaUserListSyncHelper;
|
||||
private final OaSyncRawDumpHelper oaSyncRawDumpHelper;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
@@ -74,16 +75,18 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
ComposeLogUtil.addLog(log);
|
||||
// 1. 同步公司和部门并记录
|
||||
Date startTime = this.syncAndRecord(DataSyncRecordUtils::createOAOrgFetch, queryStartTime -> {
|
||||
// 0. 确保双顶级:id=135 集团(GWWL) + 独立「外部组织」
|
||||
// 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. 更新部门祖级列表
|
||||
@@ -150,10 +153,10 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince) {
|
||||
public OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince, String companyOaId) {
|
||||
try {
|
||||
ComposeLogUtil.addLog(log);
|
||||
return this.syncDepartmentFromOaPage(current, size, incremental, modifiedSince);
|
||||
return this.syncDepartmentFromOaPage(current, size, incremental, modifiedSince, companyOaId);
|
||||
} finally {
|
||||
ComposeLogUtil.removeLastLog();
|
||||
}
|
||||
@@ -221,29 +224,28 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步部门
|
||||
* 同步部门:先整理已同步公司,再按公司 oaId 逐个轮询查询部门
|
||||
* @param startTime 查询开始时间
|
||||
*/
|
||||
private void syncDept(Date startTime) {
|
||||
String subCompanyIds = getOaSubCompanyIds(false);
|
||||
if (StringUtils.isEmpty(subCompanyIds)) {
|
||||
List<String> companyOaIds = listAllowedCompanyOaIds(false);
|
||||
if (CollectionUtil.isEmpty(companyOaIds)) {
|
||||
ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数");
|
||||
return;
|
||||
}
|
||||
// 未处理的数据
|
||||
List<Dept> notHandleList = new ArrayList<>();
|
||||
// 1. 设置查询参数
|
||||
OADepartmentSearch departmentSearch = buildDepartmentSearch(startTime, subCompanyIds);
|
||||
// 2. 分页查询并处理数据
|
||||
OAUtils.pageSyncHandler(departmentSearch, param -> oaClient.queryDepartmentPage(new OASearch<>(param)), response -> {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(response));
|
||||
return new ServiceException("调用OA接口查询部门信息失败");
|
||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
|
||||
// 处理数据
|
||||
OrgSyncCount syncCount = handleDept(list, false);
|
||||
notHandleList.addAll(syncCount.getNotHandledList());
|
||||
});
|
||||
// 3. 未处理的数据
|
||||
for (String companyOaId : companyOaIds) {
|
||||
ComposeLogUtil.getLastLog().info("按公司轮询同步部门 companyOaId={}", companyOaId);
|
||||
OADepartmentSearch departmentSearch = buildDepartmentSearch(startTime, companyOaId);
|
||||
OAUtils.pageSyncHandler(departmentSearch, param -> oaClient.queryDepartmentPage(new OASearch<>(param)), response -> {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 companyOaId={} {}", companyOaId, JSON.toJSONString(response));
|
||||
return new ServiceException("调用OA接口查询部门信息失败");
|
||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
|
||||
OrgSyncCount syncCount = handleDept(list, false);
|
||||
notHandleList.addAll(syncCount.getNotHandledList());
|
||||
});
|
||||
}
|
||||
if (CollectionUtil.isNotEmpty(notHandleList)) {
|
||||
ComposeLogUtil.getLastLog().warn("同步部门,还有未处理的数据:{}", JSON.toJSONString(notHandleList));
|
||||
}
|
||||
@@ -261,7 +263,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 OA 人员列表同步组织与人员
|
||||
* 从 OA 人员列表同步人员并绑定已有组织(不维护组织树,须先同步组织架构)
|
||||
*
|
||||
* @param startTime 增量查询开始时间,为空则全量
|
||||
* @return 处理的人员数量
|
||||
@@ -282,7 +284,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按页从 OA 人员列表同步组织与人员
|
||||
* 按页从 OA 人员列表同步人员并绑定已有组织(不维护组织树,须先同步组织架构)
|
||||
*
|
||||
* @param current 当前页
|
||||
* @param size 每页条数
|
||||
@@ -293,6 +295,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);
|
||||
@@ -306,6 +312,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
List<OAPersonResponse> 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);
|
||||
@@ -323,6 +332,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());
|
||||
@@ -341,9 +353,12 @@ 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=135 集团(GWWL) + 独立「外部组织」
|
||||
// 首屏确保双顶级:id=1 集团(GWWL) + 独立「外部组织」
|
||||
if (pageNo == 1) {
|
||||
deptService.ensureOaSyncRootDept();
|
||||
if (!incremental) {
|
||||
oaSyncRawDumpHelper.beginOrgFullDump();
|
||||
}
|
||||
}
|
||||
IncrementalStart incrementalStart = resolveIncrementalStart(incremental, modifiedSince, DeptCategory.COMPANY);
|
||||
OACompanySearch companySearch = buildCompanySearch(incrementalStart.getStartTime());
|
||||
@@ -358,6 +373,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
List<OACompanyResponse> 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()));
|
||||
@@ -365,7 +383,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();
|
||||
}
|
||||
@@ -378,54 +397,105 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按页从 OA 部门接口同步部门
|
||||
* 按页从 OA 部门接口同步部门:公司数据整理完成后,按公司 oaId 逐个轮询查询
|
||||
*
|
||||
* @param current 当前页
|
||||
* @param current 当前页(相对当前公司)
|
||||
* @param size 每页条数
|
||||
* @param incremental 是否增量同步
|
||||
* @param modifiedSince 增量起始时间(后续分页回传)
|
||||
* @param companyOaId 当前公司 OA 主键;空则从第一家开始
|
||||
* @return 本页同步结果
|
||||
*/
|
||||
private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size, boolean incremental, String modifiedSince) {
|
||||
private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size, boolean incremental, String modifiedSince,
|
||||
String companyOaId) {
|
||||
int pageNo = current < 1 ? 1 : current;
|
||||
int pageSize = size < 1 ? 20 : Math.min(size, 200);
|
||||
String subCompanyIds = getOaSubCompanyIds(true);
|
||||
if (StringUtils.isEmpty(subCompanyIds)) {
|
||||
// 先整理已同步企业列表,再按企业轮询
|
||||
List<String> companyOaIds = listAllowedCompanyOaIds(true);
|
||||
if (CollectionUtil.isEmpty(companyOaIds)) {
|
||||
ComposeLogUtil.getLastLog().warn("未查询到已同步的OA公司,跳过部门同步");
|
||||
OaOrgSyncPageVO emptyPageVO = buildOrgSyncPageVO("department", pageNo, pageSize, 0L, 0, OrgSyncCount.empty());
|
||||
emptyPageVO.setFinished(true);
|
||||
emptyPageVO.setCompanyFinished(true);
|
||||
emptyPageVO.setCompanyIndex(0);
|
||||
emptyPageVO.setCompanyTotal(0);
|
||||
if (!incremental) {
|
||||
emptyPageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump());
|
||||
}
|
||||
return emptyPageVO;
|
||||
}
|
||||
int companyIndex = 0;
|
||||
String currentCompanyOaId = companyOaIds.get(0);
|
||||
if (StringUtils.isNotBlank(companyOaId)) {
|
||||
int foundIndex = companyOaIds.indexOf(companyOaId.trim());
|
||||
if (foundIndex >= 0) {
|
||||
companyIndex = foundIndex;
|
||||
currentCompanyOaId = companyOaIds.get(foundIndex);
|
||||
} else {
|
||||
ComposeLogUtil.getLastLog().warn("回传公司oaId不在待同步列表中,回退第一家 companyOaId={}", companyOaId);
|
||||
}
|
||||
}
|
||||
IncrementalStart incrementalStart = resolveIncrementalStart(incremental, modifiedSince, DeptCategory.DEPT);
|
||||
OADepartmentSearch departmentSearch = buildDepartmentSearch(incrementalStart.getStartTime(), subCompanyIds);
|
||||
// 查询部门时只传当前公司的 oa_id
|
||||
OADepartmentSearch departmentSearch = buildDepartmentSearch(incrementalStart.getStartTime(), currentCompanyOaId);
|
||||
departmentSearch.setCurPage(pageNo);
|
||||
departmentSearch.setPageSize(pageSize);
|
||||
ComposeLogUtil.getLastLog().info("按公司查询OA部门 companyOaId={} companyIndex={}/{} page={}/{}",
|
||||
currentCompanyOaId, companyIndex + 1, companyOaIds.size(), pageNo, pageSize);
|
||||
OAResponse<OADepartmentResponse> oaResponse = oaClient.queryDepartmentPage(new OASearch<>(departmentSearch));
|
||||
if (oaResponse == null || !OAConstant.OA_SUCCESS_CODE.equals(oaResponse.getCode()) || oaResponse.getData() == null) {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(oaResponse));
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 companyOaId={} {}",
|
||||
currentCompanyOaId, JSON.toJSONString(oaResponse));
|
||||
throw new ServiceException("调用OA接口查询部门信息失败");
|
||||
}
|
||||
OAResponseData<OADepartmentResponse> responseData = oaResponse.getData();
|
||||
List<OADepartmentResponse> 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()));
|
||||
ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据 companyOaId={}:{}",
|
||||
currentCompanyOaId, JSON.toJSONString(syncCount.getNotHandledList()));
|
||||
}
|
||||
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("department", pageNo, pageSize, totalSize, oaDepartments.size(), syncCount);
|
||||
pageVO.setModifiedSince(incrementalStart.getModifiedSince());
|
||||
if (Boolean.TRUE.equals(pageVO.getFinished())) {
|
||||
// 部门分批同步完成后,重建层级、清理无法挂载数据并刷新祖级列表
|
||||
deptService.rebuildOaHierarchy();
|
||||
deptService.removeOaOrphansWithoutParent();
|
||||
deptService.updateAncestors(null);
|
||||
pageVO.setCompanyOaId(currentCompanyOaId);
|
||||
pageVO.setCompanyIndex(companyIndex + 1);
|
||||
pageVO.setCompanyTotal(companyOaIds.size());
|
||||
boolean companyPageFinished = Boolean.TRUE.equals(pageVO.getFinished());
|
||||
if (companyPageFinished) {
|
||||
if (companyIndex + 1 < companyOaIds.size()) {
|
||||
// 当前公司部门已拉完:先按 OA 主键校正父子挂载,再进入下一公司
|
||||
pageVO.setCompanyFinished(true);
|
||||
pageVO.setNextCompanyOaId(companyOaIds.get(companyIndex + 1));
|
||||
pageVO.setFinished(false);
|
||||
deptService.rebuildOaHierarchy();
|
||||
} else {
|
||||
// 全部公司轮询完成:清理白名单外组织、重建层级、清理无法挂载数据并刷新祖级列表
|
||||
pageVO.setCompanyFinished(true);
|
||||
pageVO.setNextCompanyOaId(null);
|
||||
pageVO.setFinished(true);
|
||||
this.removeOutsideWhitelistOrgs();
|
||||
deptService.rebuildOaHierarchy();
|
||||
deptService.removeOaOrphansWithoutParent();
|
||||
deptService.updateAncestors(null);
|
||||
if (!incremental) {
|
||||
pageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pageVO.setCompanyFinished(false);
|
||||
pageVO.setNextCompanyOaId(currentCompanyOaId);
|
||||
pageVO.setFinished(false);
|
||||
}
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||
ComposeLogUtil.getLastLog().info("OA部门分页同步完成 mode={} {}/{},成功{},跳过{}",
|
||||
incremental ? "incremental" : "full", pageNo, totalSize,
|
||||
syncCount.getSyncedCount(), syncCount.getSkippedCount());
|
||||
ComposeLogUtil.getLastLog().info("OA部门分页同步完成 mode={} companyOaId={} company={}/{} page={}/{},成功{},跳过{}",
|
||||
incremental ? "incremental" : "full", currentCompanyOaId, companyIndex + 1, companyOaIds.size(),
|
||||
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
|
||||
return pageVO;
|
||||
}
|
||||
|
||||
@@ -574,15 +644,15 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
/**
|
||||
* 组装 OA 部门分页查询参数
|
||||
*
|
||||
* @param startTime 增量查询开始时间
|
||||
* @param subCompanyIds 子公司 id 列表
|
||||
* @param startTime 增量查询开始时间
|
||||
* @param companyOaId 当前公司 OA 主键(单次只传一家公司)
|
||||
* @return 查询参数
|
||||
*/
|
||||
private OADepartmentSearch buildDepartmentSearch(Date startTime, String subCompanyIds) {
|
||||
private OADepartmentSearch buildDepartmentSearch(Date startTime, String companyOaId) {
|
||||
OADepartmentSearch departmentSearch = new OADepartmentSearch();
|
||||
departmentSearch.setCurPage(1);
|
||||
departmentSearch.setPageSize(20);
|
||||
departmentSearch.setSubcompanyid1(subCompanyIds);
|
||||
departmentSearch.setSubcompanyid1(companyOaId);
|
||||
if (startTime != null) {
|
||||
departmentSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
|
||||
}
|
||||
@@ -623,6 +693,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
Set<String> companyNames = syncAll ? Collections.emptySet() : getCompanyNames();
|
||||
List<Dept> allParam = oaCompanies.stream()
|
||||
.filter(this::isAllowedOaCompany)
|
||||
.filter(company -> syncAll || companyNames.contains(company.getSubcompanyname()))
|
||||
.map(deptConvert::company2dept)
|
||||
.toList();
|
||||
@@ -679,16 +750,18 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
return OrgSyncCount.empty();
|
||||
}
|
||||
Map<String, Dept> companyByOaId = loadCompanyByOaId();
|
||||
Set<String> allowedCompanyOaIds = resolveAllowedCompanyOaIds(companyByOaId);
|
||||
String rootCompanyId = getRootCompanyId();
|
||||
if (!syncAll && rootCompanyId == null) {
|
||||
return new OrgSyncCount(0, oaDepts.size(), Collections.emptyList());
|
||||
}
|
||||
Set<String> rootCompanyDeptNames = syncAll ? Collections.emptySet() : getRootCompanyDeptNames();
|
||||
List<Dept> 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())))
|
||||
|| (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentmark())))
|
||||
.map(dept -> deptConvert.dept2dept(dept, companyByOaId))
|
||||
.toList();
|
||||
int filteredSkipCount = oaDepts.size() - allParam.size();
|
||||
@@ -733,9 +806,11 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
String parentOaId = dept.getOaSupSubComId();
|
||||
Dept belongCompany = companyByOaId.values().stream()
|
||||
.filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode()))
|
||||
.filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode())
|
||||
|| StringUtils.equals(company.getOaId(), dept.getBelongCompanyCode()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
// 根部门:oaSupSubComId 即所属公司 OA 主键,必须挂公司(OA 公司/部门主键会撞号)
|
||||
if (belongCompany != null && (OAConstant.ROOT_COMPANY_ID.equals(parentOaId)
|
||||
|| StringUtils.equals(parentOaId, belongCompany.getOaId()))) {
|
||||
dept.setParentCode(belongCompany.getDeptCode());
|
||||
@@ -746,9 +821,19 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
if (parentDept == null) {
|
||||
parentDept = existingByOaId.get(parentOaId);
|
||||
}
|
||||
// 跨公司同号部门不可作为上级(例如总部部门 id=19 与公司 id=19)
|
||||
if (parentDept != null && StringUtils.isNotBlank(dept.getBelongCompanyCode())
|
||||
&& StringUtils.isNotBlank(parentDept.getBelongCompanyCode())
|
||||
&& !StringUtils.equals(dept.getBelongCompanyCode(), parentDept.getBelongCompanyCode())) {
|
||||
parentDept = null;
|
||||
}
|
||||
Dept parentCompany = StringUtils.isNotBlank(parentOaId) ? companyByOaId.get(parentOaId) : null;
|
||||
if (parentDept != null && StringUtils.isNotBlank(parentDept.getDeptCode())) {
|
||||
dept.setParentCode(parentDept.getDeptCode());
|
||||
dept.setParentId(parentDept.getId());
|
||||
} else if (parentCompany != null && StringUtils.isNotBlank(parentCompany.getDeptCode())) {
|
||||
dept.setParentCode(parentCompany.getDeptCode());
|
||||
dept.setParentId(parentCompany.getId());
|
||||
} else if (belongCompany != null) {
|
||||
// 上级部门尚未就绪时先挂所属公司,分批结束后按 OA 主键重建
|
||||
dept.setParentCode(belongCompany.getDeptCode());
|
||||
@@ -836,8 +921,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
.eq(Dept::getDeptCategory, deptCategory.getCode())
|
||||
.isNotNull(Dept::getOaId)
|
||||
.ne(Dept::getOaId, "")
|
||||
.select(Dept::getId, Dept::getDeptCode, Dept::getOaModified, Dept::getParentId,
|
||||
Dept::getOaId, Dept::getBelongCompanyCode, Dept::getParentCode, Dept::getOaSupSubComId));
|
||||
.select(Dept::getId, Dept::getDeptCode, Dept::getDeptName, Dept::getFullName,
|
||||
Dept::getOaModified, Dept::getParentId, Dept::getOaId, Dept::getBelongCompanyCode,
|
||||
Dept::getParentCode, Dept::getOaSupSubComId));
|
||||
Map<String, Dept> byOaId = new HashMap<>();
|
||||
for (Dept dept : deptList) {
|
||||
if (StringUtils.isNotBlank(dept.getOaId())) {
|
||||
@@ -962,7 +1048,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
|
||||
/**
|
||||
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。
|
||||
* 组织编码已按新规则重算且与库中不一致时,即使 modified 相同也要更新。
|
||||
* 组织编码已按新规则重算、父子挂载不一致、或名称字段与当前映射不一致时,即使 modified 相同也要更新。
|
||||
*/
|
||||
private boolean shouldSkipOaUpdate(Dept existing, Dept incoming) {
|
||||
if (existing == null) {
|
||||
@@ -972,6 +1058,23 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
&& !StringUtils.equals(existing.getDeptCode(), incoming.getDeptCode())) {
|
||||
return false;
|
||||
}
|
||||
if (incoming != null && incoming.getParentId() != null
|
||||
&& !Objects.equals(existing.getParentId(), incoming.getParentId())) {
|
||||
return false;
|
||||
}
|
||||
if (incoming != null && StringUtils.isNotBlank(incoming.getParentCode())
|
||||
&& !StringUtils.equals(StringUtils.trimToEmpty(existing.getParentCode()),
|
||||
StringUtils.trimToEmpty(incoming.getParentCode()))) {
|
||||
return false;
|
||||
}
|
||||
if (incoming != null && !StringUtils.equals(StringUtils.trimToEmpty(existing.getDeptName()),
|
||||
StringUtils.trimToEmpty(incoming.getDeptName()))) {
|
||||
return false;
|
||||
}
|
||||
if (incoming != null && !StringUtils.equals(StringUtils.trimToEmpty(existing.getFullName()),
|
||||
StringUtils.trimToEmpty(incoming.getFullName()))) {
|
||||
return false;
|
||||
}
|
||||
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
|
||||
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
|
||||
if (localModified.isEmpty() || remoteModified.isEmpty()) {
|
||||
@@ -1012,12 +1115,112 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OA 部门查询使用的子公司 id
|
||||
* 是否允许同步的 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<String> resolveAllowedCompanyOaIds(Map<String, Dept> companyByOaId) {
|
||||
Set<String> 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<Dept> oaCompanyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
.isNotNull(Dept::getOaId)
|
||||
.ne(Dept::getOaId, ""));
|
||||
Map<String, Dept> companyByOaId = new HashMap<>();
|
||||
for (Dept company : oaCompanyList) {
|
||||
if (StringUtils.isNotBlank(company.getOaId())) {
|
||||
companyByOaId.put(company.getOaId(), company);
|
||||
}
|
||||
}
|
||||
Set<String> allowedCompanyOaIds = resolveAllowedCompanyOaIds(companyByOaId);
|
||||
Set<Long> deleteIds = new HashSet<>();
|
||||
for (Dept company : oaCompanyList) {
|
||||
if (!allowedCompanyOaIds.contains(company.getOaId())) {
|
||||
deleteIds.add(company.getId());
|
||||
}
|
||||
}
|
||||
Map<String, String> companyCodeToOaId = new HashMap<>();
|
||||
for (Dept company : oaCompanyList) {
|
||||
if (StringUtils.isNotBlank(company.getDeptCode()) && StringUtils.isNotBlank(company.getOaId())) {
|
||||
companyCodeToOaId.put(company.getDeptCode(), company.getOaId());
|
||||
}
|
||||
}
|
||||
List<Dept> oaDeptList = deptService.list(Wrappers.<Dept>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<Dept> 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 主键列表(公司阶段完成后调用)
|
||||
*
|
||||
* @param syncAll true=取已同步的全部 OA 公司;false=仅取字典配置的公司
|
||||
* @return 逗号分隔的 OA 公司 id
|
||||
* @return 白名单内公司 oaId 列表(去重、保序)
|
||||
*/
|
||||
private String getOaSubCompanyIds(boolean syncAll) {
|
||||
private List<String> listAllowedCompanyOaIds(boolean syncAll) {
|
||||
List<Dept> companyList;
|
||||
if (syncAll) {
|
||||
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
@@ -1028,7 +1231,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
} else {
|
||||
Set<String> companyNames = this.getCompanyNames();
|
||||
if (companyNames.isEmpty()) {
|
||||
return null;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.in(Dept::getDeptName, companyNames)
|
||||
@@ -1037,11 +1240,17 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
.ne(Dept::getOaId, "")
|
||||
);
|
||||
}
|
||||
return companyList.stream()
|
||||
.map(this::resolveOaCompanyId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.collect(Collectors.joining(","));
|
||||
Map<String, Dept> companyByOaId = new LinkedHashMap<>();
|
||||
for (Dept company : companyList) {
|
||||
String oaId = this.resolveOaCompanyId(company);
|
||||
if (StringUtils.isNotBlank(oaId)) {
|
||||
companyByOaId.putIfAbsent(oaId, company);
|
||||
}
|
||||
}
|
||||
Set<String> allowedCompanyOaIds = resolveAllowedCompanyOaIds(companyByOaId);
|
||||
return companyByOaId.keySet().stream()
|
||||
.filter(allowedCompanyOaIds::contains)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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<OAPersonResponse> collectingPersons = Collections.synchronizedList(new ArrayList<>());
|
||||
private volatile boolean collecting;
|
||||
|
||||
/**
|
||||
* 机构全量同步会话收集的公司/部门原始数据
|
||||
*/
|
||||
private final List<OACompanyResponse> collectingCompanies = Collections.synchronizedList(new ArrayList<>());
|
||||
private final List<OADepartmentResponse> 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<OAPersonResponse> oaPersons) {
|
||||
if (!collecting || oaPersons == null || oaPersons.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
collectingPersons.addAll(oaPersons);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集本页公司原始数据
|
||||
*/
|
||||
public void collectCompanies(List<OACompanyResponse> oaCompanies) {
|
||||
if (!collectingOrg || oaCompanies == null || oaCompanies.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
collectingCompanies.addAll(oaCompanies);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集本页部门原始数据
|
||||
*/
|
||||
public void collectDepartments(List<OADepartmentResponse> oaDepartments) {
|
||||
if (!collectingOrg || oaDepartments == null || oaDepartments.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
collectingDepartments.addAll(oaDepartments);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量同步结束:先写入人员层级 JSON,再异步补齐公司/部门接口原始层级数据
|
||||
*
|
||||
* @return 人员原始层级 JSON 文件路径,失败时返回 null
|
||||
*/
|
||||
public synchronized String finishFullDump() {
|
||||
if (!collecting) {
|
||||
return null;
|
||||
}
|
||||
List<OAPersonResponse> persons = new ArrayList<>(collectingPersons);
|
||||
collecting = false;
|
||||
collectingPersons.clear();
|
||||
try {
|
||||
String stamp = DateUtil.format(new Date(), "yyyyMMddHHmmss");
|
||||
Map<String, Object> 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<OACompanyResponse> companies = new ArrayList<>(collectingCompanies);
|
||||
List<OADepartmentResponse> 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<OACompanyResponse> companies = fetchAllCompanies();
|
||||
List<OADepartmentResponse> 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<OACompanyResponse> companies,
|
||||
List<OADepartmentResponse> departments) throws IOException {
|
||||
Map<String, Object> 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<OACompanyResponse> fetchAllCompanies() {
|
||||
List<OACompanyResponse> 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<OADepartmentResponse> fetchAllDepartments(List<OACompanyResponse> companies) {
|
||||
List<OADepartmentResponse> 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<Map<String, Object>> buildCompanyTree(List<OACompanyResponse> companies) {
|
||||
Map<String, Map<String, Object>> nodeMap = new LinkedHashMap<>();
|
||||
for (OACompanyResponse company : companies) {
|
||||
if (company == null || StringUtils.isBlank(company.getId())) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> 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<Map<String, Object>>());
|
||||
nodeMap.put(company.getId(), node);
|
||||
}
|
||||
return linkChildren(nodeMap);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildDepartmentTree(List<OADepartmentResponse> departments) {
|
||||
Map<String, Map<String, Object>> nodeMap = new LinkedHashMap<>();
|
||||
for (OADepartmentResponse department : departments) {
|
||||
if (department == null || StringUtils.isBlank(department.getId())) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> 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<Map<String, Object>>());
|
||||
nodeMap.put(department.getId(), node);
|
||||
}
|
||||
return linkChildren(nodeMap);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildPersonTree(List<OAPersonResponse> persons) {
|
||||
Map<String, Map<String, Object>> companyMap = new LinkedHashMap<>();
|
||||
Map<String, Map<String, Object>> 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<String, Object> companyNode = companyMap.computeIfAbsent(companyId, key -> {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", key);
|
||||
node.put("parentId", "0");
|
||||
node.put("type", "company");
|
||||
node.put("name", StringUtils.defaultIfBlank(person.getSubcompanyname(), key));
|
||||
Map<String, Object> 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<Map<String, Object>>());
|
||||
return node;
|
||||
});
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> companyChildren = (List<Map<String, Object>>) companyNode.get("children");
|
||||
Map<String, Object> deptNode = deptMap.computeIfAbsent(deptId, key -> {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", key);
|
||||
node.put("parentId", companyId);
|
||||
node.put("type", "department");
|
||||
node.put("name", StringUtils.defaultIfBlank(person.getDepartmentname(), key));
|
||||
Map<String, Object> 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<Map<String, Object>>());
|
||||
companyChildren.add(node);
|
||||
return node;
|
||||
});
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> deptChildren = (List<Map<String, Object>>) deptNode.get("children");
|
||||
Map<String, Object> 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<Map<String, Object>> linkChildren(Map<String, Map<String, Object>> nodeMap) {
|
||||
List<Map<String, Object>> roots = new ArrayList<>();
|
||||
for (Map<String, Object> node : nodeMap.values()) {
|
||||
String parentId = Objects.toString(node.get("parentId"), "0");
|
||||
Map<String, Object> parent = nodeMap.get(parentId);
|
||||
if (parent != null && !Objects.equals(parent.get("id"), node.get("id"))) {
|
||||
((List<Map<String, Object>>) 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<String, Object> 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();
|
||||
}
|
||||
|
||||
}
|
||||
+381
-260
@@ -30,7 +30,6 @@ import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springblade.common.constant.DataStatusEnum;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.constant.BladeConstant;
|
||||
import org.springblade.core.tool.utils.DigestUtil;
|
||||
@@ -53,7 +52,6 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -66,10 +64,9 @@ import java.util.stream.Collectors;
|
||||
import static org.springblade.common.constant.CommonConstant.DEFAULT_PARAM_PASSWORD;
|
||||
import static org.springblade.common.constant.CommonConstant.DEFAULT_PARAM_ROLE;
|
||||
import static org.springblade.common.constant.CommonConstant.DEFAULT_ROLE;
|
||||
import static org.springblade.common.constant.CommonConstant.YES;
|
||||
|
||||
/**
|
||||
* 从 OA 人员列表提取组织并同步人员
|
||||
* OA 人员同步:绑定已有组织机构,不维护组织树
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@@ -86,104 +83,312 @@ public class OaUserListSyncHelper {
|
||||
private final IRoleService roleService;
|
||||
|
||||
/**
|
||||
* 从人员数据提取二级公司、三级部门,并挂到集团顶级(id=135/GWWL) 下
|
||||
* 加载已有组织索引,并将人员旁路产生的 OAC/OAD 存量归并到正式组织。
|
||||
* <p>人员同步不再新建/更新组织树,须先执行组织架构同步。</p>
|
||||
*
|
||||
* @param oaPersons OA人员
|
||||
* @return 组织索引
|
||||
* @return 组织索引(仅含已有组织)
|
||||
*/
|
||||
public OaOrgIndex syncOrgsFromPersons(List<OAPersonResponse> oaPersons) {
|
||||
OaOrgIndex orgIndex = new OaOrgIndex();
|
||||
if (CollectionUtil.isEmpty(oaPersons)) {
|
||||
return orgIndex;
|
||||
}
|
||||
Date orgSyncStart = new Date();
|
||||
Dept rootCompany = this.getOrCreateRootCompany();
|
||||
String tenantId = resolveTenantId();
|
||||
Map<String, OAPersonResponse> companyPersonMap = new LinkedHashMap<>();
|
||||
Map<String, OAPersonResponse> departmentPersonMap = new LinkedHashMap<>();
|
||||
for (OAPersonResponse oaPerson : oaPersons) {
|
||||
String companyKey = resolveCompanyKey(oaPerson);
|
||||
if (StringUtils.isNotBlank(companyKey) && StringUtils.isNotBlank(oaPerson.getSubcompanyname())) {
|
||||
companyPersonMap.putIfAbsent(companyKey, oaPerson);
|
||||
}
|
||||
String departmentKey = resolveDepartmentKey(oaPerson);
|
||||
if (StringUtils.isNotBlank(departmentKey) && StringUtils.isNotBlank(oaPerson.getDepartmentname())
|
||||
&& StringUtils.isNotBlank(oaPerson.getSubcompanyname())) {
|
||||
departmentPersonMap.putIfAbsent(departmentKey, oaPerson);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Dept> existingCompanyByCode = new HashMap<>();
|
||||
Map<String, Dept> existingCompanyByName = new HashMap<>();
|
||||
deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getParentId, rootCompany.getId())
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
).forEach(dept -> {
|
||||
if (StringUtils.isNotBlank(dept.getDeptCode())) {
|
||||
existingCompanyByCode.put(dept.getDeptCode(), dept);
|
||||
}
|
||||
if (StringUtils.isNotBlank(dept.getDeptName())) {
|
||||
existingCompanyByName.put(dept.getDeptName(), dept);
|
||||
}
|
||||
});
|
||||
List<Dept> addCompanies = new ArrayList<>();
|
||||
List<Dept> updateCompanies = new ArrayList<>();
|
||||
companyPersonMap.forEach((companyKey, oaPerson) -> {
|
||||
String oaCode = buildCompanyCode(oaPerson, companyKey);
|
||||
Dept existing = existingCompanyByCode.get(oaCode);
|
||||
if (existing == null) {
|
||||
existing = existingCompanyByName.get(oaPerson.getSubcompanyname());
|
||||
}
|
||||
Dept company = this.upsertOrg(oaPerson.getSubcompanyname(), oaCode, existing, rootCompany, tenantId,
|
||||
DeptCategory.COMPANY, addCompanies, updateCompanies);
|
||||
orgIndex.companyByOaId.put(companyKey, company);
|
||||
});
|
||||
this.saveOrgs(addCompanies, updateCompanies, DeptCategory.COMPANY);
|
||||
|
||||
Map<String, Dept> existingDeptByCode = new HashMap<>();
|
||||
Map<String, Dept> existingDeptByParentAndName = new HashMap<>();
|
||||
List<Long> companyIds = orgIndex.companyByOaId.values().stream()
|
||||
.map(Dept::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(companyIds)) {
|
||||
deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.in(Dept::getParentId, companyIds)
|
||||
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
|
||||
).forEach(dept -> {
|
||||
if (StringUtils.isNotBlank(dept.getDeptCode())) {
|
||||
existingDeptByCode.put(dept.getDeptCode(), dept);
|
||||
}
|
||||
if (dept.getParentId() != null && StringUtils.isNotBlank(dept.getDeptName())) {
|
||||
existingDeptByParentAndName.put(dept.getParentId() + "#" + dept.getDeptName(), dept);
|
||||
}
|
||||
});
|
||||
}
|
||||
List<Dept> addDepartments = new ArrayList<>();
|
||||
List<Dept> updateDepartments = new ArrayList<>();
|
||||
departmentPersonMap.forEach((departmentKey, oaPerson) -> {
|
||||
Dept parentCompany = orgIndex.findCompany(oaPerson);
|
||||
if (parentCompany == null || parentCompany.getId() == null) {
|
||||
return;
|
||||
}
|
||||
String oaCode = buildDepartmentCode(oaPerson, departmentKey);
|
||||
Dept existing = existingDeptByCode.get(oaCode);
|
||||
if (existing == null) {
|
||||
existing = existingDeptByParentAndName.get(parentCompany.getId() + "#" + oaPerson.getDepartmentname());
|
||||
}
|
||||
Dept department = this.upsertOrg(oaPerson.getDepartmentname(), oaCode, existing, parentCompany, tenantId,
|
||||
DeptCategory.DEPT, addDepartments, updateDepartments);
|
||||
orgIndex.deptByOaId.put(departmentKey, department);
|
||||
orgIndex.deptByCompanyAndName.put(resolveCompanyKey(oaPerson) + "#" + oaPerson.getDepartmentname(), department);
|
||||
});
|
||||
this.saveOrgs(addDepartments, updateDepartments, DeptCategory.DEPT);
|
||||
deptService.updateAncestors(orgSyncStart);
|
||||
ComposeLogUtil.getLastLog().info("同步人员提取组织完成,二级公司{}个,三级部门{}个",
|
||||
this.repairPersonBypassOrgs();
|
||||
this.loadExistingOrgIndex(orgIndex);
|
||||
ComposeLogUtil.getLastLog().info("人员同步绑定已有组织,二级公司{}个,三级部门{}个",
|
||||
orgIndex.companyByOaId.size(), orgIndex.deptByOaId.size());
|
||||
return orgIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OAC/OAD 旁路节点上的人员关系迁移到同 oa_id 的正式组织,并逻辑删除旁路节点
|
||||
*/
|
||||
private void repairPersonBypassOrgs() {
|
||||
List<Dept> companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode()));
|
||||
List<Dept> departmentList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode()));
|
||||
Map<String, Dept> officialCompanyByOaId = new HashMap<>();
|
||||
Map<String, Dept> officialDeptByOaId = new HashMap<>();
|
||||
Map<Long, Dept> companyById = new HashMap<>();
|
||||
for (Dept company : companyList) {
|
||||
if (company.getId() != null) {
|
||||
companyById.put(company.getId(), company);
|
||||
}
|
||||
if (!isPersonBypassCode(company.getDeptCode(), DeptCategory.COMPANY)) {
|
||||
this.putPreferredByOaId(officialCompanyByOaId, company, DeptCategory.COMPANY);
|
||||
}
|
||||
}
|
||||
for (Dept department : departmentList) {
|
||||
if (!isPersonBypassCode(department.getDeptCode(), DeptCategory.DEPT)) {
|
||||
this.putPreferredByOaId(officialDeptByOaId, department, DeptCategory.DEPT);
|
||||
}
|
||||
}
|
||||
|
||||
Set<Long> softDeleteIds = new HashSet<>();
|
||||
Set<Long> affectedUserIds = new HashSet<>();
|
||||
int migratedDeptBindingCount = 0;
|
||||
int migratedCompanyBindingCount = 0;
|
||||
|
||||
for (Dept bypass : departmentList) {
|
||||
if (!isPersonBypassCode(bypass.getDeptCode(), DeptCategory.DEPT) || bypass.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
String oaId = StringUtils.trimToNull(bypass.getOaId());
|
||||
if (oaId == null) {
|
||||
oaId = extractOaIdFromBypassCode(bypass.getDeptCode(), DeptCategory.DEPT);
|
||||
}
|
||||
if (oaId == null) {
|
||||
continue;
|
||||
}
|
||||
Dept official = officialDeptByOaId.get(oaId);
|
||||
if (official == null || official.getId() == null || Objects.equals(official.getId(), bypass.getId())
|
||||
|| isPersonBypassCode(official.getDeptCode(), DeptCategory.DEPT)) {
|
||||
continue;
|
||||
}
|
||||
Dept officialCompany = resolveBelongCompany(official, companyById);
|
||||
migratedDeptBindingCount += this.migrateUserDeptBindings(bypass, official, officialCompany, affectedUserIds);
|
||||
softDeleteIds.add(bypass.getId());
|
||||
}
|
||||
|
||||
for (Dept bypass : companyList) {
|
||||
if (!isPersonBypassCode(bypass.getDeptCode(), DeptCategory.COMPANY) || bypass.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
String oaId = StringUtils.trimToNull(bypass.getOaId());
|
||||
if (oaId == null) {
|
||||
oaId = extractOaIdFromBypassCode(bypass.getDeptCode(), DeptCategory.COMPANY);
|
||||
}
|
||||
if (oaId == null) {
|
||||
continue;
|
||||
}
|
||||
Dept official = officialCompanyByOaId.get(oaId);
|
||||
if (official == null || official.getId() == null || Objects.equals(official.getId(), bypass.getId())
|
||||
|| isPersonBypassCode(official.getDeptCode(), DeptCategory.COMPANY)) {
|
||||
continue;
|
||||
}
|
||||
migratedCompanyBindingCount += this.migrateUserCompanyBindings(bypass, official, affectedUserIds);
|
||||
long childCount = deptService.count(Wrappers.<Dept>lambdaQuery().eq(Dept::getParentId, bypass.getId()));
|
||||
if (childCount == 0) {
|
||||
softDeleteIds.add(bypass.getId());
|
||||
} else {
|
||||
ComposeLogUtil.getLastLog().warn("旁路公司仍有子节点,暂不删除 companyId={} deptCode={} childCount={}",
|
||||
bypass.getId(), bypass.getDeptCode(), childCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (CollectionUtil.isNotEmpty(softDeleteIds)) {
|
||||
deptService.removeByIds(softDeleteIds);
|
||||
ComposeLogUtil.getLastLog().info("人员同步归并旁路组织完成,迁移部门关系{}条,迁移公司关系{}条,逻辑删除旁路节点{}个",
|
||||
migratedDeptBindingCount, migratedCompanyBindingCount, softDeleteIds.size());
|
||||
} else if (migratedDeptBindingCount > 0 || migratedCompanyBindingCount > 0) {
|
||||
ComposeLogUtil.getLastLog().info("人员同步归并旁路组织完成,迁移部门关系{}条,迁移公司关系{}条",
|
||||
migratedDeptBindingCount, migratedCompanyBindingCount);
|
||||
}
|
||||
this.refreshUserDeptFields(affectedUserIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅加载已有组织,按 oa_id / 名称建立索引供人员绑定
|
||||
*/
|
||||
private void loadExistingOrgIndex(OaOrgIndex orgIndex) {
|
||||
Map<String, Dept> companyByOaId = new HashMap<>();
|
||||
Map<String, Dept> companyByName = new HashMap<>();
|
||||
Map<Long, Dept> companyById = new HashMap<>();
|
||||
deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
).forEach(dept -> {
|
||||
if (dept.getId() != null) {
|
||||
companyById.put(dept.getId(), dept);
|
||||
}
|
||||
this.putPreferredByOaKey(companyByOaId, resolveOrgOaKey(dept, DeptCategory.COMPANY), dept, DeptCategory.COMPANY);
|
||||
if (StringUtils.isNotBlank(dept.getDeptName())) {
|
||||
Dept previous = companyByName.get(dept.getDeptName());
|
||||
if (previous == null
|
||||
|| (isPersonBypassCode(previous.getDeptCode(), DeptCategory.COMPANY)
|
||||
&& !isPersonBypassCode(dept.getDeptCode(), DeptCategory.COMPANY))) {
|
||||
companyByName.put(dept.getDeptName(), dept);
|
||||
}
|
||||
}
|
||||
});
|
||||
companyByOaId.forEach((oaId, dept) -> orgIndex.companyByOaId.put(oaId, dept));
|
||||
companyByName.forEach((name, dept) -> orgIndex.companyByOaId.putIfAbsent("NAME:" + name, dept));
|
||||
|
||||
Map<String, Dept> deptByOaId = new HashMap<>();
|
||||
deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
|
||||
).forEach(dept -> {
|
||||
this.putPreferredByOaKey(deptByOaId, resolveOrgOaKey(dept, DeptCategory.DEPT), dept, DeptCategory.DEPT);
|
||||
Dept parentCompany = resolveBelongCompany(dept, companyById);
|
||||
if (parentCompany == null || StringUtils.isBlank(dept.getDeptName())) {
|
||||
return;
|
||||
}
|
||||
String companyKey = resolveOrgOaKey(parentCompany, DeptCategory.COMPANY);
|
||||
if (companyKey == null && StringUtils.isNotBlank(parentCompany.getDeptName())) {
|
||||
companyKey = "NAME:" + parentCompany.getDeptName();
|
||||
}
|
||||
if (companyKey == null) {
|
||||
return;
|
||||
}
|
||||
String nameKey = companyKey + "#" + dept.getDeptName();
|
||||
Dept previous = orgIndex.deptByCompanyAndName.get(nameKey);
|
||||
if (previous == null
|
||||
|| (isPersonBypassCode(previous.getDeptCode(), DeptCategory.DEPT)
|
||||
&& !isPersonBypassCode(dept.getDeptCode(), DeptCategory.DEPT))) {
|
||||
orgIndex.deptByCompanyAndName.put(nameKey, dept);
|
||||
}
|
||||
});
|
||||
orgIndex.deptByOaId.putAll(deptByOaId);
|
||||
}
|
||||
|
||||
/** 优先取 oa_id;旁路空 oa_id 时从 OAC/OAD 编码后缀解析 */
|
||||
private String resolveOrgOaKey(Dept dept, DeptCategory deptCategory) {
|
||||
if (dept == null) {
|
||||
return null;
|
||||
}
|
||||
String oaId = StringUtils.trimToNull(dept.getOaId());
|
||||
if (oaId != null) {
|
||||
return oaId;
|
||||
}
|
||||
return extractOaIdFromBypassCode(dept.getDeptCode(), deptCategory);
|
||||
}
|
||||
|
||||
private int migrateUserDeptBindings(Dept bypass, Dept official, Dept officialCompany, Set<Long> affectedUserIds) {
|
||||
List<UserDept> bindingList = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
|
||||
.and(wrapper -> wrapper.eq(UserDept::getDeptId, bypass.getId())
|
||||
.or().eq(UserDept::getDeptCode, bypass.getDeptCode())));
|
||||
if (CollectionUtil.isEmpty(bindingList)) {
|
||||
return 0;
|
||||
}
|
||||
Set<Long> userIds = bindingList.stream()
|
||||
.map(UserDept::getUserId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
Map<String, UserDept> existingByKey = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
|
||||
.in(UserDept::getUserId, userIds)
|
||||
).stream()
|
||||
.collect(Collectors.toMap(this::getUserDeptKey, userDept -> userDept, (first, second) -> first));
|
||||
List<UserDept> updateList = new ArrayList<>();
|
||||
List<Long> removeIds = new ArrayList<>();
|
||||
for (UserDept binding : bindingList) {
|
||||
if (binding.getUserId() == null) {
|
||||
continue;
|
||||
}
|
||||
affectedUserIds.add(binding.getUserId());
|
||||
String companyCode = officialCompany != null ? officialCompany.getDeptCode() : binding.getCompanyCode();
|
||||
String companyName = officialCompany != null ? officialCompany.getDeptName() : binding.getCompanyName();
|
||||
String newKey = binding.getUserId() + StringUtils.defaultString(companyCode)
|
||||
+ StringUtils.defaultString(official.getDeptCode());
|
||||
UserDept alreadyOfficial = existingByKey.get(newKey);
|
||||
if (alreadyOfficial != null && !Objects.equals(alreadyOfficial.getId(), binding.getId())) {
|
||||
removeIds.add(binding.getId());
|
||||
continue;
|
||||
}
|
||||
binding.setDeptId(official.getId());
|
||||
binding.setDeptCode(official.getDeptCode());
|
||||
binding.setDeptName(official.getDeptName());
|
||||
if (officialCompany != null) {
|
||||
binding.setCompanyCode(companyCode);
|
||||
binding.setCompanyName(companyName);
|
||||
}
|
||||
updateList.add(binding);
|
||||
existingByKey.put(newKey, binding);
|
||||
}
|
||||
if (CollectionUtil.isNotEmpty(updateList)) {
|
||||
userDeptService.updateBatchById(updateList);
|
||||
}
|
||||
if (CollectionUtil.isNotEmpty(removeIds)) {
|
||||
userDeptService.removeByIds(removeIds);
|
||||
}
|
||||
return updateList.size() + removeIds.size();
|
||||
}
|
||||
|
||||
private int migrateUserCompanyBindings(Dept bypass, Dept official, Set<Long> affectedUserIds) {
|
||||
if (StringUtils.isBlank(bypass.getDeptCode())) {
|
||||
return 0;
|
||||
}
|
||||
List<UserDept> bindingList = userDeptService.list(Wrappers.<UserDept>lambdaQuery()
|
||||
.eq(UserDept::getCompanyCode, bypass.getDeptCode()));
|
||||
if (CollectionUtil.isEmpty(bindingList)) {
|
||||
return 0;
|
||||
}
|
||||
List<UserDept> updateList = new ArrayList<>();
|
||||
for (UserDept binding : bindingList) {
|
||||
if (binding.getUserId() != null) {
|
||||
affectedUserIds.add(binding.getUserId());
|
||||
}
|
||||
binding.setCompanyCode(official.getDeptCode());
|
||||
binding.setCompanyName(official.getDeptName());
|
||||
updateList.add(binding);
|
||||
}
|
||||
userDeptService.updateBatchById(updateList);
|
||||
return updateList.size();
|
||||
}
|
||||
|
||||
private void refreshUserDeptFields(Set<Long> userIds) {
|
||||
if (CollectionUtil.isEmpty(userIds)) {
|
||||
return;
|
||||
}
|
||||
List<UserDeptIdsVO> userDeptIdsList = userDeptService.queryUserDeptIds(userIds);
|
||||
List<User> updateUserParams = userDeptIdsList.stream()
|
||||
.filter(userDeptIds -> StringUtils.isNotBlank(userDeptIds.getDeptIds()))
|
||||
.filter(userDeptIds -> {
|
||||
int deptIdLength = userDeptIds.getDeptIds().length();
|
||||
int deptCodesLength = StringUtils.length(userDeptIds.getDeptCodes());
|
||||
return deptIdLength <= MAX_DEPT_ID_LENGTH && deptCodesLength <= MAX_DEPT_ID_LENGTH;
|
||||
})
|
||||
.map(userDeptIds -> {
|
||||
User user = new User();
|
||||
user.setId(userDeptIds.getUserId());
|
||||
user.setDeptId(userDeptIds.getDeptIds());
|
||||
user.setDeptCodes(userDeptIds.getDeptCodes());
|
||||
return user;
|
||||
}).toList();
|
||||
if (CollectionUtil.isNotEmpty(updateUserParams)) {
|
||||
userService.updateBatchById(updateUserParams);
|
||||
}
|
||||
}
|
||||
|
||||
private Dept resolveBelongCompany(Dept department, Map<Long, Dept> companyById) {
|
||||
if (department == null) {
|
||||
return null;
|
||||
}
|
||||
if (department.getParentId() != null) {
|
||||
Dept parent = companyById.get(department.getParentId());
|
||||
if (parent != null) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(department.getBelongCompanyCode())) {
|
||||
return companyById.values().stream()
|
||||
.filter(company -> department.getBelongCompanyCode().equals(company.getDeptCode()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 OAC37 / OAD37 旁路编码解析 OA 主键;哈希旁路(OACNxxx)返回 null
|
||||
*/
|
||||
private String extractOaIdFromBypassCode(String deptCode, DeptCategory deptCategory) {
|
||||
if (StringUtils.isBlank(deptCode) || !isPersonBypassCode(deptCode, deptCategory)) {
|
||||
return null;
|
||||
}
|
||||
String prefix = DeptCategory.COMPANY.equals(deptCategory)
|
||||
? OAConvertConstant.COMPANY_OA_PREFIX
|
||||
: OAConvertConstant.DEPARTMENT_OA_PREFIX;
|
||||
if (deptCode.length() <= prefix.length()) {
|
||||
return null;
|
||||
}
|
||||
String suffix = deptCode.substring(prefix.length());
|
||||
if (suffix.isEmpty() || suffix.startsWith("N")) {
|
||||
return null;
|
||||
}
|
||||
return suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步人员并绑定到三级部门
|
||||
*
|
||||
@@ -198,70 +403,37 @@ public class OaUserListSyncHelper {
|
||||
Map<String, OAPersonResponse> uniquePersonMap = new LinkedHashMap<>();
|
||||
int skippedCount = 0;
|
||||
for (OAPersonResponse oaPerson : oaPersons) {
|
||||
String account = userConvert.resolveAccount(oaPerson);
|
||||
if (StringUtils.isBlank(account)) {
|
||||
String oaLoginId = resolveOaLoginId(oaPerson);
|
||||
if (StringUtils.isBlank(oaLoginId)) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
uniquePersonMap.putIfAbsent(account, oaPerson);
|
||||
uniquePersonMap.put(oaLoginId, oaPerson);
|
||||
}
|
||||
if (uniquePersonMap.isEmpty()) {
|
||||
ComposeLogUtil.getLastLog().warn("OA人员均缺少loginid/工号/手机号,跳过人员同步");
|
||||
ComposeLogUtil.getLastLog().warn("OA人员均缺少登录名,跳过人员同步");
|
||||
return new PersonSyncCount(0, skippedCount);
|
||||
}
|
||||
String tenantId = resolveTenantId();
|
||||
String defaultPassword = DigestUtil.encrypt(ParamCache.getValue(DEFAULT_PARAM_PASSWORD));
|
||||
List<User> users = uniquePersonMap.values().stream()
|
||||
.map(person -> {
|
||||
User user = userConvert.person2user(person, defaultPassword);
|
||||
List<User> users = uniquePersonMap.entrySet().stream()
|
||||
.map(entry -> {
|
||||
User user = userConvert.person2user(entry.getValue(), defaultPassword);
|
||||
user.setTenantId(tenantId);
|
||||
user.setOaLoginId(entry.getKey());
|
||||
user.setAccount(entry.getKey());
|
||||
return user;
|
||||
})
|
||||
.toList();
|
||||
List<String> accounts = users.stream()
|
||||
.map(User::getAccount)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
List<String> phones = users.stream()
|
||||
.map(User::getPhone)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<String, User> existingByAccount = new HashMap<>();
|
||||
Map<String, User> existingByPhone = new HashMap<>();
|
||||
userService.list(Wrappers.<User>lambdaQuery()
|
||||
.and(wrapper -> {
|
||||
wrapper.in(User::getAccount, accounts);
|
||||
if (CollectionUtil.isNotEmpty(phones)) {
|
||||
wrapper.or().in(User::getPhone, phones);
|
||||
}
|
||||
})
|
||||
).stream()
|
||||
.peek(userService::decryptPhone)
|
||||
.forEach(user -> {
|
||||
if (StringUtils.isNotBlank(user.getAccount())) {
|
||||
existingByAccount.put(user.getAccount(), user);
|
||||
}
|
||||
if (StringUtils.isNotBlank(user.getPhone())) {
|
||||
existingByPhone.put(user.getPhone(), user);
|
||||
}
|
||||
});
|
||||
Map<String, User> existingByOaLoginId = this.loadUsersByOaLoginId(tenantId, uniquePersonMap.keySet());
|
||||
Map<String, Long> userMap = new HashMap<>();
|
||||
Set<Long> existsUserIds = new HashSet<>();
|
||||
Set<Long> unchangedUserIds = new HashSet<>();
|
||||
users.forEach(user -> {
|
||||
User existingUser = existingByAccount.get(user.getAccount());
|
||||
if (existingUser == null && StringUtils.isNotBlank(user.getPhone())) {
|
||||
existingUser = existingByPhone.get(user.getPhone());
|
||||
}
|
||||
User existingUser = existingByOaLoginId.get(user.getOaLoginId());
|
||||
if (existingUser != null) {
|
||||
user.setId(existingUser.getId());
|
||||
user.setPassword(null);
|
||||
existsUserIds.add(existingUser.getId());
|
||||
if (shouldSkipOaUpdate(existingUser, user)) {
|
||||
unchangedUserIds.add(existingUser.getId());
|
||||
}
|
||||
} else {
|
||||
user.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
user.setPostId("-1");
|
||||
@@ -269,9 +441,9 @@ public class OaUserListSyncHelper {
|
||||
user.setDataScopeRange(1);
|
||||
user.setDataLevelRange(1);
|
||||
user.setIncludeNewCustomer(0);
|
||||
userService.encryptPhone(user);
|
||||
}
|
||||
userMap.put(user.getAccount(), user.getId());
|
||||
userService.encryptPhone(user);
|
||||
userMap.put(user.getOaLoginId(), user.getId());
|
||||
});
|
||||
|
||||
List<User> addUsers = users.stream()
|
||||
@@ -283,18 +455,13 @@ public class OaUserListSyncHelper {
|
||||
}
|
||||
List<User> updateUsers = users.stream()
|
||||
.filter(user -> existsUserIds.contains(user.getId()))
|
||||
.filter(user -> !unchangedUserIds.contains(user.getId()))
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(updateUsers)) {
|
||||
ComposeLogUtil.getLastLog().info("批量修改用户:{}", updateUsers.size());
|
||||
userService.updateBatchById(updateUsers);
|
||||
}
|
||||
if (!unchangedUserIds.isEmpty()) {
|
||||
ComposeLogUtil.getLastLog().info("跳过未变更用户:{}", unchangedUserIds.size());
|
||||
}
|
||||
ComposeLogUtil.getLastLog().info("缺少账号已跳过的人员:{}", skippedCount);
|
||||
ComposeLogUtil.getLastLog().info("缺少OA登录名已跳过的人员:{}", skippedCount);
|
||||
int syncedCount = addUsers.size() + updateUsers.size();
|
||||
skippedCount += unchangedUserIds.size();
|
||||
if (userMap.isEmpty()) {
|
||||
return new PersonSyncCount(0, skippedCount);
|
||||
}
|
||||
@@ -306,11 +473,8 @@ public class OaUserListSyncHelper {
|
||||
Set<Long> existsUserDeptIds = new HashSet<>(userDeptMap.values());
|
||||
List<UserDept> userDeptList = oaPersons.stream()
|
||||
.filter(oaPerson -> {
|
||||
String account = userConvert.resolveAccount(oaPerson);
|
||||
if (!userMap.containsKey(account)) {
|
||||
return false;
|
||||
}
|
||||
return !unchangedUserIds.contains(userMap.get(account));
|
||||
String oaLoginId = resolveOaLoginId(oaPerson);
|
||||
return StringUtils.isNotBlank(oaLoginId) && userMap.containsKey(oaLoginId);
|
||||
})
|
||||
.map(oaPerson -> this.buildUserDept(oaPerson, userMap, orgIndex))
|
||||
.filter(Objects::nonNull)
|
||||
@@ -349,7 +513,6 @@ public class OaUserListSyncHelper {
|
||||
.collect(Collectors.toSet());
|
||||
String defaultRoleId = getDefaultRoleId();
|
||||
List<User> updateUserParams = list.stream()
|
||||
.filter(userDeptIds -> !unchangedUserIds.contains(userDeptIds.getUserId()))
|
||||
.filter(userDeptIds -> {
|
||||
if (StringUtils.isBlank(userDeptIds.getDeptIds())) {
|
||||
return false;
|
||||
@@ -381,87 +544,94 @@ public class OaUserListSyncHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段
|
||||
* 按 OA 登录名加载已有用户。同一登录名有多条时保留较早的一条,后续同步都更新这一条。
|
||||
*/
|
||||
private boolean shouldSkipOaUpdate(User existing, User incoming) {
|
||||
if (existing == null) {
|
||||
return false;
|
||||
private Map<String, User> loadUsersByOaLoginId(String tenantId, Collection<String> oaLoginIds) {
|
||||
Map<String, User> existingByOaLoginId = new HashMap<>();
|
||||
if (CollectionUtil.isEmpty(oaLoginIds)) {
|
||||
return existingByOaLoginId;
|
||||
}
|
||||
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
|
||||
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
|
||||
if (localModified.isEmpty() || remoteModified.isEmpty()) {
|
||||
return false;
|
||||
List<String> oaLoginIdList = oaLoginIds.stream()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
int batchSize = 500;
|
||||
for (int fromIndex = 0; fromIndex < oaLoginIdList.size(); fromIndex += batchSize) {
|
||||
List<String> oaLoginIdBatch = oaLoginIdList.subList(fromIndex, Math.min(fromIndex + batchSize, oaLoginIdList.size()));
|
||||
userService.list(Wrappers.<User>lambdaQuery()
|
||||
.eq(User::getTenantId, tenantId)
|
||||
.in(User::getOaLoginId, oaLoginIdBatch))
|
||||
.forEach(user -> {
|
||||
String oaLoginId = StringUtils.trimToNull(user.getOaLoginId());
|
||||
if (oaLoginId == null) {
|
||||
return;
|
||||
}
|
||||
User previous = existingByOaLoginId.get(oaLoginId);
|
||||
if (previous == null) {
|
||||
existingByOaLoginId.put(oaLoginId, user);
|
||||
return;
|
||||
}
|
||||
if (user.getId() != null && previous.getId() != null && user.getId() < previous.getId()) {
|
||||
ComposeLogUtil.getLastLog().warn("OA登录名重复,保留较早用户 oaLoginId={} keepUserId={} dropUserId={}",
|
||||
oaLoginId, user.getId(), previous.getId());
|
||||
existingByOaLoginId.put(oaLoginId, user);
|
||||
return;
|
||||
}
|
||||
ComposeLogUtil.getLastLog().warn("OA登录名重复,保留较早用户 oaLoginId={} keepUserId={} dropUserId={}",
|
||||
oaLoginId, previous.getId(), user.getId());
|
||||
});
|
||||
}
|
||||
return localModified.equals(remoteModified);
|
||||
return existingByOaLoginId;
|
||||
}
|
||||
|
||||
private Dept getOrCreateRootCompany() {
|
||||
return deptService.ensureOaSyncRootDept();
|
||||
private String resolveOaLoginId(OAPersonResponse oaPerson) {
|
||||
if (oaPerson == null) {
|
||||
return null;
|
||||
}
|
||||
return StringUtils.trimToNull(oaPerson.getLoginid());
|
||||
}
|
||||
|
||||
private Dept upsertOrg(String name, String oaCode, Dept existing, Dept parent, String tenantId,
|
||||
DeptCategory deptCategory, List<Dept> addList, List<Dept> updateList) {
|
||||
if (existing != null) {
|
||||
Dept updateParam = new Dept();
|
||||
updateParam.setId(existing.getId());
|
||||
updateParam.setDeptName(name);
|
||||
updateParam.setFullName(name);
|
||||
updateParam.setShortName(name);
|
||||
updateParam.setParentId(parent.getId());
|
||||
updateParam.setParentCode(parent.getDeptCode());
|
||||
updateParam.setAncestors(buildAncestors(parent));
|
||||
updateParam.setIsOa(YES);
|
||||
updateParam.setSyncTime(new Date());
|
||||
updateList.add(updateParam);
|
||||
existing.setDeptName(name);
|
||||
existing.setFullName(name);
|
||||
existing.setShortName(name);
|
||||
existing.setParentId(parent.getId());
|
||||
existing.setParentCode(parent.getDeptCode());
|
||||
existing.setAncestors(updateParam.getAncestors());
|
||||
return existing;
|
||||
private boolean isPersonBypassCode(String deptCode, DeptCategory deptCategory) {
|
||||
if (StringUtils.isBlank(deptCode)) {
|
||||
return true;
|
||||
}
|
||||
Dept dept = this.buildOrgDept(name, oaCode, parent, tenantId, deptCategory);
|
||||
dept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
addList.add(dept);
|
||||
return dept;
|
||||
if (DeptCategory.COMPANY.equals(deptCategory)) {
|
||||
return deptCode.startsWith(OAConvertConstant.COMPANY_OA_PREFIX);
|
||||
}
|
||||
return deptCode.startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX);
|
||||
}
|
||||
|
||||
private void saveOrgs(List<Dept> addList, List<Dept> updateList, DeptCategory deptCategory) {
|
||||
if (CollectionUtil.isNotEmpty(addList)) {
|
||||
ComposeLogUtil.getLastLog().info("批量新增{}:{}", deptCategory.getName(), addList.size());
|
||||
deptService.saveBatch(addList);
|
||||
}
|
||||
if (CollectionUtil.isNotEmpty(updateList)) {
|
||||
ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size());
|
||||
deptService.updateBatchById(updateList);
|
||||
/** 同一 oa_id 多条时优先正式编码,避免人员挂到 OAC/OAD 旁路重复节点。 */
|
||||
private void putPreferredByOaId(Map<String, Dept> byOaId, Dept dept, DeptCategory deptCategory) {
|
||||
if (dept == null || StringUtils.isBlank(dept.getOaId())) {
|
||||
return;
|
||||
}
|
||||
this.putPreferredByOaKey(byOaId, dept.getOaId().trim(), dept, deptCategory);
|
||||
}
|
||||
|
||||
private Dept buildOrgDept(String name, String deptCode, Dept parent, String tenantId, DeptCategory deptCategory) {
|
||||
Dept dept = new Dept();
|
||||
dept.setTenantId(tenantId);
|
||||
dept.setParentId(parent.getId());
|
||||
dept.setParentCode(parent.getDeptCode());
|
||||
dept.setAncestors(this.buildAncestors(parent));
|
||||
dept.setDeptName(name);
|
||||
dept.setFullName(name);
|
||||
dept.setShortName(name);
|
||||
dept.setDeptCode(deptCode);
|
||||
dept.setBelongCompanyCode(DeptCategory.COMPANY.equals(deptCategory) ? deptCode : parent.getBelongCompanyCode());
|
||||
dept.setDeptCategory(deptCategory.getCode());
|
||||
dept.setSort(0);
|
||||
dept.setStatus(DataStatusEnum.ENABLE.getCode());
|
||||
dept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
|
||||
dept.setIsOa(YES);
|
||||
dept.setIsPlatformCompany(0);
|
||||
dept.setSyncTime(new Date());
|
||||
return dept;
|
||||
private void putPreferredByOaKey(Map<String, Dept> byOaId, String oaKey, Dept dept, DeptCategory deptCategory) {
|
||||
if (dept == null || StringUtils.isBlank(oaKey)) {
|
||||
return;
|
||||
}
|
||||
Dept previous = byOaId.get(oaKey);
|
||||
if (previous == null) {
|
||||
byOaId.put(oaKey, dept);
|
||||
return;
|
||||
}
|
||||
boolean previousBypass = isPersonBypassCode(previous.getDeptCode(), deptCategory);
|
||||
boolean currentBypass = isPersonBypassCode(dept.getDeptCode(), deptCategory);
|
||||
if (previousBypass && !currentBypass) {
|
||||
byOaId.put(oaKey, dept);
|
||||
}
|
||||
}
|
||||
|
||||
private UserDept buildUserDept(OAPersonResponse oaPerson, Map<String, Long> userMap, OaOrgIndex orgIndex) {
|
||||
Dept department = orgIndex.findDept(oaPerson);
|
||||
if (department == null || department.getId() == null) {
|
||||
ComposeLogUtil.getLastLog().warn(
|
||||
"人员未匹配到已有部门,请先同步组织架构 oaLoginId={} departmentid={} departmentname={} subcompanyid1={}",
|
||||
resolveOaLoginId(oaPerson), oaPerson.getDepartmentid(), oaPerson.getDepartmentname(),
|
||||
oaPerson.getSubcompanyid1());
|
||||
return null;
|
||||
}
|
||||
UserDept userDept = userConvert.person2userDept(oaPerson, userMap);
|
||||
@@ -493,55 +663,6 @@ public class OaUserListSyncHelper {
|
||||
return roleList.get(0).getId().toString();
|
||||
}
|
||||
|
||||
private String resolveCompanyKey(OAPersonResponse oaPerson) {
|
||||
if (oaPerson == null) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(oaPerson.getSubcompanyid1())) {
|
||||
return oaPerson.getSubcompanyid1().trim();
|
||||
}
|
||||
if (StringUtils.isNotBlank(oaPerson.getSubcompanyname())) {
|
||||
return "NAME:" + oaPerson.getSubcompanyname().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveDepartmentKey(OAPersonResponse oaPerson) {
|
||||
if (oaPerson == null) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(oaPerson.getDepartmentid())) {
|
||||
return oaPerson.getDepartmentid().trim();
|
||||
}
|
||||
String companyKey = resolveCompanyKey(oaPerson);
|
||||
if (StringUtils.isNotBlank(companyKey) && StringUtils.isNotBlank(oaPerson.getDepartmentname())) {
|
||||
return companyKey + ":" + oaPerson.getDepartmentname().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String buildCompanyCode(OAPersonResponse oaPerson, String companyKey) {
|
||||
if (StringUtils.isNotBlank(oaPerson.getSubcompanyid1())) {
|
||||
return OAConvertConstant.COMPANY_OA_PREFIX + oaPerson.getSubcompanyid1().trim();
|
||||
}
|
||||
return OAConvertConstant.COMPANY_OA_PREFIX + "N" + Math.abs(companyKey.hashCode());
|
||||
}
|
||||
|
||||
private String buildDepartmentCode(OAPersonResponse oaPerson, String departmentKey) {
|
||||
if (StringUtils.isNotBlank(oaPerson.getDepartmentid())) {
|
||||
return OAConvertConstant.DEPARTMENT_OA_PREFIX + oaPerson.getDepartmentid().trim();
|
||||
}
|
||||
return OAConvertConstant.DEPARTMENT_OA_PREFIX + "N" + Math.abs(departmentKey.hashCode());
|
||||
}
|
||||
|
||||
private String buildAncestors(Dept parent) {
|
||||
String ancestors = parent.getAncestors();
|
||||
if (StringUtils.isBlank(ancestors)) {
|
||||
ancestors = String.valueOf(BladeConstant.TOP_PARENT_ID);
|
||||
}
|
||||
return ancestors + "," + parent.getId();
|
||||
}
|
||||
|
||||
private String resolveTenantId() {
|
||||
String tenantId = AuthUtil.getTenantId();
|
||||
if (StringUtils.isBlank(tenantId)) {
|
||||
|
||||
+3
-2
@@ -366,6 +366,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
user.setPostId(null);
|
||||
user.setAccount(null);
|
||||
user.setIsLeader(null);
|
||||
user.setOaLoginId(null);
|
||||
CacheUtil.clear(USER_CACHE);
|
||||
return submitUserInfo(user);
|
||||
}
|
||||
@@ -407,8 +408,8 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
@Override
|
||||
public IPage<User> selectUserPage(IPage<User> page, User user, Long deptId, String tenantId) {
|
||||
normalizeUserSearch(user);
|
||||
List<Long> deptIdList = SysCache.getDeptChildIds(deptId);
|
||||
return page.setRecords(baseMapper.selectUserPage(page, user, deptIdList, tenantId));
|
||||
// 部门过滤下推到 SQL(FIND_IN_SET),避免先展开整棵子树再拼超大 IN 列表
|
||||
return page.setRecords(baseMapper.selectUserPage(page, user, deptId, tenantId));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+6
@@ -309,6 +309,12 @@ public class BfiProperties {
|
||||
*/
|
||||
private String invoiceTypes = "";
|
||||
|
||||
/**
|
||||
* 客商单位过滤开关:开启后只同步开票单位(销方)与受票单位(购方)税号均已登记在
|
||||
* 客商发票信息(blade_customer_invoice_info)中的发票,前端票池展示查询同用此开关过滤,默认关闭
|
||||
*/
|
||||
private boolean customerUnitsOnly = false;
|
||||
|
||||
/**
|
||||
* 同步 single-flight 锁租约秒数,须大于最坏一轮耗时;进程异常退出时锁在租约到期后自动释放
|
||||
*/
|
||||
|
||||
+3
@@ -32,6 +32,9 @@ public final class KingdeeLockConstant {
|
||||
/** 来源操作类型:单据删除(解锁回调携带 DELETE 时 WLTMS 废除源单并还原关联数据) */
|
||||
public static final String OPERATION_DELETE = "DELETE";
|
||||
|
||||
/** 来源操作类型:结算调整重推(结算调整单重推后解锁原结算单旧锁记录,非金蝶回调) */
|
||||
public static final String OPERATION_ADJUST_REPUSH = "ADJUST_REPUSH";
|
||||
|
||||
/** 系统标识:WLTMS(运输管理系统,即本系统) */
|
||||
public static final String SYSTEM_WLTMS = "WLTMS";
|
||||
|
||||
|
||||
+12
@@ -31,9 +31,21 @@ package org.springblade.transport.constant;
|
||||
*/
|
||||
public final class TransportBusiConstant {
|
||||
|
||||
/** 付款类型:项目预付。 */
|
||||
public static final String PAYMENT_TYPE_PROJECT_ADVANCE = "project_advance";
|
||||
|
||||
/** 付款类型:进度预付。 */
|
||||
public static final String PAYMENT_TYPE_PROGRESS_ADVANCE = "progress_advance";
|
||||
|
||||
/** 付款类型:结算付款(关联正式结算单的正式付款)。 */
|
||||
public static final String PAYMENT_TYPE_SETTLEMENT = "settlement_payment";
|
||||
|
||||
/** 结算单类型:预结算。 */
|
||||
public static final String SETTLEMENT_BILL_PRE = "pre";
|
||||
|
||||
/** 结算单类型:正式结算。 */
|
||||
public static final String SETTLEMENT_BILL_FORMAL = "formal";
|
||||
|
||||
/** 审批状态:草稿(允许编辑、删除)。 */
|
||||
public static final String DRAFT = "draft";
|
||||
|
||||
|
||||
+7
-4
@@ -61,8 +61,9 @@ public class BillLedgerController extends BladeController {
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "付款申请可用汇票")
|
||||
public R<List<BillLedgerVO>> availableOptions(@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) Long deptId, @RequestParam(required = false) Long selectedId) {
|
||||
return R.data(billLedgerService.availableOptions(keyword, deptId, selectedId));
|
||||
@RequestParam(required = false) Long deptId, @RequestParam(required = false) Long selectedId,
|
||||
@RequestParam(required = false) String payerName, @RequestParam(required = false) String payeeName) {
|
||||
return R.data(billLedgerService.availableOptions(keyword, deptId, selectedId, payerName, payeeName));
|
||||
}
|
||||
|
||||
@GetMapping("/available-page")
|
||||
@@ -70,8 +71,10 @@ public class BillLedgerController extends BladeController {
|
||||
@Operation(summary = "付款申请可用汇票分页")
|
||||
public R<IPage<BillLedgerVO>> availablePage(Query pageQuery,
|
||||
@RequestParam(required = false) String keyword, @RequestParam(required = false) Long deptId,
|
||||
@RequestParam(required = false) Long selectedId) {
|
||||
return R.data(billLedgerService.availablePage(Condition.getPage(pageQuery), keyword, deptId, selectedId));
|
||||
@RequestParam(required = false) Long selectedId, @RequestParam(required = false) String payerName,
|
||||
@RequestParam(required = false) String payeeName) {
|
||||
return R.data(billLedgerService.availablePage(Condition.getPage(pageQuery), keyword, deptId, selectedId,
|
||||
payerName, payeeName));
|
||||
}
|
||||
|
||||
@PostMapping("/submit")
|
||||
|
||||
+59
@@ -105,6 +105,65 @@ public class DriverWaybillController extends BladeController {
|
||||
return R.data(driverWaybillService.detail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/task/current")
|
||||
@ApiOperationSupport(order = 21)
|
||||
@Operation(summary = "首页:当前运输中运次", description = "当前用户手机号可见的运输中运次(有配载单的一趟车算一个)最新一条")
|
||||
public R<DriverWaybillCardVO> tripCurrentTask() {
|
||||
return R.data(driverWaybillService.tripCurrentTask());
|
||||
}
|
||||
|
||||
@GetMapping("/task/pending-preview")
|
||||
@ApiOperationSupport(order = 22)
|
||||
@Operation(summary = "首页:待接运次预览", description = "当前用户手机号可见的待接运次预览与总数(按运次计)")
|
||||
public R<DriverWaybillPreviewVO> tripPendingPreview(
|
||||
@Parameter(description = "预览条数,默认 2") @RequestParam(required = false) Integer size) {
|
||||
return R.data(driverWaybillService.tripPendingPreview(size));
|
||||
}
|
||||
|
||||
@GetMapping("/task/counts")
|
||||
@ApiOperationSupport(order = 23)
|
||||
@Operation(summary = "运次 Tab 统计", description = "按运次统计:全部 = 待接单 + 进行中 + 已完成(不含已取消)")
|
||||
public R<DriverWaybillTabCountsVO> tripCounts() {
|
||||
return R.data(driverWaybillService.tripTabCounts());
|
||||
}
|
||||
|
||||
@GetMapping("/task/page")
|
||||
@ApiOperationSupport(order = 24)
|
||||
@Operation(summary = "运次分页列表", description = "一页即若干个运次;status:空=全部,0待接单,1运输中,2已完成;keyword 支持运单号/配载单号/起终点/车牌")
|
||||
public R<IPage<DriverWaybillCardVO>> tripPage(
|
||||
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
|
||||
@Parameter(description = "每页运次数") @RequestParam(required = false) Integer size,
|
||||
@Parameter(description = "状态:0待接单/1运输中/2已完成,不传为全部") @RequestParam(required = false) String status,
|
||||
@Parameter(description = "关键字:运单号/配载单号/起终点/车牌") @RequestParam(required = false) String keyword) {
|
||||
Integer statusCode = parseStatus(status);
|
||||
return R.data(driverWaybillService.tripPage(current, size, statusCode, keyword));
|
||||
}
|
||||
|
||||
@GetMapping("/task/detail")
|
||||
@ApiOperationSupport(order = 25)
|
||||
@Operation(summary = "司机运次详情", description = "按运次主键(字符串)取整趟车;有配载单时下发站点 ①②③(stations)与每站货物条目")
|
||||
public R<DriverWaybillCardVO> tripDetail(
|
||||
@Parameter(description = "运次主键(配载单主键或运单主键)", required = true) @RequestParam String id) {
|
||||
return R.data(driverWaybillService.tripDetail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/task/accept")
|
||||
@ApiOperationSupport(order = 26)
|
||||
@Operation(summary = "运次确认接单", description = "一次接单把该运次全部子运单置为已接单;任一子单已接单即视为整单已接单")
|
||||
public R tripAccept(
|
||||
@Parameter(description = "运次主键(配载单主键或运单主键)", required = true) @RequestParam String id) {
|
||||
return R.status(driverWaybillService.tripAccept(id));
|
||||
}
|
||||
|
||||
@PostMapping("/task/reject")
|
||||
@ApiOperationSupport(order = 27)
|
||||
@Operation(summary = "运次拒绝接单", description = "整单拒绝:该运次全部未终态子运单一起置为已拒绝,此后该运次对本人不可见")
|
||||
public R tripReject(
|
||||
@Parameter(description = "运次主键(配载单主键或运单主键)", required = true) @RequestParam String id,
|
||||
@Parameter(description = "拒绝原因") @RequestParam(required = false) String reason) {
|
||||
return R.status(driverWaybillService.tripReject(id, reason));
|
||||
}
|
||||
|
||||
@PostMapping("/track")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "司机端行驶轨迹", description = "按当前用户手机号校验归属后,根据打卡记录/计划时间自动推导起止日期并查询 LBS 轨迹(WGS-84)")
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* HCS 合规回调 Mock 端点
|
||||
* <p>
|
||||
* 合同法务审批结果回调(D103)、付款法务审批结果回调(D104)、项目金蝶同步结果回调(D902)、
|
||||
* 合同金蝶同步结果回传接收(D903)四个入站回调的纯 mock 实现:仅校验来源系统标识后返回固定结果,
|
||||
* 不读写业务数据、不联动状态。真实处理逻辑保留在对应 Support 中,联调时把端点切回
|
||||
* {@link } 并调用 Support 方法即可。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@TenantIgnore
|
||||
@RequestMapping("hcs")
|
||||
@Tag(name = "HCS合规回调Mock", description = "HCS合规回调Mock端点(仅自测,返回固定 mock 数据)")
|
||||
public class HcsMockController {
|
||||
|
||||
/**
|
||||
* 来源系统标识(合规方约定值,与 third-party.hcs.src-system 默认值一致)
|
||||
*/
|
||||
private static final String SOURCE_SYSTEM = "WLTMS";
|
||||
|
||||
/**
|
||||
* 合同法务审批结果回调(D103)
|
||||
*/
|
||||
@PostMapping("/contractLegal/result")
|
||||
@ApiOperationSupport(order = 15)
|
||||
@Operation(summary = "合同法务审批结果回调Mock", description = "D103 合规推送合同特批(金蝶同步)结果,仅校验来源系统并返回 mock 数据")
|
||||
public R<String> contractLegalResult(@RequestBody(required = false) Map<String, Object> param) {
|
||||
this.checkSourceSystem("合同法务审批结果", param);
|
||||
return R.data("mock特批结果已处理:approved");
|
||||
}
|
||||
|
||||
/**
|
||||
* 付款法务审批结果回调(D104)
|
||||
*/
|
||||
@PostMapping("/paymentLegal/result")
|
||||
@ApiOperationSupport(order = 16)
|
||||
@Operation(summary = "付款法务审批结果回调Mock", description = "D104 合规推送付款特批结果,仅校验来源系统并返回 mock 数据")
|
||||
public R<String> paymentLegalResult(@RequestBody(required = false) Map<String, Object> param) {
|
||||
this.checkSourceSystem("付款法务审批结果", param);
|
||||
return R.data("mock付款特批结果接收已处理:approved");
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目金蝶同步结果回调(D902)
|
||||
*/
|
||||
@PostMapping("/projectLegal/result")
|
||||
@ApiOperationSupport(order = 18)
|
||||
@Operation(summary = "项目金蝶同步结果回调Mock", description = "D902 合规推送项目金蝶同步结果,仅校验来源系统并返回 mock 数据")
|
||||
public R<String> projectLegalResult(@RequestBody(required = false) Map<String, Object> param) {
|
||||
this.checkSourceSystem("项目金蝶同步结果", param);
|
||||
return R.data("mock项目金蝶同步结果接收已处理:approved");
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同金蝶同步结果回传接收(D903)
|
||||
*/
|
||||
@PostMapping("/contractKingdee/result")
|
||||
@ApiOperationSupport(order = 19)
|
||||
@Operation(summary = "合同金蝶同步结果回传接收Mock", description = "D903 合规推送合同金蝶最终同步结果,仅校验来源系统并返回 mock 数据")
|
||||
public R<String> contractKingdeeResult(@RequestBody(required = false) Map<String, Object> param) {
|
||||
this.checkSourceSystem("合同金蝶同步结果", param);
|
||||
return R.data("mock合同金蝶同步结果接收处理:approved");
|
||||
}
|
||||
|
||||
/**
|
||||
* 来源系统校验:仅接受合规方约定的来源标识,防伪造回传
|
||||
*
|
||||
* @param bizName 业务名称(日志用)
|
||||
* @param param 回调报文
|
||||
*/
|
||||
private void checkSourceSystem(String bizName, Map<String, Object> param) {
|
||||
String sourceSystem = param == null ? null : Objects.toString(param.get("sourceSystem"), null);
|
||||
if (!StrUtil.equalsIgnoreCase(sourceSystem, SOURCE_SYSTEM)) {
|
||||
log.warn("{}推送来源系统标识错误:sourceSystem={}", bizName, sourceSystem);
|
||||
throw new ServiceException("来源系统标识错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -84,10 +84,9 @@ public class InvoiceApplicationController extends BladeController {
|
||||
public R<IPage<Map<String, Object>>> settlementCandidates(Query query,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String contractCategory,
|
||||
@RequestParam(defaultValue = "receivable") String settlementType,
|
||||
@RequestParam(defaultValue = "unreceived") String invoiceStatus) {
|
||||
@RequestParam(defaultValue = "receivable") String settlementType) {
|
||||
return R.data(invoiceApplicationService.settlementCandidates(
|
||||
Condition.getPage(query), keyword, contractCategory, settlementType, invoiceStatus));
|
||||
Condition.getPage(query), keyword, contractCategory, settlementType));
|
||||
}
|
||||
|
||||
@GetMapping("/settlement-details")
|
||||
|
||||
+14
-3
@@ -39,6 +39,7 @@ import org.springblade.transport.pojo.vo.AdminVehicleOptionVO;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillTrackVO;
|
||||
import org.springblade.transport.service.IManageWaybillService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -69,7 +70,7 @@ public class ManageWaybillController extends BladeController {
|
||||
|
||||
@GetMapping("/stats")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "运单状态统计", description = "待接单=pending,运输中=running,已完成=completed;租户内不过滤组织(小程序调度账号组织常与运单不一致);在途异常=异常处置状态≠已完成")
|
||||
@Operation(summary = "运单状态统计", description = "待接单=pending,运输中=running,已完成=completed;只统计本组织的运单(超级管理员不过滤组织);在途异常=异常处置状态≠已完成")
|
||||
public R<AdminHomeStatsVO> stats() {
|
||||
return R.data(manageWaybillService.stats());
|
||||
}
|
||||
@@ -99,9 +100,9 @@ public class ManageWaybillController extends BladeController {
|
||||
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "运单详情", description = "调度端查看运单详情(含 punchNodes / enrouteRecords),不校验司机归属与组织;字段对齐小程序 pages/waybill/detail")
|
||||
@Operation(summary = "运次详情", description = "调度端查看整趟车详情(含 stations / punchNodes / enrouteRecords),不校验司机归属与组织;id 传运次主键字符串,已拒单的运单主键回落成单票视图")
|
||||
public R<AdminWaybillDetailVO> detail(
|
||||
@Parameter(description = "运单ID", required = true) @RequestParam Long id) {
|
||||
@Parameter(description = "运次主键(配载单主键或运单主键,字符串)", required = true) @RequestParam String id) {
|
||||
return R.data(manageWaybillService.detail(id));
|
||||
}
|
||||
|
||||
@@ -159,4 +160,14 @@ public class ManageWaybillController extends BladeController {
|
||||
return R.status(waybillService.savePunchRecord(request));
|
||||
}
|
||||
|
||||
@PostMapping("/track")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "车辆历史轨迹", description = "小程序调度端:仅需登录态,按运单绑定车牌和日期区间查询 LBS 轨迹")
|
||||
public R<WaybillTrackVO> track(
|
||||
@Parameter(description = "运单ID", required = true) @RequestParam Long id,
|
||||
@Parameter(description = "开始日期 YYYY-MM-DD", required = true) @RequestParam String startDate,
|
||||
@Parameter(description = "结束日期 YYYY-MM-DD", required = true) @RequestParam String endDate) {
|
||||
return R.data(waybillService.trackVehicle(id, startDate, endDate));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
@@ -34,6 +34,7 @@ import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
|
||||
import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO;
|
||||
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
|
||||
import org.springblade.transport.pojo.vo.ProjectAdvanceWriteOffOptionVO;
|
||||
import org.springblade.transport.service.IPaymentApplicationService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -67,6 +68,17 @@ public class PaymentApplicationController extends BladeController {
|
||||
@RequestParam Long referenceId, @RequestParam(required = false) Long excludeId) {
|
||||
return R.data(paymentApplicationService.referenceAmount(paymentType, referenceId, excludeId));
|
||||
}
|
||||
|
||||
@GetMapping("/project-advance-write-off-options")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "按合同查询可选项目预付核销列表")
|
||||
public R<List<ProjectAdvanceWriteOffOptionVO>> projectAdvanceWriteOffOptions(
|
||||
@RequestParam Long contractId,
|
||||
@RequestParam(required = false) String excludeSettlementType,
|
||||
@RequestParam(required = false) Long excludeSettlementId) {
|
||||
return R.data(paymentApplicationService.projectAdvanceWriteOffOptions(
|
||||
contractId, excludeSettlementType, excludeSettlementId));
|
||||
}
|
||||
@PostMapping("/save")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "保存付款申请")
|
||||
|
||||
+7
@@ -94,6 +94,13 @@ public class PreSettlementController extends BladeController {
|
||||
return R.data(preSettlementService.contractOptions(keyword));
|
||||
}
|
||||
|
||||
@GetMapping("/next-no")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "获取最新预结算单号")
|
||||
public R<String> nextNo() {
|
||||
return R.data(preSettlementService.nextNo());
|
||||
}
|
||||
|
||||
@GetMapping("/fee-options")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "费用类型及费用项")
|
||||
|
||||
+16
-1
@@ -7,6 +7,8 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
import org.springblade.transport.pojo.dto.SettlementAdjustmentSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.SettlementAdjustmentStatusRequest;
|
||||
import org.springblade.transport.pojo.entity.SettlementAdjustment;
|
||||
@@ -33,7 +35,20 @@ public class SettlementAdjustmentController extends BladeController {
|
||||
@PostMapping("/save") public R<Long> save(@RequestBody SettlementAdjustmentSaveRequest request) { return R.data(service.saveDraft(request)); }
|
||||
@PostMapping("/remove") public R remove(@RequestParam Long id) { service.removeDraft(id); return R.success("删除成功"); }
|
||||
@PostMapping("/submit") public R submit(@RequestBody SettlementAdjustmentStatusRequest request) { service.submit(request); return R.success("提交成功"); }
|
||||
@PostMapping("/approve") public R approve(@RequestBody SettlementAdjustmentStatusRequest request) { service.approve(request); return R.success("审批通过"); }
|
||||
@PostMapping("/approve") public R approve(@RequestBody SettlementAdjustmentStatusRequest request) {
|
||||
String hint = service.approve(request);
|
||||
// 未推送场景自动推送失败原因以警告文案返回(data=false 供前端区分提示样式)
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (Func.isNotBlank(syncFailure)) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
if (Func.isNotBlank(hint)) {
|
||||
return R.data(Boolean.TRUE, "审批通过;" + hint);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
@PostMapping("/return") public R returnBill(@RequestBody SettlementAdjustmentStatusRequest request) { service.returnBill(request); return R.success("已驳回"); }
|
||||
@PostMapping("/repush") public R<String> repush(@RequestParam Long id) { return R.data(service.repush(id)); }
|
||||
@GetMapping("/kingdee-writeoff-precheck") public R<Map<String, Object>> kingdeeWriteOffPrecheck(@RequestParam Long formalSettlementId) { return R.data(service.kingdeeWriteOffPrecheck(formalSettlementId)); }
|
||||
@PostMapping("/sync-kingdee") public R<String> syncKingdee(@RequestParam Long id) { return R.data(service.syncKingdee(id)); }
|
||||
}
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportBatchRequest;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportVoidRequest;
|
||||
import org.springblade.transport.pojo.vo.SettlementImportBatchVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||
import org.springblade.transport.service.ISettlementImportBatchService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 导入结算单控制器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@PreAuth(menu = "settlement_import")
|
||||
@RequestMapping("settlement-import")
|
||||
@Tag(name = "导入结算单", description = "导入结算单管理")
|
||||
public class SettlementImportController extends BladeController {
|
||||
|
||||
private final ISettlementImportBatchService settlementImportBatchService;
|
||||
|
||||
@GetMapping("/options")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "筛选项与表单选项")
|
||||
public R<Map<String, List<Map<String, Object>>>> options() {
|
||||
return R.data(settlementImportBatchService.options());
|
||||
}
|
||||
|
||||
@GetMapping("/next-code")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "下一个批次号")
|
||||
public R<String> nextCode() {
|
||||
return R.data(settlementImportBatchService.nextBatchNo());
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "导入结算单批次分页")
|
||||
public R<IPage<SettlementImportBatchVO>> list(SettlementImportBatchRequest request, Query query) {
|
||||
return R.data(settlementImportBatchService.page(Condition.getPage(query), request));
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "导入结算单批次详情")
|
||||
public R<SettlementImportBatchVO> detail(@RequestParam Long id) {
|
||||
return R.data(settlementImportBatchService.detail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/export-template")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导出导入模板")
|
||||
public void exportTemplate(HttpServletResponse response) {
|
||||
settlementImportBatchService.exportTemplate(response);
|
||||
}
|
||||
|
||||
@PostMapping("/parse")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "解析导入 Excel")
|
||||
public R<Map<String, Object>> parse(@RequestPart("file") MultipartFile file) {
|
||||
return R.data(settlementImportBatchService.parse(file));
|
||||
}
|
||||
|
||||
@PostMapping("/validate")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "校验导入数据")
|
||||
public R<WaybillImportValidateResultVO> validate(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.validate(request));
|
||||
}
|
||||
|
||||
@PostMapping("/export-failures")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "导出失败明细")
|
||||
public void exportFailures(@RequestBody SettlementImportBatchRequest request, HttpServletResponse response) {
|
||||
settlementImportBatchService.exportFailures(request, null, response);
|
||||
}
|
||||
|
||||
@PostMapping("/draft")
|
||||
@ApiOperationSupport(order = 9)
|
||||
@Operation(summary = "保存草稿")
|
||||
public R<SettlementImportBatchVO> draft(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.saveDraft(request));
|
||||
}
|
||||
|
||||
@PostMapping("/confirm")
|
||||
@ApiOperationSupport(order = 10)
|
||||
@Operation(summary = "确认导入结算单")
|
||||
public R<SettlementImportBatchVO> confirm(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.confirm(request));
|
||||
}
|
||||
|
||||
@PostMapping("/void")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "作废导入结算单批次")
|
||||
public R voidBatch(@RequestBody SettlementImportVoidRequest request) {
|
||||
settlementImportBatchService.voidBatch(request);
|
||||
return R.success("作废成功");
|
||||
}
|
||||
|
||||
@PostMapping("/remove")
|
||||
@ApiOperationSupport(order = 12)
|
||||
@Operation(summary = "删除草稿批次")
|
||||
public R remove(@RequestParam Long id) {
|
||||
settlementImportBatchService.removeDraft(id);
|
||||
return R.success("删除成功");
|
||||
}
|
||||
|
||||
@GetMapping("/history-waybills")
|
||||
@ApiOperationSupport(order = 13)
|
||||
@Operation(summary = "历史运单可选列表")
|
||||
public R<List<Map<String, Object>>> historyWaybills(SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.historyWaybills(request));
|
||||
}
|
||||
}
|
||||
+7
@@ -373,6 +373,13 @@ public class WaybillController extends BladeController {
|
||||
return R.data(waybillService.batchComplete(ids));
|
||||
}
|
||||
|
||||
@PostMapping("/confirm-return")
|
||||
@ApiOperationSupport(order = 24)
|
||||
@Operation(summary = "确认回单", description = "回单节点要求业务员确认时用:该运单的司机打卡节点全部已打卡且无未了结异常,才置为已完成")
|
||||
public R confirmReturn(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.status(waybillService.confirmReturnReceipt(id));
|
||||
}
|
||||
|
||||
@PostMapping("/road-loading")
|
||||
@ApiOperationSupport(order = 23)
|
||||
@Operation(summary = "公路配载", description = "传入ids")
|
||||
|
||||
+496
@@ -0,0 +1,496 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.DateUtil;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.springblade.common.excel.ImportCellReadException;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 导入结算单 Excel 读取器(Apache POI)。
|
||||
* <p>
|
||||
* 定位「*运输费」或「运输费」列,其后直到日期/备注等固定列(兼容旧模板中的「结算金额」)为动态费用列。
|
||||
* 结算金额不由模板填写,解析时按「运输费 + 各费用项」自动合计。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class SettlementImportFeeReader {
|
||||
|
||||
private static final DataFormatter DATA_FORMATTER = new DataFormatter(Locale.CHINA);
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
private static final Set<String> DATE_FIELDS = Set.of(
|
||||
"actualStartDate", "actualEndDate", "planStartDate", "planEndDate"
|
||||
);
|
||||
private static final Set<String> OPTIONAL_DECIMAL_FIELDS = Set.of("mileage", "unitPrice");
|
||||
/** 费用列结束边界:其后为固定业务列;含旧模板「结算金额」以便兼容历史文件 */
|
||||
private static final Set<String> FEE_END_HEADERS = Set.of(
|
||||
"结算金额",
|
||||
"*实际发货时间", "实际发货时间",
|
||||
"*实际完成时间", "实际完成时间",
|
||||
"预计发货时间", "预计完成时间",
|
||||
"备注", "同一运单标识号"
|
||||
);
|
||||
|
||||
private SettlementImportFeeReader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析结果
|
||||
*/
|
||||
public static final class ParseResult {
|
||||
private final List<Map<String, Object>> rows;
|
||||
private final List<String> feeItemNames;
|
||||
private final List<String> warnings;
|
||||
private final List<String> headers;
|
||||
|
||||
public ParseResult(List<Map<String, Object>> rows, List<String> feeItemNames, List<String> warnings,
|
||||
List<String> headers) {
|
||||
this.rows = rows;
|
||||
this.feeItemNames = feeItemNames;
|
||||
this.warnings = warnings;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getRows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
public List<String> getFeeItemNames() {
|
||||
return feeItemNames;
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public List<String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
|
||||
public static ParseResult read(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("上传文件不能为空");
|
||||
}
|
||||
try (InputStream inputStream = file.getInputStream(); Workbook workbook = WorkbookFactory.create(inputStream)) {
|
||||
Sheet sheet = resolveDataSheet(workbook);
|
||||
Row headerRow = sheet.getRow(sheet.getFirstRowNum());
|
||||
if (headerRow == null) {
|
||||
throw new ServiceException("未识别到表头,请使用标准导入结算单模板");
|
||||
}
|
||||
Map<Integer, String> headers = readHeaderMap(headerRow);
|
||||
int transportFeeColumn = findTransportFeeColumn(headers);
|
||||
if (transportFeeColumn < 0) {
|
||||
throw new ServiceException("未找到「*运输费」或「运输费」列");
|
||||
}
|
||||
List<Integer> feeColumns = new ArrayList<>();
|
||||
List<String> feeItemNames = new ArrayList<>();
|
||||
int endColumn = findFeeEndColumn(headers, transportFeeColumn);
|
||||
for (int column = transportFeeColumn + 1; column < endColumn; column++) {
|
||||
String name = headers.get(column);
|
||||
if (name == null || name.isBlank() || FEE_END_HEADERS.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
feeColumns.add(column);
|
||||
feeItemNames.add(name);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
int firstDataRow = sheet.getFirstRowNum() + 1;
|
||||
int lastRow = sheet.getLastRowNum();
|
||||
for (int rowIndex = firstDataRow; rowIndex <= lastRow; rowIndex++) {
|
||||
Row row = sheet.getRow(rowIndex);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> mapped = mapRow(headers, feeColumns, row, rowIndex);
|
||||
if (mapped != null) {
|
||||
rows.add(mapped);
|
||||
}
|
||||
}
|
||||
return new ParseResult(rows, feeItemNames, List.of(), new ArrayList<>(headers.values()));
|
||||
} catch (ServiceException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("读取导入结算单 Excel 失败:" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 优先取含「*运输费/运输费」表头的工作表,否则取第一个非空表。 */
|
||||
private static Sheet resolveDataSheet(Workbook workbook) {
|
||||
Sheet fallback = null;
|
||||
for (int index = 0; index < workbook.getNumberOfSheets(); index++) {
|
||||
Sheet sheet = workbook.getSheetAt(index);
|
||||
if (sheet == null) {
|
||||
continue;
|
||||
}
|
||||
Row headerRow = sheet.getRow(sheet.getFirstRowNum());
|
||||
if (headerRow == null) {
|
||||
continue;
|
||||
}
|
||||
Map<Integer, String> headers = readHeaderMap(headerRow);
|
||||
if (findTransportFeeColumn(headers) >= 0) {
|
||||
return sheet;
|
||||
}
|
||||
if (fallback == null && !headers.isEmpty()) {
|
||||
fallback = sheet;
|
||||
}
|
||||
}
|
||||
if (fallback != null) {
|
||||
return fallback;
|
||||
}
|
||||
throw new ServiceException("Excel 中未找到有效工作表");
|
||||
}
|
||||
|
||||
private static Map<Integer, String> readHeaderMap(Row headerRow) {
|
||||
Map<Integer, String> headers = new LinkedHashMap<>();
|
||||
short lastCell = headerRow.getLastCellNum();
|
||||
for (int column = 0; column < lastCell; column++) {
|
||||
String value = normalizeHeader(cellString(headerRow.getCell(column)));
|
||||
if (value != null && !value.isBlank()) {
|
||||
headers.put(column, value);
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** 去掉 BOM/不可见空白,全角星号转半角,便于匹配「*运输费」。 */
|
||||
private static String normalizeHeader(String raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
String text = raw.replace("\uFEFF", "")
|
||||
.replace('\u00A0', ' ')
|
||||
.replace('*', '*')
|
||||
.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static Map<String, Object> mapRow(Map<Integer, String> headers, List<Integer> feeColumns,
|
||||
Row row, int excelRowIndex) {
|
||||
Map<String, Object> mapped = new LinkedHashMap<>();
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
for (Map.Entry<Integer, String> entry : headers.entrySet()) {
|
||||
int column = entry.getKey();
|
||||
String trimmedHeader = entry.getValue();
|
||||
Cell poiCell = row.getCell(column);
|
||||
if (feeColumns.contains(column)) {
|
||||
feeItems.put(trimmedHeader, decimal(cellString(poiCell), trimmedHeader, excelRowIndex));
|
||||
continue;
|
||||
}
|
||||
String field = FIXED_HEADER_MAP.get(trimmedHeader);
|
||||
if (field == null) {
|
||||
mapped.put(trimmedHeader, blankToNull(cellString(poiCell)));
|
||||
continue;
|
||||
}
|
||||
if (DATE_FIELDS.contains(field)) {
|
||||
mapped.put(field, dateCellString(poiCell));
|
||||
} else if (OPTIONAL_DECIMAL_FIELDS.contains(field)) {
|
||||
mapped.put(field, optionalDecimal(cellString(poiCell), trimmedHeader, excelRowIndex));
|
||||
} else if ("transportFee".equals(field) || "quantity".equals(field)) {
|
||||
mapped.put(field, decimal(cellString(poiCell), trimmedHeader, excelRowIndex));
|
||||
} else if ("settlementAmount".equals(field)) {
|
||||
// 旧模板可能仍含结算金额列,读入后忽略,统一按运输费+费用项自动合计
|
||||
continue;
|
||||
} else if ("serialNumber".equals(field)) {
|
||||
mapped.put(field, intValue(cellString(poiCell)));
|
||||
} else {
|
||||
mapped.put(field, blankToNull(cellString(poiCell)));
|
||||
}
|
||||
mapped.put(trimmedHeader, mapped.get(field));
|
||||
}
|
||||
|
||||
boolean hasBusinessValue = mapped.values().stream().anyMatch(value -> {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
if (value instanceof String text) {
|
||||
return !text.isBlank();
|
||||
}
|
||||
return true;
|
||||
}) || feeItems.values().stream().anyMatch(amount -> amount != null && amount.compareTo(BigDecimal.ZERO) != 0);
|
||||
if (!hasBusinessValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BigDecimal transportFee = toDecimal(mapped.get("transportFee"));
|
||||
BigDecimal feeItemTotal = feeItems.values().stream()
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
BigDecimal settlementAmount = transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
|
||||
mapped.put("settlementAmount", settlementAmount);
|
||||
mapped.put("结算金额", settlementAmount);
|
||||
mapped.put("feeItems", feeItems);
|
||||
mapped.put("freight", transportFee);
|
||||
mapped.put("运费", transportFee);
|
||||
mapped.put("*运输费", transportFee);
|
||||
mapped.put("运输费", transportFee);
|
||||
mapped.put("otherFeeTotal", feeItemTotal);
|
||||
mapped.put("其他费用合计", feeItemTotal);
|
||||
mapped.put("freightTotal", settlementAmount);
|
||||
mapped.put("运费合计", settlementAmount);
|
||||
if (mapped.get("unitPrice") != null) {
|
||||
mapped.put("单价", mapped.get("unitPrice"));
|
||||
mapped.put("运输单价", mapped.get("unitPrice"));
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
private static int findTransportFeeColumn(Map<Integer, String> headers) {
|
||||
int starred = findColumnExact(headers, "*运输费");
|
||||
if (starred >= 0) {
|
||||
return starred;
|
||||
}
|
||||
return findColumnExact(headers, "运输费");
|
||||
}
|
||||
|
||||
/** 费用列右边界:取运输费之后第一个固定结束表头列下标;无则取表头最大下标 + 1。 */
|
||||
private static int findFeeEndColumn(Map<Integer, String> headers, int transportFeeColumn) {
|
||||
int endColumn = headers.keySet().stream().mapToInt(Integer::intValue).max().orElse(transportFeeColumn) + 1;
|
||||
for (Map.Entry<Integer, String> entry : headers.entrySet()) {
|
||||
int column = entry.getKey();
|
||||
if (column <= transportFeeColumn) {
|
||||
continue;
|
||||
}
|
||||
if (FEE_END_HEADERS.contains(entry.getValue()) && column < endColumn) {
|
||||
endColumn = column;
|
||||
}
|
||||
}
|
||||
return endColumn;
|
||||
}
|
||||
|
||||
private static int findColumnExact(Map<Integer, String> headers, String header) {
|
||||
return headers.entrySet().stream()
|
||||
.filter(entry -> header.equals(entry.getValue()))
|
||||
.mapToInt(Map.Entry::getKey)
|
||||
.findFirst()
|
||||
.orElse(-1);
|
||||
}
|
||||
|
||||
private static String cellString(Cell cell) {
|
||||
if (cell == null) {
|
||||
return null;
|
||||
}
|
||||
String text = DATA_FORMATTER.formatCellValue(cell);
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
text = text.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取日期单元格:Excel 日期序列优先格式化为 yyyy-MM-dd;
|
||||
* 文本短日期(如 1/1/26、10/10/2026)归一为四位年,供后续 LenientDateParser 识别。
|
||||
*/
|
||||
private static String dateCellString(Cell cell) {
|
||||
if (cell == null) {
|
||||
return null;
|
||||
}
|
||||
CellType cellType = cell.getCellType();
|
||||
if (cellType == CellType.FORMULA) {
|
||||
cellType = cell.getCachedFormulaResultType();
|
||||
}
|
||||
if (cellType == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
|
||||
Date date = cell.getDateCellValue();
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||
return DATE_FORMATTER.format(localDate);
|
||||
}
|
||||
String text = cellString(cell);
|
||||
return normalizeExcelShortDate(text);
|
||||
}
|
||||
|
||||
/** 将 Excel 常见短日期文本归一为 yyyy-MM-dd;无法识别时原样返回交由校验层报错。 */
|
||||
private static String normalizeExcelShortDate(String text) {
|
||||
if (text == null || text.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String normalized = text.trim().replace('.', '/').replace('-', '/');
|
||||
String[] parts = normalized.split("[/\\s]+");
|
||||
if (parts.length < 3) {
|
||||
return text.trim();
|
||||
}
|
||||
try {
|
||||
int first = Integer.parseInt(parts[0]);
|
||||
int second = Integer.parseInt(parts[1]);
|
||||
int third = Integer.parseInt(parts[2].replaceAll("[^0-9].*$", ""));
|
||||
int year;
|
||||
int month;
|
||||
int day;
|
||||
if (parts[0].length() == 4) {
|
||||
year = first;
|
||||
month = second;
|
||||
day = third;
|
||||
} else if (parts[2].length() == 4 || parts[2].length() == 2) {
|
||||
// M/d/yy 或 M/d/yyyy(Excel 中文本/短日期常见)
|
||||
month = first;
|
||||
day = second;
|
||||
year = parts[2].length() == 2 ? 2000 + third : third;
|
||||
if (parts[2].length() == 2 && third >= 100) {
|
||||
return text.trim();
|
||||
}
|
||||
} else {
|
||||
return text.trim();
|
||||
}
|
||||
LocalDate localDate = LocalDate.of(year, month, day);
|
||||
return DATE_FORMATTER.format(localDate);
|
||||
} catch (Exception exception) {
|
||||
return text.trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal decimal(String cell, String columnName, Integer excelRowIndex) {
|
||||
if (cell == null || cell.isBlank()) {
|
||||
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
String normalized = cell.trim().replace(",", "");
|
||||
try {
|
||||
return new BigDecimal(normalized).setScale(2, RoundingMode.HALF_UP);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new ImportCellReadException(excelRowIndex, columnName, cell.trim());
|
||||
}
|
||||
}
|
||||
|
||||
/** 可选数值:空白返回 null,避免空里程被当成 0 触发「必须为正数」。 */
|
||||
private static BigDecimal optionalDecimal(String cell, String columnName, Integer excelRowIndex) {
|
||||
if (cell == null || cell.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return decimal(cell, columnName, excelRowIndex);
|
||||
}
|
||||
|
||||
private static Integer intValue(String cell) {
|
||||
if (cell == null || cell.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(cell.trim().replace(",", "")).intValue();
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String blankToNull(String cell) {
|
||||
if (cell == null) {
|
||||
return null;
|
||||
}
|
||||
String text = cell.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static BigDecimal toDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
if (value instanceof BigDecimal bigDecimal) {
|
||||
return bigDecimal;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(value).trim()).setScale(2, RoundingMode.HALF_UP);
|
||||
} catch (NumberFormatException exception) {
|
||||
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Map<String, String> FIXED_HEADER_MAP = buildFixedHeaderMap();
|
||||
|
||||
private static Map<String, String> buildFixedHeaderMap() {
|
||||
Map<String, String> map = new LinkedHashMap<>();
|
||||
map.put("序号", "serialNumber");
|
||||
map.put("原始单号", "originalNo");
|
||||
map.put("配载标识号", "loadingIdentifier");
|
||||
map.put("*车牌号/船班号/船次号/班次号", "vehicleNo");
|
||||
map.put("车牌号/船班号/船次号/班次号", "vehicleNo");
|
||||
map.put("*车牌号/航班号/船号/班列号", "vehicleNo");
|
||||
map.put("车牌号/航班号/船号/班列号", "vehicleNo");
|
||||
map.put("*运输方式", "transportType");
|
||||
map.put("运输方式", "transportType");
|
||||
map.put("运输类型", "transportType");
|
||||
map.put("*运输类型", "transportType");
|
||||
map.put("司机/船长姓名", "driverName");
|
||||
map.put("司机/船长手机号", "driverPhone");
|
||||
map.put("*发货地址", "departureAddress");
|
||||
map.put("发货地址", "departureAddress");
|
||||
map.put("发货联系人", "departureContact");
|
||||
map.put("发货联系人电话", "departurePhone");
|
||||
map.put("*到货地址", "arrivalAddress");
|
||||
map.put("到货地址", "arrivalAddress");
|
||||
map.put("收货联系人", "arrivalContact");
|
||||
map.put("收货联系人电话", "arrivalPhone");
|
||||
map.put("*货物名称", "cargoName");
|
||||
map.put("货物名称", "cargoName");
|
||||
map.put("*货物类型", "cargoType");
|
||||
map.put("货物类型", "cargoType");
|
||||
map.put("包装", "packageType");
|
||||
map.put("*数量", "quantity");
|
||||
map.put("数量", "quantity");
|
||||
map.put("*数量单位", "quantityUnit");
|
||||
map.put("数量单位", "quantityUnit");
|
||||
map.put("规格", "specification");
|
||||
map.put("型号", "model");
|
||||
map.put("里程(km)", "mileage");
|
||||
map.put("里程", "mileage");
|
||||
map.put("运输单价", "unitPrice");
|
||||
map.put("单价", "unitPrice");
|
||||
map.put("*运输费", "transportFee");
|
||||
map.put("运输费", "transportFee");
|
||||
map.put("结算金额", "settlementAmount");
|
||||
map.put("*实际发货时间", "actualStartDate");
|
||||
map.put("实际发货时间", "actualStartDate");
|
||||
map.put("*实际完成时间", "actualEndDate");
|
||||
map.put("实际完成时间", "actualEndDate");
|
||||
map.put("预计发货时间", "planStartDate");
|
||||
map.put("预计完成时间", "planEndDate");
|
||||
map.put("备注", "remark");
|
||||
map.put("同一运单标识号", "waybillIdentifier");
|
||||
return map;
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnore;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 导入结算单 Excel 固定列模型(失败明细兜底导出;费用项由用户在原模板中自行增列)。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(28)
|
||||
@ContentRowHeight(18)
|
||||
public class SettlementImportTemplateExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("序号")
|
||||
private Integer serialNumber;
|
||||
@ExcelProperty("原始单号")
|
||||
private String originalNo;
|
||||
@ExcelProperty("配载标识号")
|
||||
private String loadingIdentifier;
|
||||
@ExcelProperty("*车牌号/航班号/船号/班列号")
|
||||
private String vehicleNo;
|
||||
@ExcelProperty("*运输方式")
|
||||
private String transportType;
|
||||
@ExcelProperty("司机/船长姓名")
|
||||
private String driverName;
|
||||
@ExcelProperty("司机/船长手机号")
|
||||
private String driverPhone;
|
||||
@ExcelProperty("*发货地址")
|
||||
private String departureAddress;
|
||||
@ExcelProperty("发货联系人")
|
||||
private String departureContact;
|
||||
@ExcelProperty("发货联系人电话")
|
||||
private String departurePhone;
|
||||
@ExcelProperty("*到货地址")
|
||||
private String arrivalAddress;
|
||||
@ExcelProperty("收货联系人")
|
||||
private String arrivalContact;
|
||||
@ExcelProperty("收货联系人电话")
|
||||
private String arrivalPhone;
|
||||
@ExcelProperty("*货物名称")
|
||||
private String cargoName;
|
||||
@ExcelProperty("*货物类型")
|
||||
private String cargoType;
|
||||
@ExcelProperty("包装")
|
||||
private String packageType;
|
||||
@ExcelProperty("*数量")
|
||||
private BigDecimal quantity;
|
||||
@ExcelProperty("*数量单位")
|
||||
private String quantityUnit;
|
||||
@ExcelProperty("规格")
|
||||
private String specification;
|
||||
@ExcelProperty("型号")
|
||||
private String model;
|
||||
@ExcelProperty("里程(km)")
|
||||
private BigDecimal mileage;
|
||||
@ExcelProperty("运输单价")
|
||||
private BigDecimal unitPrice;
|
||||
@ExcelProperty("*运输费")
|
||||
private BigDecimal transportFee;
|
||||
/** 模板已去掉结算金额列;仅失败明细内部保留,不导出为表头 */
|
||||
@ExcelIgnore
|
||||
private BigDecimal settlementAmount;
|
||||
@ExcelProperty("*实际发货时间")
|
||||
private String actualStartDate;
|
||||
@ExcelProperty("*实际完成时间")
|
||||
private String actualEndDate;
|
||||
@ExcelProperty("预计发货时间")
|
||||
private String planStartDate;
|
||||
@ExcelProperty("预计完成时间")
|
||||
private String planEndDate;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("同一运单标识号")
|
||||
private String waybillIdentifier;
|
||||
|
||||
/** 导入失败原因(不导出到模板,仅用于失败明细) */
|
||||
@ExcelIgnore
|
||||
private String errorMessage;
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellStyle;
|
||||
import org.apache.poi.ss.usermodel.DataValidation;
|
||||
import org.apache.poi.ss.usermodel.DataValidationConstraint;
|
||||
import org.apache.poi.ss.usermodel.DataValidationHelper;
|
||||
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||
import org.apache.poi.ss.usermodel.Font;
|
||||
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
||||
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.VerticalAlignment;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.util.CellRangeAddressList;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 导入结算单模板导出:填写说明 + 固定数据表头 + 运输方式下拉。
|
||||
* <p>
|
||||
* 模板不预置费用项列;用户可在「*运输费」与后续固定列之间自行插入系统已启用的费用项列。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class SettlementImportTemplateExporter {
|
||||
|
||||
private static final List<String> FIXED_HEADERS_BEFORE_FEE = List.of(
|
||||
"序号",
|
||||
"原始单号",
|
||||
"配载标识号",
|
||||
"*车牌号/航班号/船号/班列号",
|
||||
"*运输方式",
|
||||
"司机/船长姓名",
|
||||
"司机/船长手机号",
|
||||
"*发货地址",
|
||||
"发货联系人",
|
||||
"发货联系人电话",
|
||||
"*到货地址",
|
||||
"收货联系人",
|
||||
"收货联系人电话",
|
||||
"*货物名称",
|
||||
"*货物类型",
|
||||
"包装",
|
||||
"*数量",
|
||||
"*数量单位",
|
||||
"规格",
|
||||
"型号",
|
||||
"里程(km)",
|
||||
"运输单价",
|
||||
"*运输费"
|
||||
);
|
||||
|
||||
private static final List<String> FIXED_HEADERS_AFTER_FEE = List.of(
|
||||
"*实际发货时间",
|
||||
"*实际完成时间",
|
||||
"预计发货时间",
|
||||
"预计完成时间",
|
||||
"备注",
|
||||
"同一运单标识号"
|
||||
);
|
||||
|
||||
private SettlementImportTemplateExporter() {
|
||||
}
|
||||
|
||||
public static void export(HttpServletResponse response, List<String> transportTypeOptions,
|
||||
List<String> feeItemNames) {
|
||||
List<String> headers = buildHeaders();
|
||||
// 运输方式由调用方传入业务字典 transport_type 的字典名称,禁止写死
|
||||
List<String> transportOptions = transportTypeOptions == null ? List.of() : transportTypeOptions.stream()
|
||||
.filter(item -> item != null && !item.isBlank())
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.toList();
|
||||
try (Workbook workbook = new XSSFWorkbook()) {
|
||||
writeInstructionSheet(workbook, transportOptions, feeItemNames);
|
||||
Sheet dataSheet = workbook.createSheet("导入结算单模板");
|
||||
CellStyle headerStyle = createHeaderStyle(workbook);
|
||||
Row headerRow = dataSheet.createRow(0);
|
||||
for (int index = 0; index < headers.size(); index++) {
|
||||
Cell cell = headerRow.createCell(index);
|
||||
cell.setCellValue(headers.get(index));
|
||||
cell.setCellStyle(headerStyle);
|
||||
dataSheet.setColumnWidth(index, Math.min(28, Math.max(12, headers.get(index).length() + 4)) * 256);
|
||||
}
|
||||
int transportTypeColumn = headers.indexOf("*运输方式");
|
||||
if (transportTypeColumn >= 0 && !transportOptions.isEmpty()) {
|
||||
addDropdown(dataSheet, transportTypeColumn, transportOptions);
|
||||
}
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.setHeader("Content-disposition",
|
||||
"attachment;filename=" + URLEncoder.encode("导入结算单模板", StandardCharsets.UTF_8) + ".xlsx");
|
||||
workbook.write(response.getOutputStream());
|
||||
} catch (IOException exception) {
|
||||
throw new ServiceException("导出导入结算单模板失败:" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 固定列模板:不预置费用项列,由用户自行插入。 */
|
||||
private static List<String> buildHeaders() {
|
||||
List<String> headers = new ArrayList<>(FIXED_HEADERS_BEFORE_FEE);
|
||||
headers.addAll(FIXED_HEADERS_AFTER_FEE);
|
||||
return headers;
|
||||
}
|
||||
|
||||
private static void writeInstructionSheet(Workbook workbook, List<String> transportOptions,
|
||||
List<String> feeItemNames) {
|
||||
Sheet sheet = workbook.createSheet("填写说明");
|
||||
CellStyle titleStyle = workbook.createCellStyle();
|
||||
Font titleFont = workbook.createFont();
|
||||
titleFont.setBold(true);
|
||||
titleStyle.setFont(titleFont);
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add("导入结算单填写说明");
|
||||
lines.add("1. 请不要修改「导入结算单模板」工作表的固定表头;带 * 号为必填项。");
|
||||
lines.add("2. 运输方式请从下拉项中选择,须与业务字典 transport_type 的字典名称一致。");
|
||||
lines.add("3. 日期请填写 yyyy-MM-dd(如 2026-01-01);Excel 日期单元格也可直接选择。");
|
||||
lines.add("4. 里程(km) 可为空;填写时须为正数。");
|
||||
lines.add("5. 「*运输费」之后、「*实际发货时间」之前可按需自行插入费用项列,列名须与系统已启用费用项名称完全一致;金额不为 0 时若不存在或已停用则无法导入。");
|
||||
lines.add("6. 模板不含结算金额列;导入后系统按「运输费 + 各费用项」自动合计结算金额。");
|
||||
if (transportOptions != null && !transportOptions.isEmpty()) {
|
||||
lines.add("7. 当前可选运输方式(业务字典 transport_type 名称):"
|
||||
+ String.join("、", transportOptions) + "。");
|
||||
} else {
|
||||
lines.add("7. 运输方式请从业务字典 transport_type 维护的名称中选择。");
|
||||
}
|
||||
if (feeItemNames != null && !feeItemNames.isEmpty()) {
|
||||
lines.add("8. 费用项请按实际需要在模板中自行增列,列名须与系统已启用费用项一致;当前系统已启用费用项:"
|
||||
+ String.join("、", feeItemNames.stream().limit(30).toList())
|
||||
+ (feeItemNames.size() > 30 ? " 等" : "") + "。");
|
||||
} else {
|
||||
lines.add("8. 费用项请按实际需要在模板中自行增列,列名须与系统已启用的费用项主数据名称完全一致。");
|
||||
}
|
||||
for (int index = 0; index < lines.size(); index++) {
|
||||
Row row = sheet.createRow(index);
|
||||
Cell cell = row.createCell(0);
|
||||
cell.setCellValue(lines.get(index));
|
||||
if (index == 0) {
|
||||
cell.setCellStyle(titleStyle);
|
||||
}
|
||||
}
|
||||
sheet.setColumnWidth(0, 120 * 256);
|
||||
}
|
||||
|
||||
private static void addDropdown(Sheet sheet, int columnIndex, List<String> options) {
|
||||
DataValidationHelper helper = sheet.getDataValidationHelper();
|
||||
String[] values = options.toArray(new String[0]);
|
||||
DataValidationConstraint constraint = helper.createExplicitListConstraint(values);
|
||||
CellRangeAddressList addressList = new CellRangeAddressList(1, 2000, columnIndex, columnIndex);
|
||||
DataValidation validation = helper.createValidation(constraint, addressList);
|
||||
validation.setEmptyCellAllowed(true);
|
||||
validation.setShowErrorBox(true);
|
||||
sheet.addValidationData(validation);
|
||||
}
|
||||
|
||||
private static CellStyle createHeaderStyle(Workbook workbook) {
|
||||
CellStyle style = workbook.createCellStyle();
|
||||
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
||||
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
style.setAlignment(HorizontalAlignment.CENTER);
|
||||
style.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||
Font font = workbook.createFont();
|
||||
font.setBold(true);
|
||||
style.setFont(font);
|
||||
return style;
|
||||
}
|
||||
|
||||
}
|
||||
+11
@@ -27,9 +27,20 @@ package org.springblade.transport.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springblade.transport.pojo.entity.BillPayment;
|
||||
|
||||
/** 汇票付款 Mapper。 @author Chill */
|
||||
@Mapper
|
||||
public interface BillPaymentMapper extends BaseMapper<BillPayment> {
|
||||
|
||||
/**
|
||||
* 查询指定租户、指定日期前缀下已使用的最大汇票付款流水号。
|
||||
* 不过滤逻辑删除数据,避免唯一索引仍占用历史编号时重复生成。
|
||||
*/
|
||||
@Select("SELECT COALESCE(MAX(CAST(SUBSTRING(payment_no, CHAR_LENGTH(#{prefix}) + 1) AS UNSIGNED)), 0) "
|
||||
+ "FROM blade_bill_payment WHERE tenant_id = #{tenantId} "
|
||||
+ "AND payment_no LIKE CONCAT(#{prefix}, '%')")
|
||||
Long selectMaxDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
||||
}
|
||||
|
||||
+1
-2
@@ -101,8 +101,7 @@
|
||||
approved_time,
|
||||
kingdee_status,
|
||||
kingdee_fail_reason,
|
||||
kingdee_sync_time
|
||||
approved_time,
|
||||
kingdee_sync_time,
|
||||
(
|
||||
SELECT s.review_status
|
||||
FROM blade_customer_credit_score s
|
||||
|
||||
+9
@@ -29,4 +29,13 @@ public interface FormalSettlementMapper extends BaseMapper<FormalSettlement> {
|
||||
/** 恢复逻辑删除的正式结算单主记录。 */
|
||||
@Update("UPDATE blade_formal_settlement SET is_deleted = 0 WHERE tenant_id = #{tenantId} AND id = #{id} AND is_deleted = 1")
|
||||
int restoreByIdIncludingDeleted(@Param("tenantId") String tenantId, @Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 查询指定租户、指定日期前缀下已使用的最大正式结算单流水号。
|
||||
* 不过滤逻辑删除数据,避免唯一索引仍占用历史编号时重复生成。
|
||||
*/
|
||||
@Select("SELECT COALESCE(MAX(CAST(SUBSTRING(formal_settlement_no, CHAR_LENGTH(#{prefix}) + 1) AS UNSIGNED)), 0) "
|
||||
+ "FROM blade_formal_settlement WHERE tenant_id = #{tenantId} "
|
||||
+ "AND formal_settlement_no LIKE CONCAT(#{prefix}, '%')")
|
||||
Long selectMaxDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
||||
}
|
||||
|
||||
+11
@@ -10,9 +10,20 @@ package org.springblade.transport.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementPayment;
|
||||
|
||||
/** 正式结算付款申请 Mapper。 @author Chill */
|
||||
@Mapper
|
||||
public interface FormalSettlementPaymentMapper extends BaseMapper<FormalSettlementPayment> {
|
||||
|
||||
/**
|
||||
* 查询指定租户、指定日期前缀下已使用的最大付款申请流水号。
|
||||
* 不过滤逻辑删除数据,避免唯一索引仍占用历史编号时重复生成。
|
||||
*/
|
||||
@Select("SELECT COALESCE(MAX(CAST(SUBSTRING(payment_no, CHAR_LENGTH(#{prefix}) + 1) AS UNSIGNED)), 0) "
|
||||
+ "FROM blade_formal_settlement_payment WHERE tenant_id = #{tenantId} "
|
||||
+ "AND payment_no LIKE CONCAT(#{prefix}, '%')")
|
||||
Long selectMaxDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
||||
}
|
||||
|
||||
+11
@@ -27,6 +27,8 @@ package org.springblade.transport.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplication;
|
||||
|
||||
/**
|
||||
@@ -36,4 +38,13 @@ import org.springblade.transport.pojo.entity.InvoiceApplication;
|
||||
*/
|
||||
@Mapper
|
||||
public interface InvoiceApplicationMapper extends BaseMapper<InvoiceApplication> {
|
||||
|
||||
/**
|
||||
* 查询指定租户、指定日期前缀下已使用的最大开票申请流水号。
|
||||
* 不过滤逻辑删除数据,避免唯一索引仍占用历史编号时重复生成。
|
||||
*/
|
||||
@Select("SELECT COALESCE(MAX(CAST(SUBSTRING(application_no, CHAR_LENGTH(#{prefix}) + 1) AS UNSIGNED)), 0) "
|
||||
+ "FROM blade_invoice_application WHERE tenant_id = #{tenantId} "
|
||||
+ "AND application_no LIKE CONCAT(#{prefix}, '%')")
|
||||
Long selectMaxDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
||||
}
|
||||
|
||||
+6
-4
@@ -44,11 +44,13 @@ public interface KingdeeInvoicePoolMapper extends BaseMapper<KingdeeInvoicePool>
|
||||
/**
|
||||
* 金蝶票据池镜像统一分页查询:仅出可用票(剩余可用金额>0 且未使用),认领/登记标记由 EXISTS 子查询直出
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
|
||||
* @param tenantId 当前租户
|
||||
* @param page 分页参数
|
||||
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
|
||||
* @param tenantId 当前租户
|
||||
* @param customerUnitsOnly 客商单位过滤开关:开启时只出销/购方税号均登记在客商发票信息中的发票(与定时同步同开关)
|
||||
* @return 统一视图分页
|
||||
*/
|
||||
IPage<KingdeeInvoicePoolVO> selectInvoicePoolPage(IPage<KingdeeInvoicePoolVO> page,
|
||||
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId);
|
||||
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId,
|
||||
@Param("customerUnitsOnly") boolean customerUnitsOnly);
|
||||
}
|
||||
|
||||
+12
@@ -39,12 +39,24 @@
|
||||
AND p.available_amount > 0 AND p.amount_use_status = '0'
|
||||
</sql>
|
||||
|
||||
<!-- 客商单位过滤:开票单位(销方)与受票单位(购方)税号均须登记在有效客商发票信息中
|
||||
(与定时同步拉取过滤同开关同口径;自定义 SQL 无租户插件,手工带租户与逻辑删除) -->
|
||||
<sql id="customerUnitsFilter">
|
||||
AND EXISTS (SELECT 1 FROM blade_customer_invoice_info ci
|
||||
WHERE ci.is_deleted = 0 AND ci.status = 1 AND ci.tenant_id = p.tenant_id AND ci.tax_no = p.saler_tax_no)
|
||||
AND EXISTS (SELECT 1 FROM blade_customer_invoice_info cj
|
||||
WHERE cj.is_deleted = 0 AND cj.status = 1 AND cj.tenant_id = p.tenant_id AND cj.tax_no = p.buyer_tax_no)
|
||||
</sql>
|
||||
|
||||
<select id="selectInvoicePoolPage" resultType="org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO">
|
||||
SELECT
|
||||
<include refid="invoicePoolVoColumns"/>
|
||||
FROM blade_kingdee_invoice_pool p
|
||||
WHERE p.is_deleted = 0 AND p.status = 1 AND p.tenant_id = #{tenantId}
|
||||
<include refid="usableFilter"/>
|
||||
<if test="customerUnitsOnly">
|
||||
<include refid="customerUnitsFilter"/>
|
||||
</if>
|
||||
<if test="query.invoiceNo != null and query.invoiceNo != ''">
|
||||
AND p.invoice_no LIKE CONCAT('%', #{query.invoiceNo}, '%')
|
||||
</if>
|
||||
|
||||
+11
@@ -21,9 +21,20 @@ package org.springblade.transport.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springblade.transport.pojo.entity.PaymentApplication;
|
||||
|
||||
/** 付款申请 Mapper。 @author Chill */
|
||||
@Mapper
|
||||
public interface PaymentApplicationMapper extends BaseMapper<PaymentApplication> {
|
||||
|
||||
/**
|
||||
* 查询指定租户、指定日期前缀下已使用的最大付款申请流水号。
|
||||
* 不过滤逻辑删除数据,避免唯一索引仍占用历史编号时重复生成。
|
||||
*/
|
||||
@Select("SELECT COALESCE(MAX(CAST(SUBSTRING(payment_no, CHAR_LENGTH(#{prefix}) + 1) AS UNSIGNED)), 0) "
|
||||
+ "FROM blade_payment_application WHERE tenant_id = #{tenantId} "
|
||||
+ "AND payment_no LIKE CONCAT(#{prefix}, '%')")
|
||||
Long selectMaxDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
||||
}
|
||||
|
||||
+10
@@ -10,6 +10,8 @@ package org.springblade.transport.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementAdvance;
|
||||
|
||||
/**
|
||||
@@ -19,4 +21,12 @@ import org.springblade.transport.pojo.entity.PreSettlementAdvance;
|
||||
*/
|
||||
@Mapper
|
||||
public interface PreSettlementAdvanceMapper extends BaseMapper<PreSettlementAdvance> {
|
||||
|
||||
/**
|
||||
* 查询预付单号最大流水(含逻辑删除)。
|
||||
*/
|
||||
@Select("SELECT COALESCE(MAX(CAST(SUBSTRING(advance_no, CHAR_LENGTH(#{prefix}) + 1) AS UNSIGNED)), 0) "
|
||||
+ "FROM blade_pre_settlement_advance WHERE tenant_id = #{tenantId} "
|
||||
+ "AND advance_no LIKE CONCAT(#{prefix}, '%')")
|
||||
Long selectMaxDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
||||
}
|
||||
|
||||
+18
@@ -10,6 +10,8 @@ package org.springblade.transport.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springblade.transport.pojo.entity.PreSettlement;
|
||||
|
||||
/**
|
||||
@@ -19,4 +21,20 @@ import org.springblade.transport.pojo.entity.PreSettlement;
|
||||
*/
|
||||
@Mapper
|
||||
public interface PreSettlementMapper extends BaseMapper<PreSettlement> {
|
||||
|
||||
/**
|
||||
* 查询预结算单号最大流水(含逻辑删除)。
|
||||
*/
|
||||
@Select("SELECT COALESCE(MAX(CAST(SUBSTRING(pre_settlement_no, CHAR_LENGTH(#{prefix}) + 1) AS UNSIGNED)), 0) "
|
||||
+ "FROM blade_pre_settlement WHERE tenant_id = #{tenantId} "
|
||||
+ "AND pre_settlement_no LIKE CONCAT(#{prefix}, '%')")
|
||||
Long selectMaxPreSettlementDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
||||
|
||||
/**
|
||||
* 查询预结算转正式结算单号最大流水(含逻辑删除)。
|
||||
*/
|
||||
@Select("SELECT COALESCE(MAX(CAST(SUBSTRING(formal_settlement_no, CHAR_LENGTH(#{prefix}) + 1) AS UNSIGNED)), 0) "
|
||||
+ "FROM blade_pre_settlement WHERE tenant_id = #{tenantId} "
|
||||
+ "AND formal_settlement_no LIKE CONCAT(#{prefix}, '%')")
|
||||
Long selectMaxFormalSettlementDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user