feat: 金蝶票池选票统一查镜像(收票登记与结算单认领统一分页查询+按票号补查+A801-1限定正常票,下线实时透传假分页)

This commit is contained in:
刘泉佳
2026-09-22 21:24:00 +08:00
parent b17f07b6c1
commit 56e3e9deb0
12 changed files with 506 additions and 301 deletions
@@ -1,74 +0,0 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 正式结算单认领查询 全票池发票查询条件
*
* @author Chill
*/
@Data
public class FormalSettlementInvoicePoolQueryDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 页码,默认 1
*/
private Long current = 1L;
/**
* 每页条数,默认 10,最大 500
*/
private Long size = 10L;
/**
* 发票号码
*/
private String invoiceNo;
/**
* 开票日期起 yyyy-MM-dd
*/
private String invoiceDateStart;
/**
* 开票日期止 yyyy-MM-dd
*/
private String invoiceDateEnd;
/**
* 销方税号
*/
private String salerTaxNo;
/**
* 购方税号
*/
private String buyerTaxNo;
/**
* 单据使用状态,多个用英文逗号分隔[1未用,30在用,60已用,65已入账]
*/
private String expenseStatus;
}
@@ -0,0 +1,117 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 金蝶票据池镜像统一查询条件(收票登记选票与正式结算单认领共用)
*
* @author Chill
*/
@Data
@Schema(description = "金蝶票据池镜像统一查询条件")
public class KingdeeInvoicePoolQueryDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 当前页
*/
@Schema(description = "当前页")
private Long current = 1L;
/**
* 每页条数
*/
@Schema(description = "每页条数")
private Long size = 10L;
/**
* 发票号码,模糊
*/
@Schema(description = "发票号码(模糊)")
private String invoiceNo;
/**
* 开票日期起 yyyy-MM-dd
*/
@Schema(description = "开票日期起 yyyy-MM-dd")
private String invoiceDateStart;
/**
* 开票日期止 yyyy-MM-dd
*/
@Schema(description = "开票日期止 yyyy-MM-dd")
private String invoiceDateEnd;
/**
* 开票单位(销方名称),模糊
*/
@Schema(description = "开票单位(销方名称,模糊)")
private String salerName;
/**
* 受票单位(购方名称),模糊
*/
@Schema(description = "受票单位(购方名称,模糊)")
private String buyerName;
/**
* 发票类型名称,模糊(镜像存中文名)
*/
@Schema(description = "发票类型名称(模糊)")
private String invoiceType;
/**
* 销方税号,精确
*/
@Schema(description = "销方税号")
private String salerTaxNo;
/**
* 购方税号,精确
*/
@Schema(description = "购方税号")
private String buyerTaxNo;
/**
* 认领过滤:true 仅已认领,false 仅未认领,null 不过滤(已认领=存在正式结算单发票行)
*/
@Schema(description = "认领过滤:true已认领/false未认领/null不限")
private Boolean claimed;
/**
* 登记过滤:true 仅已登记,false 仅未登记,null 不过滤(已登记=存在非作废收票登记)
*/
@Schema(description = "登记过滤:true已登记/false未登记/null不限")
private Boolean registered;
}
@@ -1,87 +0,0 @@
/**
* 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 lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 正式结算单认领查询 全票池发票VO
*
* @author Chill
*/
@Data
public class FormalSettlementInvoicePoolVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 发票号码
*/
private String invoiceNo;
/**
* 发票流水号
*/
private String serialNo;
/**
* 开票日期
*/
private String invoiceDate;
/**
* 发票类型名称
*/
private String invoiceTypeName;
/**
* 税率(百分数,如 13);票池明细缺失时为 null
*/
private BigDecimal taxRate;
/**
* 发票金额(含税,取票池价税合计)
*/
private BigDecimal invoiceAmount;
/**
* 可匹配发票金额(含税,本次口径等于价税合计)
*/
private BigDecimal availableInvoiceAmount;
/**
* 认领金额(默认等于可匹配金额,由前端按结算余额分配)
*/
private BigDecimal matchedAmount;
/**
* 开票单位(销方名称)
*/
private String salerName;
/**
* 受票单位(购方名称)
*/
private String buyerName;
/**
* 附件信息JSON,结构 {"source":"piaozone","path":"A801-2下载path","name":"文件名"}
*/
private String attachmentJson;
}
@@ -0,0 +1,153 @@
/**
* 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;
/**
* 金蝶票据池镜像统一视图(含本地认领/登记标记;认领口径:可匹配与默认认领金额=价税合计)
*
* @author Chill
*/
@Data
@Schema(description = "金蝶票据池镜像统一视图")
public class KingdeeInvoicePoolVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 镜像主键,收票登记保存绑定用(kingdeeInvoicePoolId) */
@Schema(description = "镜像主键")
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 发票号码 */
@Schema(description = "发票号码")
private String invoiceNo;
/** 金蝶发票流水号(票池唯一标识) */
@Schema(description = "金蝶发票流水号")
private String serialNo;
/** 开票日期 yyyy-MM-dd */
@Schema(description = "开票日期")
private String invoiceDate;
/** 发票类型名称(镜像落库时已由编码转名称) */
@Schema(description = "发票类型名称")
private String invoiceType;
/** 税率百分数,如 13 */
@Schema(description = "税率百分数")
private BigDecimal taxRate;
/** 价税合计 */
@Schema(description = "发票金额(价税合计)")
private BigDecimal invoiceAmount;
/** 税额 */
@Schema(description = "税额")
private BigDecimal taxAmount;
/** 受票单位(购方名称) */
@Schema(description = "受票单位")
private String receiverName;
/** 购方税号 */
@Schema(description = "购方税号")
private String buyerTaxNo;
/** 开票单位(销方名称) */
@Schema(description = "开票单位")
private String issuerName;
/** 销方税号 */
@Schema(description = "销方税号")
private String salerTaxNo;
/** 销方开户行名称 */
@Schema(description = "销方开户行名称")
private String bankName;
/** 销方银行账号 */
@Schema(description = "销方银行账号")
private String bankAccount;
/** 销方开户行及账号 */
@Schema(description = "开户行及账号")
private String issuingBank;
/** 销方电话 */
@Schema(description = "电话")
private String phone;
/** 客户邮箱 JSON 串 */
@Schema(description = "客户邮箱JSON")
private String customerEmails;
/** 部门邮箱 JSON 串 */
@Schema(description = "部门邮箱JSON")
private String departmentEmails;
/** 金蝶流水号(登记侧展示为金蝶单据号) */
@Schema(description = "金蝶单据号")
private String kingdeeBillNo;
/** 同步状态 unsynced/synced/failed */
@Schema(description = "金蝶同步状态")
private String kingdeeStatus;
/** 查验状态 */
@Schema(description = "查验状态")
private String checkStatus;
/** 附件 JSON(票易通下载 path) */
@Schema(description = "附件信息JSON")
private String attachmentsJson;
/** 可认领金额,口径同发票金额 */
@Schema(description = "可认领金额")
private BigDecimal availableInvoiceAmount;
/** 默认认领金额,口径同发票金额 */
@Schema(description = "默认认领金额")
private BigDecimal matchedAmount;
/** 是否已被正式结算单认领 */
@Schema(description = "是否已认领")
private Boolean claimed;
/** 是否已存在非作废收票登记 */
@Schema(description = "是否已登记")
private Boolean registered;
}
@@ -27,14 +27,15 @@ import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest;
import org.springblade.transport.pojo.dto.FormalSettlementBatchPaymentRequest;
import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest;
import org.springblade.transport.pojo.dto.FormalSettlementInvoiceClaimRequest;
import org.springblade.transport.pojo.dto.FormalSettlementInvoicePoolQueryDTO;
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest;
import org.springblade.transport.pojo.entity.FormalSettlementDetailFee;
import org.springblade.transport.pojo.vo.FormalSettlementInvoicePoolVO;
import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
import org.springblade.transport.pojo.vo.FormalSettlementVO;
import org.springblade.transport.excel.FormalSettlementExcel;
import org.springblade.transport.pojo.vo.PreSettlementVO;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IInvoiceReceiptService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IReceiptFlowService;
import org.springframework.web.bind.annotation.GetMapping;
@@ -63,6 +64,7 @@ public class FormalSettlementController extends BladeController {
private final IFormalSettlementService formalSettlementService;
private final IPreSettlementService preSettlementService;
private final IReceiptFlowService receiptFlowService;
private final IInvoiceReceiptService invoiceReceiptService;
@GetMapping("/list")
@ApiOperationSupport(order = 1)
@@ -260,8 +262,15 @@ public class FormalSettlementController extends BladeController {
@GetMapping("/invoice-pool")
@ApiOperationSupport(order = 21)
@Operation(summary = "查询发票云全票池发票(认领查询)")
public R<IPage<FormalSettlementInvoicePoolVO>> invoicePool(FormalSettlementInvoicePoolQueryDTO query) {
return R.data(formalSettlementService.invoicePoolQuery(query));
@Operation(summary = "金蝶票据池镜像分页查询(认领查询)")
public R<IPage<KingdeeInvoicePoolVO>> invoicePool(KingdeeInvoicePoolQueryDTO query) {
return R.data(invoiceReceiptService.invoicePoolPage(query));
}
@PostMapping("/invoice-pool/fetch")
@ApiOperationSupport(order = 22)
@Operation(summary = "按发票号码从金蝶票池补查单张发票")
public R<KingdeeInvoicePoolVO> fetchInvoicePool(@RequestParam String invoiceNo) {
return R.data(invoiceReceiptService.fetchKingdeeInvoiceByNo(invoiceNo));
}
}
@@ -37,8 +37,9 @@ import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
import org.springblade.transport.service.IInvoiceReceiptService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -80,9 +81,16 @@ public class InvoiceReceiptController extends BladeController {
@GetMapping("/invoice-pool")
@ApiOperationSupport(order = 3)
@Operation(summary = "查询金蝶进项发票票据池")
public R<List<KingdeeInvoicePool>> invoicePool(@RequestParam(required = false) String keyword) {
return R.data(invoiceReceiptService.invoicePool(keyword));
@Operation(summary = "金蝶票据池镜像分页查询(含认领/登记标记)")
public R<IPage<KingdeeInvoicePoolVO>> invoicePool(KingdeeInvoicePoolQueryDTO query) {
return R.data(invoiceReceiptService.invoicePoolPage(query));
}
@PostMapping("/invoice-pool/fetch")
@ApiOperationSupport(order = 14)
@Operation(summary = "按发票号码从金蝶票池补查单张发票")
public R<KingdeeInvoicePoolVO> fetchInvoicePool(@RequestParam String invoiceNo) {
return R.data(invoiceReceiptService.fetchKingdeeInvoiceByNo(invoiceNo));
}
@GetMapping("/settlement-candidates")
@@ -26,8 +26,12 @@
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
/**
* 金蝶进项发票票据池 Mapper 接口
@@ -36,4 +40,25 @@ import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
*/
@Mapper
public interface KingdeeInvoicePoolMapper extends BaseMapper<KingdeeInvoicePool> {
/**
* 金蝶票据池镜像统一分页查询:认领/登记标记由 EXISTS 子查询直出
*
* @param page 分页参数
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
* @param tenantId 当前租户
* @return 统一视图分页
*/
IPage<KingdeeInvoicePoolVO> selectInvoicePoolPage(IPage<KingdeeInvoicePoolVO> page,
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId);
/**
* 按发票号码查询单张镜像统一视图(补查落库后回读)
*
* @param invoiceNo 发票号码
* @param tenantId 当前租户
* @return 统一视图,无镜像时 null
*/
KingdeeInvoicePoolVO selectInvoicePoolVO(@Param("invoiceNo") String invoiceNo,
@Param("tenantId") String tenantId);
}
@@ -0,0 +1,87 @@
<?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.KingdeeInvoicePoolMapper">
<!-- 已认领:任一有效正式结算单发票行出现过(结算单作废不删发票行,须关联主单剔除作废与已删;自定义 SQL 无租户插件,手工带租户与逻辑删除) -->
<sql id="claimedExists">
EXISTS (SELECT 1 FROM blade_formal_settlement_invoice c
WHERE c.is_deleted = 0 AND c.tenant_id = p.tenant_id AND c.invoice_no = p.invoice_no
AND EXISTS (SELECT 1 FROM blade_formal_settlement s
WHERE s.id = c.formal_settlement_id AND s.is_deleted = 0
AND s.approval_status != 'voided'))
</sql>
<!-- 已登记:存在非作废收票登记 -->
<sql id="registeredExists">
EXISTS (SELECT 1 FROM blade_invoice_receipt r
WHERE r.is_deleted = 0 AND r.tenant_id = p.tenant_id
AND r.approval_status != 'voided' AND r.invoice_no = p.invoice_no)
</sql>
<!-- 统一视图列:价税合计与可认领/默认认领金额同源;镜像 invoice_type 落库即中文名 -->
<sql id="invoicePoolVoColumns">
p.id, p.invoice_no, p.serial_no,
DATE_FORMAT(p.invoice_date, '%Y-%m-%d') AS invoice_date,
p.invoice_type, p.tax_rate, p.invoice_amount, p.tax_amount,
p.receiver_name, p.buyer_tax_no, p.issuer_name, p.saler_tax_no,
p.bank_name, p.bank_account, p.issuing_bank, p.phone,
p.customer_emails, p.department_emails,
p.kingdee_bill_no, p.kingdee_status, p.check_status, p.attachments_json,
p.invoice_amount AS available_invoice_amount,
p.invoice_amount AS matched_amount,
<include refid="claimedExists"/> AS claimed,
<include refid="registeredExists"/> AS registered
</sql>
<select id="selectInvoicePoolPage" resultType="org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO">
SELECT
<include refid="invoicePoolVoColumns"/>
FROM blade_kingdee_invoice_pool p
WHERE p.is_deleted = 0 AND p.status = 1 AND p.tenant_id = #{tenantId}
<if test="query.invoiceNo != null and query.invoiceNo != ''">
AND p.invoice_no LIKE CONCAT('%', #{query.invoiceNo}, '%')
</if>
<if test="query.salerName != null and query.salerName != ''">
AND p.issuer_name LIKE CONCAT('%', #{query.salerName}, '%')
</if>
<if test="query.buyerName != null and query.buyerName != ''">
AND p.receiver_name LIKE CONCAT('%', #{query.buyerName}, '%')
</if>
<if test="query.invoiceType != null and query.invoiceType != ''">
AND p.invoice_type LIKE CONCAT('%', #{query.invoiceType}, '%')
</if>
<if test="query.salerTaxNo != null and query.salerTaxNo != ''">
AND p.saler_tax_no = #{query.salerTaxNo}
</if>
<if test="query.buyerTaxNo != null and query.buyerTaxNo != ''">
AND p.buyer_tax_no = #{query.buyerTaxNo}
</if>
<if test="query.invoiceDateStart != null and query.invoiceDateStart != ''">
AND p.invoice_date &gt;= #{query.invoiceDateStart}
</if>
<if test="query.invoiceDateEnd != null and query.invoiceDateEnd != ''">
AND p.invoice_date &lt;= #{query.invoiceDateEnd}
</if>
<if test="query.claimed != null and query.claimed">
AND <include refid="claimedExists"/>
</if>
<if test="query.claimed != null and !query.claimed">
AND NOT <include refid="claimedExists"/>
</if>
<if test="query.registered != null and query.registered">
AND <include refid="registeredExists"/>
</if>
<if test="query.registered != null and !query.registered">
AND NOT <include refid="registeredExists"/>
</if>
ORDER BY p.source_updated_time DESC, p.create_time DESC
</select>
<select id="selectInvoicePoolVO" resultType="org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO">
SELECT
<include refid="invoicePoolVoColumns"/>
FROM blade_kingdee_invoice_pool p
WHERE p.is_deleted = 0 AND p.status = 1 AND p.tenant_id = #{tenantId} AND p.invoice_no = #{invoiceNo}
LIMIT 1
</select>
</mapper>
@@ -13,7 +13,6 @@ import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest;
import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.vo.FormalSettlementInvoicePoolVO;
import org.springblade.transport.pojo.vo.FormalSettlementVO;
import org.springblade.transport.pojo.vo.PreSettlementVO;
import org.springblade.transport.pojo.entity.PreSettlement;
@@ -23,7 +22,6 @@ import java.util.List;
import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest;
import org.springblade.transport.pojo.dto.FormalSettlementBatchPaymentRequest;
import org.springblade.transport.pojo.dto.FormalSettlementInvoiceClaimRequest;
import org.springblade.transport.pojo.dto.FormalSettlementInvoicePoolQueryDTO;
/**
* 正式结算单服务
@@ -50,5 +48,4 @@ public interface IFormalSettlementService extends BaseService<FormalSettlement>
String applyPayment(FormalSettlementPaymentRequest request);
List<String> applyPayments(FormalSettlementBatchPaymentRequest request);
void claimInvoices(FormalSettlementInvoiceClaimRequest request);
IPage<FormalSettlementInvoicePoolVO> invoicePoolQuery(FormalSettlementInvoicePoolQueryDTO query);
}
@@ -29,10 +29,12 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.entity.InvoiceReceipt;
import org.springblade.transport.pojo.entity.InvoiceReceiptRecord;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
import java.util.List;
import java.util.Map;
@@ -48,7 +50,22 @@ public interface IInvoiceReceiptService extends BaseService<InvoiceReceipt> {
InvoiceReceiptVO detail(Long id);
List<KingdeeInvoicePool> invoicePool(String keyword);
/**
* 金蝶票据池镜像分页查询(含本地认领/登记标记),收票登记选票与正式结算单认领共用
*
* @param query 统一查询条件
* @return 镜像分页
*/
IPage<KingdeeInvoicePoolVO> invoicePoolPage(KingdeeInvoicePoolQueryDTO query);
/**
* 按发票号码从金蝶票池精确补查单张发票:与定时同步同一套白名单校验,
* 通过后 upsert 入镜像并返回;不受增量时间窗口约束
*
* @param invoiceNo 发票号码
* @return 镜像统一视图
*/
KingdeeInvoicePoolVO fetchKingdeeInvoiceByNo(String invoiceNo);
List<Map<String, Object>> settlementCandidates(String keyword, Long receiptId);
@@ -11,7 +11,6 @@ package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
@@ -19,14 +18,6 @@ import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.thirdparty.bfi.constant.BfiConstant;
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoicePoolQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoicePoolQueryDataDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoicePoolItemVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoicePoolVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.transport.mapper.FormalSettlementDetailMapper;
import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper;
import org.springblade.transport.mapper.FormalSettlementChangeRecordMapper;
@@ -54,7 +45,6 @@ import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest;
import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest;
import org.springblade.transport.pojo.dto.FormalSettlementBatchPaymentRequest;
import org.springblade.transport.pojo.dto.FormalSettlementInvoiceClaimRequest;
import org.springblade.transport.pojo.dto.FormalSettlementInvoicePoolQueryDTO;
import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest;
import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest;
import org.springblade.transport.pojo.entity.FormalSettlement;
@@ -80,7 +70,6 @@ import org.springblade.transport.pojo.entity.ReceivablePayableDetail;
import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee;
import org.springblade.transport.pojo.entity.SettlementAdjustment;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.FormalSettlementInvoicePoolVO;
import org.springblade.transport.pojo.vo.FormalSettlementVO;
import org.springblade.transport.pojo.vo.PreSettlementVO;
import org.springblade.transport.pojo.vo.SettlementAdjustmentVO;
@@ -110,7 +99,6 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -151,7 +139,6 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
private final IWaybillService waybillService;
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
private final KingdeeArBillSyncSupport kingdeeArBillSyncSupport;
private final IBfiService bfiService;
@Override
public IPage<FormalSettlementVO> selectPage(IPage<FormalSettlement> page, FormalSettlementVO query) {
@@ -1449,109 +1436,4 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
private String required(String value, String field) { if (Func.isEmpty(value)) throw new ServiceException("请填写" + field); return value; }
private String firstNotEmpty(Object first, Object second) { return Func.isNotEmpty(first) ? String.valueOf(first) : String.valueOf(second); }
private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("内容不能超过" + max + "个字"); return value; }
@Override
public IPage<FormalSettlementInvoicePoolVO> invoicePoolQuery(FormalSettlementInvoicePoolQueryDTO query) {
long pageNo = Math.max(query.getCurrent() == null ? 1 : query.getCurrent(), 1);
// 上限 499:预留 1 条给 hasMore 探测,单次远程请求不超过 rim 契约 pageSize 上限 500
long pageSize = Math.min(Math.max(query.getSize() == null ? 10 : query.getSize(), 1), 499);
String invoiceNo = query.getInvoiceNo();
String invoiceDateStart = query.getInvoiceDateStart();
String invoiceDateEnd = query.getInvoiceDateEnd();
String salerTaxNo = query.getSalerTaxNo();
String buyerTaxNo = query.getBuyerTaxNo();
String expenseStatus = query.getExpenseStatus();
// A801-1 rim 契约不支持全量且不返回总数:透传 pageNo/pageSize 单页查询,
// 多取 1 条探测是否还有下一页,total 合成为当前已知的最小精确值供前端分页器翻页
BfiInvoicePoolQueryDataDTO data = new BfiInvoicePoolQueryDataDTO();
data.setPageNo((int) pageNo);
data.setPageSize((int) pageSize + 1);
if (StringUtil.isNotBlank(invoiceNo)) {
data.setInvoiceNo(invoiceNo.trim());
}
if (StringUtil.isNotBlank(invoiceDateStart)) {
data.setInvoiceDateStart(invoiceDateStart.trim());
}
if (StringUtil.isNotBlank(invoiceDateEnd)) {
data.setInvoiceDateEnd(invoiceDateEnd.trim());
}
if (StringUtil.isNotBlank(salerTaxNo)) {
data.setSalerTaxNo(salerTaxNo.trim());
}
if (StringUtil.isNotBlank(buyerTaxNo)) {
data.setBuyerTaxNo(buyerTaxNo.trim());
}
if (StringUtil.isNotBlank(expenseStatus)) {
data.setExpenseStatus(expenseStatus.trim());
}
BfiInvoicePoolQueryDTO param = new BfiInvoicePoolQueryDTO();
param.setQueryParam(data);
BfiPageVO<BfiInvoicePoolVO> poolPage = bfiService.queryInvoicePool(param);
List<FormalSettlementInvoicePoolVO> invoices = poolPage.getRows() == null ? List.of()
: poolPage.getRows().stream()
.map(this::toInvoicePoolVO)
.toList();
// 多取的 1 条仅用于 hasMore 判断,不返回给前端
boolean hasMore = invoices.size() > pageSize;
List<FormalSettlementInvoicePoolVO> records = hasMore ? invoices.subList(0, (int) pageSize) : invoices;
long total = hasMore ? pageNo * pageSize + 1 : (pageNo - 1) * pageSize + records.size();
log.info("正式结算单认领查询 全票池第 {} 页 {} 条/页,返回 {} 条,hasMore:{}", pageNo, pageSize, records.size(), hasMore);
Page<FormalSettlementInvoicePoolVO> page = new Page<>(pageNo, pageSize);
page.setRecords(records);
page.setTotal(total);
return page;
}
/**
* A801-1 全票池发票 -> 正式结算单认领VO 字段映射
*/
private FormalSettlementInvoicePoolVO toInvoicePoolVO(BfiInvoicePoolVO poolInvoice) {
FormalSettlementInvoicePoolVO vo = new FormalSettlementInvoicePoolVO();
// 发票号码
vo.setInvoiceNo(poolInvoice.getInvoiceNo());
// 发票流水号,票池发票唯一标识
vo.setSerialNo(poolInvoice.getSerialNo());
// 开票日期 yyyy-MM-dd
vo.setInvoiceDate(poolInvoice.getInvoiceDate());
// 发票类型编码转名称,未知编码原样返回
vo.setInvoiceTypeName(BfiConstant.INVOICE_TYPE_NAMES.getOrDefault(poolInvoice.getInvoiceType(),
poolInvoice.getInvoiceType()));
// 税率:顶层优先、回退首条明细,小数转百分数(0.13 -> 13)
vo.setTaxRate(resolveTaxRate(poolInvoice));
// 价税合计作为发票金额,可匹配金额、默认认领金额与发票金额同源
BigDecimal totalAmount = parseAmount(poolInvoice.getTotalAmount());
vo.setInvoiceAmount(totalAmount);
vo.setAvailableInvoiceAmount(totalAmount);
vo.setMatchedAmount(totalAmount);
// 开票单位(销方名称)
vo.setSalerName(poolInvoice.getSalerName());
// 受票单位(购方名称)
vo.setBuyerName(poolInvoice.getBuyerName());
// 附件信息JSON:path 供 A801-2 进项发票下载,文件名以发票号码命名
Map<String, Object> attachment = new LinkedHashMap<>();
attachment.put("source", "piaozone");
attachment.put("path", poolInvoice.getDownloadUrl());
attachment.put("name", poolInvoice.getInvoiceNo() + ".pdf");
vo.setAttachmentJson(JsonUtil.toJson(attachment));
return vo;
}
/**
* 税率优先取顶层税率的小数值换算为百分数,如 0.13 -> 13;顶层为空时回退取首条明细税率,均无时返回 null
*/
private BigDecimal resolveTaxRate(BfiInvoicePoolVO poolInvoice) {
if (StringUtil.isNotBlank(poolInvoice.getTaxRate())) {
return new BigDecimal(poolInvoice.getTaxRate()).multiply(BigDecimal.valueOf(100));
}
return Optional.ofNullable(poolInvoice.getItems()).orElse(List.of()).stream()
.map(BfiInvoicePoolItemVO::getTaxRate)
.filter(StringUtil::isNotBlank)
.findFirst()
.map(taxRate -> new BigDecimal(taxRate).multiply(BigDecimal.valueOf(100)))
.orElse(null);
}
private BigDecimal parseAmount(String value) {
return StringUtil.isBlank(value) ? BigDecimal.ZERO : new BigDecimal(value);
}
}
@@ -28,6 +28,7 @@ package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -58,6 +59,7 @@ import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper;
import org.springblade.transport.mapper.KingdeeInvoicePoolMapper;
import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.entity.CustomerContact;
import org.springblade.transport.pojo.entity.FormalSettlement;
@@ -66,6 +68,7 @@ import org.springblade.transport.pojo.entity.InvoiceReceiptRecord;
import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
import org.springblade.transport.service.IInvoiceReceiptService;
import org.springblade.transport.wrapper.InvoiceReceiptWrapper;
import org.springframework.beans.factory.ObjectProvider;
@@ -78,6 +81,7 @@ import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
@@ -135,6 +139,9 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
/** 定时绑定 single-flight 锁键前缀,按租户区分,同租户同一时刻只允许一轮全票池同步 */
private static final String POOL_SYNC_LOCK_PREFIX = "transport:invoice-receipt:kingdee-pool-sync:";
/** A801-1 查询限定发票状态:0 正常(票易通字典),作废/异常票不进镜像、不可补查 */
private static final String POOL_INVOICE_STATUS_NORMAL = "0";
private final KingdeeInvoicePoolMapper invoicePoolMapper;
private final InvoiceReceiptSettlementMapper settlementRelationMapper;
private final InvoiceReceiptRecordMapper recordMapper;
@@ -202,15 +209,75 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
}
@Override
public List<KingdeeInvoicePool> invoicePool(String keyword) {
return invoicePoolMapper.selectList(Wrappers.<KingdeeInvoicePool>lambdaQuery()
.eq(KingdeeInvoicePool::getStatus, 1)
.and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(KingdeeInvoicePool::getInvoiceNo, keyword)
.or().like(KingdeeInvoicePool::getIssuerName, keyword)
.or().like(KingdeeInvoicePool::getReceiverName, keyword))
.orderByDesc(KingdeeInvoicePool::getSourceUpdatedTime)
.orderByDesc(KingdeeInvoicePool::getCreateTime)
.last("limit 200"));
public IPage<KingdeeInvoicePoolVO> invoicePoolPage(KingdeeInvoicePoolQueryDTO query) {
long current = query.getCurrent() == null ? 1L : Math.max(query.getCurrent(), 1L);
long size = query.getSize() == null ? 10L : Math.min(Math.max(query.getSize(), 1L), 100L);
query.setCurrent(current);
query.setSize(size);
query.setInvoiceNo(trimOrNull(query.getInvoiceNo()));
query.setSalerName(trimOrNull(query.getSalerName()));
query.setBuyerName(trimOrNull(query.getBuyerName()));
query.setInvoiceType(trimOrNull(query.getInvoiceType()));
query.setSalerTaxNo(trimOrNull(query.getSalerTaxNo()));
query.setBuyerTaxNo(trimOrNull(query.getBuyerTaxNo()));
query.setInvoiceDateStart(normalizeQueryDate(query.getInvoiceDateStart()));
query.setInvoiceDateEnd(normalizeQueryDate(query.getInvoiceDateEnd()));
Page<KingdeeInvoicePoolVO> page = new Page<>(current, size);
return invoicePoolMapper.selectInvoicePoolPage(page, query, AuthUtil.getTenantId());
}
/** 空白串归一为 null,便于 XML 的 != '' 判空条件 */
private String trimOrNull(String text) {
return StringUtil.isBlank(text) ? null : text.trim();
}
/** 查询日期清洗:空转 null,格式非法直接报业务异常(自定义 SQL 无类型保护,先校验再进 XML) */
private String normalizeQueryDate(String text) {
if (StringUtil.isBlank(text)) {
return null;
}
try {
return LocalDate.parse(text.trim()).toString();
} catch (DateTimeParseException exception) {
throw new ServiceException("开票日期格式应为 yyyy-MM-dd");
}
}
@Override
public KingdeeInvoicePoolVO fetchKingdeeInvoiceByNo(String invoiceNo) {
if (StringUtil.isBlank(invoiceNo)) {
throw new ServiceException("请填写发票号码");
}
String trimmedInvoiceNo = invoiceNo.trim();
// 镜像优先回读:白名单只约束「入镜像」,回读镜像不受限;镜像已有票不再打远程,不受金蝶可用性影响
KingdeeInvoicePoolVO existingVo = invoicePoolMapper.selectInvoicePoolVO(trimmedInvoiceNo, AuthUtil.getTenantId());
if (existingVo != null) {
return existingVo;
}
// A801-1 无时间窗口约束,票池更新时间滑出增量窗口的老票也能精确命中;复用窄查后仍保留票号全等匹配的防御过滤
List<BfiInvoicePoolVO> poolRows = queryPoolByInvoiceNo(trimmedInvoiceNo);
BfiInvoicePoolVO poolRow = poolRows.stream()
.filter(row -> StringUtil.isNotBlank(row.getInvoiceNo())
&& trimmedInvoiceNo.equals(row.getInvoiceNo().trim()))
.findFirst()
.orElse(null);
if (poolRow == null) {
throw new ServiceException("金蝶票池未查询到发票 " + trimmedInvoiceNo);
}
if (filterByInvoiceTypes(List.of(poolRow)).isEmpty()) {
throw new ServiceException("发票 " + trimmedInvoiceNo + " 的票种不在同步白名单内,不能补查");
}
if (filterByArchiveTaxNos(customerTaxNoSet(), List.of(poolRow)).isEmpty()) {
throw new ServiceException("发票 " + trimmedInvoiceNo
+ " 购销双方存在未建档税号,请先在客商档案维护统一社会信用代码后再补查");
}
// upsert 自带票面指纹比对,无变化不重写,重复点击安全;随后从镜像回读统一视图(含认领/登记标记)
batchUpsertInvoicePool(List.of(poolRow));
KingdeeInvoicePoolVO vo = invoicePoolMapper.selectInvoicePoolVO(trimmedInvoiceNo, AuthUtil.getTenantId());
if (vo == null) {
throw new ServiceException("补查失败:镜像回读不到发票 " + trimmedInvoiceNo);
}
return vo;
}
@Override
@@ -752,6 +819,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
data.setPageNo(1);
data.setPageSize(POOL_PAGE_SIZE);
data.setInvoiceNo(invoiceNo.trim());
// 只认发票状态正常的票,作废/异常票不再进入镜像
data.setInvoiceStatus(POOL_INVOICE_STATUS_NORMAL);
BfiInvoicePoolQueryDTO param = new BfiInvoicePoolQueryDTO();
param.setQueryParam(data);
BfiPageVO<BfiInvoicePoolVO> page = bfiService.queryInvoicePool(param);
@@ -767,6 +836,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
data.setPageSize(POOL_PAGE_SIZE);
data.setUpdateTime(windowStart.format(POOL_TIME_FORMATTER));
data.setUpdateTimeEnd(windowEnd.format(POOL_TIME_FORMATTER));
// 只认发票状态正常的票,作废/异常票不再进入镜像
data.setInvoiceStatus(POOL_INVOICE_STATUS_NORMAL);
BfiInvoicePoolQueryDTO param = new BfiInvoicePoolQueryDTO();
param.setQueryParam(data);
BfiPageVO<BfiInvoicePoolVO> page = bfiService.queryInvoicePool(param);