✨ 新增项目预付核销,并修复平台公司多选、常用线路组织过滤与登录 IP 连坐锁

结算附件支持项目预付核销并扣减可付款金额;加固项目平台公司多选字段扩容脚本;常用线路新增归属组织与列表筛选对齐;登录失败仅按账号锁定,取消 IP 级连坐。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-16 02:01:58 +08:00
parent 0657cfe69f
commit 93a6555293
24 changed files with 817 additions and 29 deletions
@@ -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());
}
@@ -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 {
@@ -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 {
@@ -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;
}
@@ -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;
}
@@ -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<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;
@@ -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;
@@ -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;
}
@@ -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";
@@ -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 = "保存付款申请")
@@ -0,0 +1,40 @@
/**
* 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.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<SettlementProjectAdvanceWriteOff> {
/**
* 统计项目预付已核销金额(排除作废结算单,可排除当前结算单)。
*/
BigDecimal sumWriteOffByPaymentApplication(@Param("paymentApplicationId") Long paymentApplicationId,
@Param("excludeSettlementType") String excludeSettlementType,
@Param("excludeSettlementId") Long excludeSettlementId);
/**
* 统计结算单核销合计。
*/
BigDecimal sumWriteOffBySettlement(@Param("settlementType") String settlementType,
@Param("settlementId") Long settlementId);
}
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.transport.mapper.SettlementProjectAdvanceWriteOffMapper">
<select id="sumWriteOffByPaymentApplication" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(w.write_off_amount), 0)
FROM blade_settlement_project_advance_write_off w
LEFT JOIN blade_pre_settlement p
ON w.settlement_type = 'pre'
AND w.settlement_id = p.id
AND p.is_deleted = 0
LEFT JOIN blade_formal_settlement f
ON w.settlement_type = 'formal'
AND w.settlement_id = f.id
AND f.is_deleted = 0
WHERE w.is_deleted = 0
AND w.payment_application_id = #{paymentApplicationId}
AND (
(w.settlement_type = 'pre' AND p.id IS NOT NULL AND IFNULL(p.approval_status, '') &lt;&gt; 'voided')
OR (w.settlement_type = 'formal' AND f.id IS NOT NULL AND IFNULL(f.approval_status, '') &lt;&gt; 'voided')
)
<if test="excludeSettlementType != null and excludeSettlementId != null">
AND NOT (w.settlement_type = #{excludeSettlementType} AND w.settlement_id = #{excludeSettlementId})
</if>
</select>
<select id="sumWriteOffBySettlement" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(write_off_amount), 0)
FROM blade_settlement_project_advance_write_off
WHERE is_deleted = 0
AND settlement_type = #{settlementType}
AND settlement_id = #{settlementId}
</select>
</mapper>
@@ -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<PaymentApplicati
IPage<PaymentApplicationVO> selectPage(IPage<PaymentApplication> page, PaymentApplicationVO query);
PaymentApplicationVO detail(Long id);
PaymentApplicationReferenceAmountVO referenceAmount(String paymentType, Long referenceId, Long excludeId);
/** 按合同查询可选项目预付核销列表。 */
List<ProjectAdvanceWriteOffOptionVO> projectAdvanceWriteOffOptions(Long contractId,
String excludeSettlementType,
Long excludeSettlementId);
Long saveDraft(PaymentApplicationSaveRequest request);
void removeDraft(Long id);
void submit(PaymentApplicationStatusRequest request);
@@ -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>
* 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<ProjectAdvanceWriteOffOptionVO> listOptions(Long contractId, String excludeSettlementType,
Long excludeSettlementId);
/**
* 查询结算单已保存的核销明细(含动态可核销金额)。
*/
List<SettlementProjectAdvanceWriteOff> listBySettlement(String settlementType, Long settlementId);
/**
* 结算单核销金额合计。
*/
BigDecimal sumBySettlement(String settlementType, Long settlementId);
/**
* 覆盖保存核销明细,并校验不超过可核销金额与结算单金额。
*/
void replaceWriteOffs(String settlementType, Long settlementId, Long contractId,
BigDecimal settlementAmount, List<SettlementProjectAdvanceWriteOffRequest> writeOffs);
/**
* 物理清理结算单核销明细(草稿删除时)。
*/
void deleteBySettlement(String settlementType, Long settlementId);
}
@@ -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<CommonRouteMapper, C
commonRoute.setArrivalPhone(TransportBusinessSupport.trimToNull(commonRoute.getArrivalPhone()));
commonRoute.setDeptName(TransportBusinessSupport.trimToNull(commonRoute.getDeptName()));
commonRoute.setRemark(TransportBusinessSupport.trimToNull(commonRoute.getRemark()));
if (Func.isEmpty(commonRoute.getDeptId())) {
// 仅新增时绑定组织;编辑沿用原组织(submit 中已从旧记录写回)。
if (Func.isEmpty(commonRoute.getId())) {
bindDeptOnCreate(commonRoute);
}
if (commonRoute.getStatus() == null) { commonRoute.setStatus(1); }
}
/**
* 新增绑定所属组织:普通用户始终落当前组织;超管可按入参组织保存,未传时回落当前组织。
*/
private void bindDeptOnCreate(CommonRoute commonRoute) {
if (commonRoute == null) {
return;
}
boolean administrator = TransportBusinessSupport.isAdministrator();
if (!administrator || Func.isEmpty(commonRoute.getDeptId()) || commonRoute.getDeptId() <= 0) {
Dept dept = TransportBusinessSupport.currentDept("常用线路");
commonRoute.setDeptId(dept.getId());
commonRoute.setDeptName(dept.getDeptName());
return;
}
if (commonRoute.getStatus() == null) { commonRoute.setStatus(1); }
Dept dept = SysCache.getDept(commonRoute.getDeptId());
if (Func.isEmpty(dept)) {
throw new ServiceException("所属组织不存在或已删除");
}
commonRoute.setDeptName(TransportBusinessSupport.trimToNull(dept.getDeptName()));
}
private void validate(CommonRoute commonRoute) {
@@ -88,6 +88,7 @@ import org.springblade.transport.service.IInvoiceReceiptService;
import org.springblade.transport.service.IKingdeeBillLockService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.service.KingdeeSyncContext;
import org.springblade.transport.support.KingdeeApBillSyncSupport;
@@ -170,6 +171,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
private final SettlementAdjustmentMapper settlementAdjustmentMapper;
private final IContractManageService contractManageService;
private final IPreSettlementService preSettlementService;
private final ISettlementProjectAdvanceWriteOffService projectAdvanceWriteOffService;
private final IWaybillService waybillService;
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
private final KingdeeArBillSyncSupport kingdeeArBillSyncSupport;
@@ -318,6 +320,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
.eq(FormalSettlementPayment::getFormalSettlementId, id).orderByDesc(FormalSettlementPayment::getCreateTime)));
vo.setInvoices(listInvoices(settlement));
vo.setIssuedInvoices(listIssuedInvoices(id));
vo.setProjectAdvanceWriteOffs(projectAdvanceWriteOffService.listBySettlement(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, id));
Set<Long> relationApplicationIds = paymentApplicationSettlementMapper.selectList(
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.select(PaymentApplicationSettlement::getPaymentApplicationId)
@@ -541,6 +545,12 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
applySummaryRequest(settlement.getId(), request.getSummaryFees());
refreshSettlementAmount(settlement);
rebuildInvoices(settlement, request.getInvoices());
projectAdvanceWriteOffService.replaceWriteOffs(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL,
settlement.getId(),
settlement.getContractId(),
settlement.getSettlementAmount(),
request.getProjectAdvanceWriteOffs());
if (!creating && !restoring) {
saveChange(settlement.getId(), "结算单基本信息", null, "调整",
"保存正式结算单" + settlement.getFormalSettlementNo(), request.getRemark());
@@ -562,6 +572,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
.eq(FormalSettlementSummaryFee::getFormalSettlementId, id));
invoiceMapper.delete(Wrappers.<FormalSettlementInvoice>lambdaQuery()
.eq(FormalSettlementInvoice::getFormalSettlementId, id));
projectAdvanceWriteOffService.deleteBySettlement(TransportBusiConstant.SETTLEMENT_BILL_FORMAL, id);
changeRecordMapper.delete(Wrappers.<FormalSettlementChangeRecord>lambdaQuery()
.eq(FormalSettlementChangeRecord::getFormalSettlementId, id));
removeById(id);
@@ -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<PaymentApplic
private final BillLedgerUsageMapper billLedgerUsageMapper;
private final TemporaryCreditLimitMapper temporaryCreditLimitMapper;
private final IFormalSettlementService formalSettlementService;
private final ISettlementProjectAdvanceWriteOffService projectAdvanceWriteOffService;
private final IKingdeeBillLockService kingdeeBillLockService;
private final IBfiService bfiService;
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
@@ -301,13 +304,24 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
BigDecimal cumulativeAppliedAmount = preSettlementReference
? cumulativePreSettlementApplied(referenceId, excludeId)
: cumulativeFormalSettlementApplied(referenceId, excludeId);
BigDecimal writeOffAmount = preSettlementReference
? projectAdvanceWriteOffService.sumBySettlement(TransportBusiConstant.SETTLEMENT_BILL_PRE, referenceId)
: projectAdvanceWriteOffService.sumBySettlement(TransportBusiConstant.SETTLEMENT_BILL_FORMAL, referenceId);
PaymentApplicationReferenceAmountVO amount = new PaymentApplicationReferenceAmountVO();
amount.setSettlementAmount(settlementAmount);
amount.setCumulativeAppliedAmount(cumulativeAppliedAmount);
amount.setPayableAmount(settlementAmount.subtract(cumulativeAppliedAmount));
amount.setCumulativeAppliedAmount(cumulativeAppliedAmount.add(writeOffAmount));
amount.setPayableAmount(settlementAmount.subtract(cumulativeAppliedAmount).subtract(writeOffAmount)
.max(BigDecimal.ZERO));
return amount;
}
@Override
public List<ProjectAdvanceWriteOffOptionVO> 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<PaymentApplic
if (source == null) throw new ServiceException("正式结算单不存在");
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的正式结算单");
entity.setSettlementId(source.getId()); entity.setSettlementNo(source.getFormalSettlementNo()); entity.setPreSettlementId(null); entity.setPreSettlementNo(null);
entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(cumulativeFormalSettlementApplied(source.getId(), entity.getId()))); entity.setBillType("正式结算单");
entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(payableOfFormalSettlement(source.getId(), source.getSettlementAmount(), entity.getId())); entity.setBillType("正式结算单");
} else if (request.getPreSettlementId() != null) {
PreSettlement source = preSettlementMapper.selectById(request.getPreSettlementId());
if (source == null) throw new ServiceException("预结算单不存在");
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的预结算单");
entity.setPreSettlementId(source.getId()); entity.setPreSettlementNo(source.getPreSettlementNo()); entity.setSettlementId(null); entity.setSettlementNo(null); entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(cumulativePreSettlementApplied(source.getId(), entity.getId()))); entity.setBillType("预结算单");
entity.setPreSettlementId(source.getId()); entity.setPreSettlementNo(source.getPreSettlementNo()); entity.setSettlementId(null); entity.setSettlementNo(null); entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(payableOfPreSettlement(source.getId(), source.getSettlementAmount(), entity.getId())); entity.setBillType("预结算单");
} else {
entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(request.getPayableAmount()); entity.setBillType(request.getBillType());
}
@@ -1782,7 +1796,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
BigDecimal settlementAmount = settlements.stream().map(FormalSettlement::getSettlementAmount)
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal payableAmount = settlements.stream()
.map(item -> 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 BaseServiceImpl<PaymentApplic
if (settlements.isEmpty()) return;
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.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<PaymentApplic
.ne(excludeId != null, PaymentApplication::getId, excludeId));
}
/** 正式结算可付款 = 结算金额 − 已申请付款 − 项目预付核销 */
private BigDecimal payableOfFormalSettlement(Long settlementId, BigDecimal settlementAmount, Long excludeId) {
return money(settlementAmount)
.subtract(cumulativeFormalSettlementApplied(settlementId, excludeId))
.subtract(projectAdvanceWriteOffService.sumBySettlement(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, settlementId))
.max(BigDecimal.ZERO);
}
/** 预结算可付款 = 结算金额 − 已申请付款 − 项目预付核销 */
private BigDecimal payableOfPreSettlement(Long preSettlementId, BigDecimal settlementAmount, Long excludeId) {
return money(settlementAmount)
.subtract(cumulativePreSettlementApplied(preSettlementId, excludeId))
.subtract(projectAdvanceWriteOffService.sumBySettlement(
TransportBusiConstant.SETTLEMENT_BILL_PRE, preSettlementId))
.max(BigDecimal.ZERO);
}
private void fillBillLedger(PaymentApplication entity, PaymentApplicationSaveRequest request) {
if (!isBillPayment(request.getPaymentMethod())) {
entity.setBillLedgerId(null);
@@ -36,6 +36,7 @@ import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.mapper.PreSettlementAdvanceMapper;
import org.springblade.transport.mapper.PreSettlementChangeRecordMapper;
import org.springblade.transport.mapper.PreSettlementDetailFeeMapper;
@@ -67,6 +68,7 @@ import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.wrapper.PreSettlementWrapper;
import org.springblade.system.cache.UserCache;
@@ -124,6 +126,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
private final ICustomerArchiveService customerArchiveService;
private final IWaybillService waybillService;
private final ISysClient sysClient;
private final ISettlementProjectAdvanceWriteOffService projectAdvanceWriteOffService;
@Override
public IPage<PreSettlementVO> selectPage(IPage<PreSettlement> page, PreSettlementVO query) {
@@ -198,6 +201,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
List<PreSettlementAdvance> 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.<PreSettlementChangeRecord>lambdaQuery()
.eq(PreSettlementChangeRecord::getPreSettlementId, id)
.eq(PreSettlementChangeRecord::getIsDeleted, 0)
@@ -346,6 +351,12 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
rebuildSummaryFees(settlement.getId(), true);
applySummaryRequest(settlement.getId(), contractChanged ? List.of() : request.getSummaryFees());
refreshSettlementAmount(settlement);
projectAdvanceWriteOffService.replaceWriteOffs(
TransportBusiConstant.SETTLEMENT_BILL_PRE,
settlement.getId(),
settlement.getContractId(),
settlement.getSettlementAmount(),
request.getProjectAdvanceWriteOffs());
if (created) {
saveChange(settlement.getId(), "结算单基本信息", null, "新增",
"新增预结算单" + settlement.getPreSettlementNo(), "");
@@ -362,6 +373,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
}
releaseSourceDetails(settlement, listDetails(id));
deleteChildren(id);
projectAdvanceWriteOffService.deleteBySettlement(TransportBusiConstant.SETTLEMENT_BILL_PRE, id);
deleteLogic(List.of(id));
}
@@ -166,7 +166,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
appendProjectChangeRecord(projectApply, CHANGE_TYPE_DRAFT, null, STATUS_DRAFT, "草稿",
buildProjectChangeSnapshot(beforeRecord), buildProjectChangeSnapshot(projectApply));
}
return saveOrUpdate(projectApply);
return persistProjectApply(projectApply);
}
@Override
@@ -185,7 +185,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
projectApply.setApprovalStatus(STATUS_REVIEWING);
projectApply.setCurrentNode(resolveApprovalNode(projectApply));
projectApply.setCurrentProcessor("待处理");
return saveOrUpdate(projectApply);
return persistProjectApply(projectApply);
}
@Override
@@ -772,6 +772,8 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
private void validateLength(ProjectApply projectApply) {
TransportBusinessSupport.validateLength(projectApply.getProjectName(), 50, "项目名称不能超过50个字");
TransportBusinessSupport.validateLength(projectApply.getProjectShortName(), 20, "项目简称不能超过20个字");
TransportBusinessSupport.validateLength(projectApply.getUndertakeDeptId(), 500, "平台公司选择过多,请减少后再提交");
TransportBusinessSupport.validateLength(projectApply.getUndertakeDeptName(), 500, "平台公司名称过长,请减少选择后再提交");
TransportBusinessSupport.validateLength(projectApply.getSourceRemark(), 300, "项目由来说明不能超过300个字");
TransportBusinessSupport.validateLength(projectApply.getCargoQuantity(), 100, "预估货物数量不能超过100个字");
TransportBusinessSupport.validateLength(projectApply.getTransportRoute(), 100, "运输线路不能超过100个字");
@@ -1023,6 +1025,37 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
});
}
private boolean persistProjectApply(ProjectApply projectApply) {
try {
return saveOrUpdate(projectApply);
} catch (RuntimeException exception) {
throw translateUndertakeDeptPersistError(exception, projectApply);
}
}
/**
* 多选平台公司写入失败时,提示执行字段扩容脚本(原列为 bigint / varchar(100))。
*/
private RuntimeException translateUndertakeDeptPersistError(RuntimeException exception, ProjectApply projectApply) {
String message = Func.toStr(exception.getMessage());
Throwable cause = exception.getCause();
while (cause != null) {
message = message + " " + Func.toStr(cause.getMessage());
cause = cause.getCause();
}
String lowerMessage = message.toLowerCase();
boolean multiUndertake = Func.isNotEmpty(projectApply.getUndertakeDeptId())
&& (projectApply.getUndertakeDeptId().contains(",") || projectApply.getUndertakeDeptId().contains(","));
boolean columnHint = lowerMessage.contains("undertake_dept")
|| lowerMessage.contains("incorrect integer")
|| lowerMessage.contains("data truncation")
|| lowerMessage.contains("data too long");
if (multiUndertake && columnHint) {
return new ServiceException("平台公司多选保存失败,请先执行脚本 blade_project_apply_undertake_dept_multi_20261014.sql 扩容字段后重试");
}
return exception;
}
private void normalizeUndertakeDept(ProjectApply projectApply) {
if (projectApply == null) {
return;
@@ -0,0 +1,211 @@
/**
* 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.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<ProjectAdvanceWriteOffOptionVO> listOptions(Long contractId, String excludeSettlementType,
Long excludeSettlementId) {
if (contractId == null) {
throw new ServiceException("请先选择合同");
}
List<PaymentApplication> applications = paymentApplicationMapper.selectList(
Wrappers.<PaymentApplication>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<ProjectAdvanceWriteOffOptionVO> 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<SettlementProjectAdvanceWriteOff> listBySettlement(String settlementType, Long settlementId) {
if (Func.isEmpty(settlementType) || settlementId == null) {
return List.of();
}
List<SettlementProjectAdvanceWriteOff> rows = writeOffMapper.selectList(
Wrappers.<SettlementProjectAdvanceWriteOff>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<SettlementProjectAdvanceWriteOffRequest> writeOffs) {
if (Func.isEmpty(settlementType) || settlementId == null) {
throw new ServiceException("结算单不能为空");
}
if (contractId == null) {
throw new ServiceException("合同不能为空");
}
List<SettlementProjectAdvanceWriteOffRequest> requestRows = writeOffs == null ? List.of() : writeOffs;
BigDecimal settlementLimit = money(settlementAmount);
BigDecimal totalWriteOff = BigDecimal.ZERO;
Set<Long> paymentIds = new HashSet<>();
List<SettlementProjectAdvanceWriteOff> 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.<SettlementProjectAdvanceWriteOff>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.<SettlementProjectAdvanceWriteOff>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);
}
}
@@ -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 ;
@@ -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 '剩余项目资金使用额度(万元)',
@@ -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='结算单项目预付核销';