diff --git a/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java b/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java index 2c12576a..eba9dc15 100644 --- a/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java +++ b/blade-auth/src/main/java/org/springblade/auth/handler/BladeAuthorizationHandler.java @@ -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()); diff --git a/blade-auth/src/main/java/org/springblade/auth/handler/BladeLockHandler.java b/blade-auth/src/main/java/org/springblade/auth/handler/BladeLockHandler.java index ad90ea7d..af423cf4 100644 --- a/blade-auth/src/main/java/org/springblade/auth/handler/BladeLockHandler.java +++ b/blade-auth/src/main/java/org/springblade/auth/handler/BladeLockHandler.java @@ -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()); } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java index 73ba9991..d001adb4 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java @@ -32,6 +32,8 @@ public class FormalSettlementSaveRequest implements Serializable { private String remark; private List summaryFees; private List invoices; + /** 项目预付核销 */ + private List projectAdvanceWriteOffs; @Data public static class DetailAdjustment implements Serializable { diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java index dbe0667d..b2c670b7 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java @@ -76,6 +76,9 @@ public class PreSettlementSaveRequest implements Serializable { @Schema(description = "结算合计费用") private List summaryFees; + @Schema(description = "项目预付核销") + private List projectAdvanceWriteOffs; + @Data @Schema(description = "结算合计费用") public static class SummaryFee implements Serializable { diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementProjectAdvanceWriteOffRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementProjectAdvanceWriteOffRequest.java new file mode 100644 index 00000000..be281de0 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementProjectAdvanceWriteOffRequest.java @@ -0,0 +1,53 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 结算单项目预付核销保存行 + * + * @author Chill + */ +@Data +@Schema(description = "结算单项目预付核销保存行") +public class SettlementProjectAdvanceWriteOffRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "项目预付付款申请ID") + private Long paymentApplicationId; + + @Schema(description = "本次核销金额") + private BigDecimal writeOffAmount; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementProjectAdvanceWriteOff.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementProjectAdvanceWriteOff.java new file mode 100644 index 00000000..57be9c22 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementProjectAdvanceWriteOff.java @@ -0,0 +1,90 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 结算单项目预付核销实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_settlement_project_advance_write_off") +@Schema(description = "结算单项目预付核销") +public class SettlementProjectAdvanceWriteOff extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "结算单类型:pre/formal") + private String settlementType; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "预结算/正式结算单ID") + private Long settlementId; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "项目预付付款申请ID") + private Long paymentApplicationId; + + @Schema(description = "预付单号") + private String paymentNo; + + @Schema(description = "项目预付金额") + private BigDecimal projectAdvanceAmount; + + @Schema(description = "本次核销金额") + private BigDecimal writeOffAmount; + + @Schema(description = "金蝶预付单号") + private String kingdeeBillNo; + + @TableField(exist = false) + @Schema(description = "可核销金额(动态计算)") + private BigDecimal availableWriteOffAmount; + + @TableField(exist = false) + @Schema(description = "预付单创建人姓名") + private String createUserName; + + @TableField(exist = false) + @Schema(description = "预付单创建时间(展示用)") + private Date paymentCreateTime; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java index 2702fbb5..dc889541 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java @@ -20,6 +20,7 @@ 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; @@ -64,6 +65,8 @@ public class FormalSettlementVO extends FormalSettlement implements KingdeeUnloc @TableField(exist = false) private List summaryFees; @TableField(exist = false) private List payments; @TableField(exist = false) private List invoices; + @TableField(exist = false) @Schema(description = "项目预付核销") + private List projectAdvanceWriteOffs; @TableField(exist = false) @Schema(description = "开票申请经金蝶回写的发票结果(含文件地址,只读展示)") private List issuedInvoices; @TableField(exist = false) private List paymentApplications; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java index f4573a63..36955339 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java @@ -35,6 +35,7 @@ import org.springblade.transport.pojo.entity.PreSettlementChangeRecord; import org.springblade.transport.pojo.entity.PreSettlementDetail; import org.springblade.transport.pojo.entity.PreSettlementDetailFee; import org.springblade.transport.pojo.entity.PreSettlementSummaryFee; +import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serial; @@ -100,6 +101,10 @@ public class PreSettlementVO extends PreSettlement { @Schema(description = "预付信息") private List advances; + @TableField(exist = false) + @Schema(description = "项目预付核销") + private List projectAdvanceWriteOffs; + @TableField(exist = false) @Schema(description = "变更记录") private List changeRecords; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectAdvanceWriteOffOptionVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectAdvanceWriteOffOptionVO.java new file mode 100644 index 00000000..42d85b13 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectAdvanceWriteOffOptionVO.java @@ -0,0 +1,72 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 可选项目预付核销选项 + * + * @author Chill + */ +@Data +@Schema(description = "可选项目预付核销选项") +public class ProjectAdvanceWriteOffOptionVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "付款申请ID") + private Long paymentApplicationId; + + @Schema(description = "预付单号") + private String paymentNo; + + @Schema(description = "项目预付金额") + private BigDecimal projectAdvanceAmount; + + @Schema(description = "可核销金额") + private BigDecimal availableWriteOffAmount; + + @Schema(description = "金蝶预付单号") + private String kingdeeBillNo; + + @Schema(description = "创建人姓名") + private String createUserName; + + @Schema(description = "创建时间") + private Date createTime; + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java index cf5e82b3..f27ce70d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java @@ -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"; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java index 92a7f5a2..ae6f6796 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java @@ -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> 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 = "保存付款申请") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementProjectAdvanceWriteOffMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementProjectAdvanceWriteOffMapper.java new file mode 100644 index 00000000..a9aa2767 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementProjectAdvanceWriteOffMapper.java @@ -0,0 +1,40 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff; + +import java.math.BigDecimal; + +/** + * 结算单项目预付核销 Mapper + * + * @author Chill + */ +@Mapper +public interface SettlementProjectAdvanceWriteOffMapper extends BaseMapper { + + /** + * 统计项目预付已核销金额(排除作废结算单,可排除当前结算单)。 + */ + BigDecimal sumWriteOffByPaymentApplication(@Param("paymentApplicationId") Long paymentApplicationId, + @Param("excludeSettlementType") String excludeSettlementType, + @Param("excludeSettlementId") Long excludeSettlementId); + + /** + * 统计结算单核销合计。 + */ + BigDecimal sumWriteOffBySettlement(@Param("settlementType") String settlementType, + @Param("settlementId") Long settlementId); + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementProjectAdvanceWriteOffMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementProjectAdvanceWriteOffMapper.xml new file mode 100644 index 00000000..3c21ab32 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementProjectAdvanceWriteOffMapper.xml @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java index 6d4807af..e68ec0c0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java @@ -28,6 +28,7 @@ import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest; import org.springblade.transport.pojo.entity.PaymentApplication; import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO; import org.springblade.transport.pojo.vo.PaymentApplicationVO; +import org.springblade.transport.pojo.vo.ProjectAdvanceWriteOffOptionVO; import java.util.List; @@ -36,6 +37,11 @@ public interface IPaymentApplicationService extends BaseService selectPage(IPage page, PaymentApplicationVO query); PaymentApplicationVO detail(Long id); PaymentApplicationReferenceAmountVO referenceAmount(String paymentType, Long referenceId, Long excludeId); + + /** 按合同查询可选项目预付核销列表。 */ + List projectAdvanceWriteOffOptions(Long contractId, + String excludeSettlementType, + Long excludeSettlementId); Long saveDraft(PaymentApplicationSaveRequest request); void removeDraft(Long id); void submit(PaymentApplicationStatusRequest request); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ISettlementProjectAdvanceWriteOffService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ISettlementProjectAdvanceWriteOffService.java new file mode 100644 index 00000000..9c1d7368 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ISettlementProjectAdvanceWriteOffService.java @@ -0,0 +1,53 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service; + +import org.springblade.transport.pojo.dto.SettlementProjectAdvanceWriteOffRequest; +import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff; +import org.springblade.transport.pojo.vo.ProjectAdvanceWriteOffOptionVO; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 结算单项目预付核销服务 + * + * @author Chill + */ +public interface ISettlementProjectAdvanceWriteOffService { + + /** + * 按合同查询可选项目预付(审批通过且仍有可核销余额)。 + */ + List listOptions(Long contractId, String excludeSettlementType, + Long excludeSettlementId); + + /** + * 查询结算单已保存的核销明细(含动态可核销金额)。 + */ + List listBySettlement(String settlementType, Long settlementId); + + /** + * 结算单核销金额合计。 + */ + BigDecimal sumBySettlement(String settlementType, Long settlementId); + + /** + * 覆盖保存核销明细,并校验不超过可核销金额与结算单金额。 + */ + void replaceWriteOffs(String settlementType, Long settlementId, Long contractId, + BigDecimal settlementAmount, List writeOffs); + + /** + * 物理清理结算单核销明细(草稿删除时)。 + */ + void deleteBySettlement(String settlementType, Long settlementId); + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java index b2b2da65..2c8ec740 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java @@ -30,6 +30,7 @@ import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.SysCache; import org.springblade.system.cache.UserCache; import org.springblade.system.pojo.entity.Dept; import org.springblade.transport.excel.CommonRouteExportExcel; @@ -217,12 +218,32 @@ public class CommonRouteServiceImpl extends BaseServiceImpl relationApplicationIds = paymentApplicationSettlementMapper.selectList( Wrappers.lambdaQuery() .select(PaymentApplicationSettlement::getPaymentApplicationId) @@ -541,6 +545,12 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() .eq(FormalSettlementInvoice::getFormalSettlementId, id)); + projectAdvanceWriteOffService.deleteBySettlement(TransportBusiConstant.SETTLEMENT_BILL_FORMAL, id); changeRecordMapper.delete(Wrappers.lambdaQuery() .eq(FormalSettlementChangeRecord::getFormalSettlementId, id)); removeById(id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index 742b5931..38057c1a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -98,7 +98,9 @@ import org.springblade.transport.pojo.vo.PaymentApplicationVO; import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IKingdeeBillLockService; import org.springblade.transport.service.IPaymentApplicationService; +import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService; import org.springblade.transport.service.KingdeeSyncContext; +import org.springblade.transport.pojo.vo.ProjectAdvanceWriteOffOptionVO; import org.springblade.transport.support.KingdeeApBillSyncSupport; import org.springblade.transport.support.KingdeeAttachmentSyncSupport; import org.springblade.transport.support.KingdeeSyncLockSupport; @@ -159,6 +161,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl projectAdvanceWriteOffOptions(Long contractId, + String excludeSettlementType, + Long excludeSettlementId) { + return projectAdvanceWriteOffService.listOptions(contractId, excludeSettlementType, excludeSettlementId); + } + @Override @Transactional(rollbackFor = Exception.class) public Long saveDraft(PaymentApplicationSaveRequest request) { @@ -1741,12 +1755,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl money(item.getSettlementAmount()).subtract(cumulativeFormalSettlementApplied(item.getId(), entity.getId())).max(BigDecimal.ZERO)) + .map(item -> payableOfFormalSettlement(item.getId(), item.getSettlementAmount(), entity.getId())) .reduce(BigDecimal.ZERO, BigDecimal::add); if (money(entity.getAppliedAmount()).compareTo(payableAmount) > 0) throw new ServiceException("申请付款金额不能超过所选结算单可付款金额合计"); FormalSettlement first = settlements.get(0); @@ -1796,15 +1810,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())); - BigDecimal totalBase = settlements.stream().map(item -> money(item.getSettlementAmount()).subtract( - cumulativeFormalSettlementApplied(item.getId(), entity.getId())).max(BigDecimal.ZERO)) + BigDecimal totalBase = settlements.stream() + .map(item -> payableOfFormalSettlement(item.getId(), item.getSettlementAmount(), entity.getId())) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal remainingApplied = money(entity.getAppliedAmount()); BigDecimal remainingPaid = money(entity.getPaidAmount()); for (int index = 0; index < settlements.size(); index++) { FormalSettlement settlement = settlements.get(index); - BigDecimal base = money(settlement.getSettlementAmount()).subtract( - cumulativeFormalSettlementApplied(settlement.getId(), entity.getId())).max(BigDecimal.ZERO); + BigDecimal base = payableOfFormalSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId()); BigDecimal applied = index == settlements.size() - 1 ? remainingApplied : money(entity.getAppliedAmount()).multiply(base).divide(totalBase, 2, RoundingMode.DOWN); BigDecimal paid = index == settlements.size() - 1 ? remainingPaid @@ -1881,6 +1894,24 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl selectPage(IPage page, PreSettlementVO query) { @@ -198,6 +201,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl advances = listAdvances(id); advances.forEach(advance -> advance.setCreateUserName(UserCache.getUserRealName(advance.getCreateUser()))); vo.setAdvances(advances); + vo.setProjectAdvanceWriteOffs(projectAdvanceWriteOffService.listBySettlement( + TransportBusiConstant.SETTLEMENT_BILL_PRE, id)); vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.lambdaQuery() .eq(PreSettlementChangeRecord::getPreSettlementId, id) .eq(PreSettlementChangeRecord::getIsDeleted, 0) @@ -346,6 +351,12 @@ public class PreSettlementServiceImpl extends BaseServiceImpl + * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.constant.TransportBusiConstant; +import org.springblade.transport.mapper.PaymentApplicationMapper; +import org.springblade.transport.mapper.SettlementProjectAdvanceWriteOffMapper; +import org.springblade.transport.pojo.dto.SettlementProjectAdvanceWriteOffRequest; +import org.springblade.transport.pojo.entity.PaymentApplication; +import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff; +import org.springblade.transport.pojo.vo.ProjectAdvanceWriteOffOptionVO; +import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * 结算单项目预付核销服务实现 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class SettlementProjectAdvanceWriteOffServiceImpl implements ISettlementProjectAdvanceWriteOffService { + + private final SettlementProjectAdvanceWriteOffMapper writeOffMapper; + private final PaymentApplicationMapper paymentApplicationMapper; + + @Override + public List listOptions(Long contractId, String excludeSettlementType, + Long excludeSettlementId) { + if (contractId == null) { + throw new ServiceException("请先选择合同"); + } + List applications = paymentApplicationMapper.selectList( + Wrappers.lambdaQuery() + .eq(PaymentApplication::getContractId, contractId) + .eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_PROJECT_ADVANCE) + .eq(PaymentApplication::getApprovalStatus, TransportBusiConstant.APPROVED) + .eq(PaymentApplication::getIsDeleted, 0) + .orderByDesc(PaymentApplication::getCreateTime)); + List options = new ArrayList<>(); + for (PaymentApplication application : applications) { + BigDecimal projectAdvanceAmount = money(application.getAppliedAmount()); + BigDecimal usedAmount = money(writeOffMapper.sumWriteOffByPaymentApplication( + application.getId(), excludeSettlementType, excludeSettlementId)); + BigDecimal availableAmount = projectAdvanceAmount.subtract(usedAmount).max(BigDecimal.ZERO); + if (availableAmount.compareTo(BigDecimal.ZERO) <= 0) { + continue; + } + ProjectAdvanceWriteOffOptionVO option = new ProjectAdvanceWriteOffOptionVO(); + option.setPaymentApplicationId(application.getId()); + option.setPaymentNo(application.getPaymentNo()); + option.setProjectAdvanceAmount(projectAdvanceAmount); + option.setAvailableWriteOffAmount(availableAmount); + option.setKingdeeBillNo(application.getKingdeeBillNo()); + option.setCreateUserName(UserCache.getUserRealName(application.getCreateUser())); + option.setCreateTime(application.getCreateTime()); + options.add(option); + } + return options; + } + + @Override + public List listBySettlement(String settlementType, Long settlementId) { + if (Func.isEmpty(settlementType) || settlementId == null) { + return List.of(); + } + List rows = writeOffMapper.selectList( + Wrappers.lambdaQuery() + .eq(SettlementProjectAdvanceWriteOff::getSettlementType, settlementType) + .eq(SettlementProjectAdvanceWriteOff::getSettlementId, settlementId) + .eq(SettlementProjectAdvanceWriteOff::getIsDeleted, 0) + .orderByAsc(SettlementProjectAdvanceWriteOff::getCreateTime) + .orderByAsc(SettlementProjectAdvanceWriteOff::getId)); + for (SettlementProjectAdvanceWriteOff row : rows) { + PaymentApplication application = paymentApplicationMapper.selectById(row.getPaymentApplicationId()); + BigDecimal projectAdvanceAmount = application == null + ? money(row.getProjectAdvanceAmount()) : money(application.getAppliedAmount()); + BigDecimal usedAmount = money(writeOffMapper.sumWriteOffByPaymentApplication( + row.getPaymentApplicationId(), settlementType, settlementId)); + row.setProjectAdvanceAmount(projectAdvanceAmount); + row.setAvailableWriteOffAmount(projectAdvanceAmount.subtract(usedAmount).max(BigDecimal.ZERO)); + if (application != null) { + row.setPaymentNo(application.getPaymentNo()); + row.setKingdeeBillNo(application.getKingdeeBillNo()); + row.setCreateUserName(UserCache.getUserRealName(application.getCreateUser())); + row.setPaymentCreateTime(application.getCreateTime()); + } else { + row.setCreateUserName(UserCache.getUserRealName(row.getCreateUser())); + row.setPaymentCreateTime(row.getCreateTime()); + } + } + return rows; + } + + @Override + public BigDecimal sumBySettlement(String settlementType, Long settlementId) { + if (Func.isEmpty(settlementType) || settlementId == null) { + return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + } + return money(writeOffMapper.sumWriteOffBySettlement(settlementType, settlementId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void replaceWriteOffs(String settlementType, Long settlementId, Long contractId, + BigDecimal settlementAmount, List writeOffs) { + if (Func.isEmpty(settlementType) || settlementId == null) { + throw new ServiceException("结算单不能为空"); + } + if (contractId == null) { + throw new ServiceException("合同不能为空"); + } + List requestRows = writeOffs == null ? List.of() : writeOffs; + BigDecimal settlementLimit = money(settlementAmount); + BigDecimal totalWriteOff = BigDecimal.ZERO; + Set paymentIds = new HashSet<>(); + List entities = new ArrayList<>(); + for (SettlementProjectAdvanceWriteOffRequest request : requestRows) { + if (request == null || request.getPaymentApplicationId() == null) { + throw new ServiceException("请选择项目预付单"); + } + if (!paymentIds.add(request.getPaymentApplicationId())) { + throw new ServiceException("同一项目预付单不可重复核销"); + } + BigDecimal writeOffAmount = money(request.getWriteOffAmount()); + if (writeOffAmount.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("本次核销金额必须大于0"); + } + PaymentApplication application = paymentApplicationMapper.selectById(request.getPaymentApplicationId()); + if (application == null || Objects.equals(application.getIsDeleted(), 1)) { + throw new ServiceException("项目预付单不存在"); + } + if (!TransportBusiConstant.PAYMENT_TYPE_PROJECT_ADVANCE.equals(application.getPaymentType())) { + throw new ServiceException("仅可核销项目预付单"); + } + if (!TransportBusiConstant.APPROVED.equals(application.getApprovalStatus())) { + throw new ServiceException("仅可选择审核通过的项目预付单"); + } + if (!Objects.equals(contractId, application.getContractId())) { + throw new ServiceException("项目预付单与结算单合同不一致"); + } + BigDecimal projectAdvanceAmount = money(application.getAppliedAmount()); + BigDecimal usedAmount = money(writeOffMapper.sumWriteOffByPaymentApplication( + application.getId(), settlementType, settlementId)); + BigDecimal availableAmount = projectAdvanceAmount.subtract(usedAmount).max(BigDecimal.ZERO); + if (writeOffAmount.compareTo(availableAmount) > 0) { + throw new ServiceException("预付单" + Func.toStr(application.getPaymentNo(), "") + + "本次核销金额不能超过可核销金额"); + } + totalWriteOff = totalWriteOff.add(writeOffAmount); + SettlementProjectAdvanceWriteOff entity = new SettlementProjectAdvanceWriteOff(); + entity.setSettlementType(settlementType); + entity.setSettlementId(settlementId); + entity.setPaymentApplicationId(application.getId()); + entity.setPaymentNo(application.getPaymentNo()); + entity.setProjectAdvanceAmount(projectAdvanceAmount); + entity.setWriteOffAmount(writeOffAmount); + entity.setKingdeeBillNo(application.getKingdeeBillNo()); + entity.setStatus(1); + entity.setIsDeleted(0); + entities.add(entity); + } + if (totalWriteOff.compareTo(settlementLimit) > 0) { + throw new ServiceException("项目预付核销合计不能超过结算单金额"); + } + writeOffMapper.delete(Wrappers.lambdaQuery() + .eq(SettlementProjectAdvanceWriteOff::getSettlementType, settlementType) + .eq(SettlementProjectAdvanceWriteOff::getSettlementId, settlementId)); + for (SettlementProjectAdvanceWriteOff entity : entities) { + writeOffMapper.insert(entity); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteBySettlement(String settlementType, Long settlementId) { + if (Func.isEmpty(settlementType) || settlementId == null) { + return; + } + writeOffMapper.delete(Wrappers.lambdaQuery() + .eq(SettlementProjectAdvanceWriteOff::getSettlementType, settlementType) + .eq(SettlementProjectAdvanceWriteOff::getSettlementId, settlementId)); + } + + private BigDecimal money(BigDecimal value) { + return value == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) + : value.setScale(2, RoundingMode.HALF_UP); + } + +} diff --git a/doc/sql/transport/blade_project_apply_undertake_dept_multi_20261014.sql b/doc/sql/transport/blade_project_apply_undertake_dept_multi_20261014.sql index b2d5d222..2b96a193 100644 --- a/doc/sql/transport/blade_project_apply_undertake_dept_multi_20261014.sql +++ b/doc/sql/transport/blade_project_apply_undertake_dept_multi_20261014.sql @@ -1,4 +1,64 @@ --- 平台公司支持多选:ID 逗号分隔,名称顿号分隔 -ALTER TABLE `blade_project_apply` - MODIFY COLUMN `undertake_dept_id` varchar(500) DEFAULT NULL COMMENT '平台公司ID,多个逗号分隔', - MODIFY COLUMN `undertake_dept_name` varchar(500) DEFAULT NULL COMMENT '平台公司,多个顿号分隔'; +-- 项目立项平台公司多选:ID 逗号分隔,名称顿号分隔 +-- 原字段:undertake_dept_id bigint、undertake_dept_name varchar(100) +-- 选择 2 个及以上平台公司时,若未扩容会报 Incorrect integer value / Data truncation +-- 整列索引挂在 varchar(500) 上可能因键长超限导致 ALTER 失败,故先删索引再改前缀索引 + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS `upgrade_project_apply_undertake_dept_multi_20261014`$$ +CREATE PROCEDURE `upgrade_project_apply_undertake_dept_multi_20261014`() +BEGIN + DECLARE db_name varchar(128); + DECLARE need_modify tinyint DEFAULT 0; + SET db_name = DATABASE(); + + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = db_name + AND table_name = 'blade_project_apply' + AND column_name = 'undertake_dept_id' + AND ( + data_type IN ('bigint', 'int', 'integer') + OR (data_type IN ('varchar', 'char') AND IFNULL(character_maximum_length, 0) < 500) + ) + ) OR EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = db_name + AND table_name = 'blade_project_apply' + AND column_name = 'undertake_dept_name' + AND data_type IN ('varchar', 'char') + AND IFNULL(character_maximum_length, 0) < 500 + ) THEN + SET need_modify = 1; + END IF; + + IF need_modify = 1 THEN + IF EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = db_name + AND table_name = 'blade_project_apply' + AND index_name = 'idx_project_apply_dept' + ) THEN + ALTER TABLE `blade_project_apply` DROP INDEX `idx_project_apply_dept`; + END IF; + + ALTER TABLE `blade_project_apply` + MODIFY COLUMN `undertake_dept_id` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '平台公司ID,多个逗号分隔', + MODIFY COLUMN `undertake_dept_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '平台公司,多个顿号分隔'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = db_name + AND table_name = 'blade_project_apply' + AND index_name = 'idx_project_apply_dept' + ) THEN + ALTER TABLE `blade_project_apply` + ADD INDEX `idx_project_apply_dept` (`undertake_dept_id`(191)) USING BTREE; + END IF; +END$$ + +CALL `upgrade_project_apply_undertake_dept_multi_20261014`()$$ +DROP PROCEDURE `upgrade_project_apply_undertake_dept_multi_20261014`$$ + +DELIMITER ; diff --git a/doc/sql/transport/blade_project_contract_management.sql b/doc/sql/transport/blade_project_contract_management.sql index 59890b1b..62a9ecd2 100644 --- a/doc/sql/transport/blade_project_contract_management.sql +++ b/doc/sql/transport/blade_project_contract_management.sql @@ -60,7 +60,7 @@ CREATE TABLE `blade_project_apply` ( `void_reason` varchar(500) DEFAULT NULL COMMENT '作废原因', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `uk_project_apply_name` (`project_name`, `is_deleted`) USING BTREE, - KEY `idx_project_apply_dept` (`undertake_dept_id`) USING BTREE, + KEY `idx_project_apply_dept` (`undertake_dept_id`(191)) USING BTREE, KEY `idx_project_apply_status` (`approval_status`) USING BTREE, KEY `idx_project_apply_create_time` (`create_time`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目立项'; @@ -80,8 +80,8 @@ CREATE TABLE `blade_temporary_credit_limit` ( `project_id` bigint(20) DEFAULT NULL COMMENT '项目ID', `project_code` varchar(100) DEFAULT NULL COMMENT '项目编号', `project_name` varchar(100) DEFAULT NULL COMMENT '项目名称', - `undertake_dept_id` varchar(500) DEFAULT NULL COMMENT '平台公司ID,多个逗号分隔', - `undertake_dept_name` varchar(500) DEFAULT NULL COMMENT '平台公司,多个顿号分隔', + `undertake_dept_id` bigint(20) DEFAULT NULL COMMENT '承办部门ID', + `undertake_dept_name` varchar(100) DEFAULT NULL COMMENT '承办部门', `project_fund_limit` decimal(18,2) DEFAULT NULL COMMENT '项目资金使用额度(万元)', `used_fund_limit` decimal(18,2) DEFAULT NULL COMMENT '已使用项目资金额度(万元)', `remaining_fund_limit` decimal(18,2) DEFAULT NULL COMMENT '剩余项目资金使用额度(万元)', diff --git a/doc/sql/transport/blade_settlement_project_advance_write_off_20261016.sql b/doc/sql/transport/blade_settlement_project_advance_write_off_20261016.sql new file mode 100644 index 00000000..7aba7877 --- /dev/null +++ b/doc/sql/transport/blade_settlement_project_advance_write_off_20261016.sql @@ -0,0 +1,23 @@ +-- 结算单项目预付核销明细 +CREATE TABLE IF NOT EXISTS `blade_settlement_project_advance_write_off` ( + `id` bigint NOT NULL COMMENT '主键', + `tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID', + `settlement_type` varchar(20) NOT NULL COMMENT '结算单类型 pre/formal', + `settlement_id` bigint NOT NULL COMMENT '预结算/正式结算单ID', + `payment_application_id` bigint NOT NULL COMMENT '项目预付付款申请ID', + `payment_no` varchar(64) DEFAULT NULL COMMENT '预付单号', + `project_advance_amount` decimal(18,2) DEFAULT 0.00 COMMENT '项目预付金额', + `write_off_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '本次核销金额', + `kingdee_bill_no` varchar(100) DEFAULT NULL COMMENT '金蝶预付单号', + `create_user` bigint DEFAULT NULL COMMENT '创建人', + `create_dept` bigint DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `status` int DEFAULT 1 COMMENT '业务状态', + `is_deleted` int DEFAULT 0 COMMENT '是否已删除', + PRIMARY KEY (`id`), + KEY `idx_spa_write_off_settlement` (`settlement_type`,`settlement_id`), + KEY `idx_spa_write_off_payment` (`payment_application_id`), + KEY `idx_spa_write_off_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='结算单项目预付核销';