Merge remote-tracking branch 'origin/master' into dev
This commit is contained in:
+2
@@ -27,12 +27,14 @@ package org.springblade.transport;
|
||||
|
||||
import org.springblade.core.cloud.client.BladeCloudApplication;
|
||||
import org.springblade.core.launch.BladeApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 运输服务启动器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@EnableScheduling
|
||||
@BladeCloudApplication
|
||||
public class TransportApplication {
|
||||
|
||||
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* 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.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springblade.thirdparty.bfi.constant.BfiConstant;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 业财融合(BFI)推送金蝶配置属性,统一收口 Nacos 配置 thirdParty.bfi.*:
|
||||
* <p>
|
||||
* srcSys 为客商、付款申请等各业务推送金蝶的公共参数(外部系统编号);
|
||||
* customer、payApply 为各业务专属参数;未配置时使用字段默认值。
|
||||
* <p>
|
||||
* prefix 须为全小写(thirdparty.bfi),Spring 宽松绑定可匹配 Nacos 中的 camelCase 键(thirdParty.payApply.srcSys 等)。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "thirdparty.bfi")
|
||||
public class BfiProperties {
|
||||
|
||||
/**
|
||||
* 数据来源系统编码(公共):客商、付款申请等推送金蝶时统一使用,默认 WLTMS(运输TMS)
|
||||
*/
|
||||
private String srcSys = BfiConstant.DATA_SOURCE_WLTMS;
|
||||
|
||||
/**
|
||||
* 付款申请推送配置(A103-3 付款申请保存)
|
||||
*/
|
||||
private PayApply payApply = new PayApply();
|
||||
|
||||
/**
|
||||
* 开票申请推送配置(A303-1 开票申请单新增 / A303-5 开票申请发票查询)
|
||||
*/
|
||||
private InvoiceApply invoiceApply = new InvoiceApply();
|
||||
|
||||
/**
|
||||
* 收票单推送配置(A303-2 收票单批量保存 / A302-6 收票指定应付 / A801-3 收票单查询回写)
|
||||
*/
|
||||
private ReceivedInvoice receivedInvoice = new ReceivedInvoice();
|
||||
|
||||
/**
|
||||
* 收款认领公告查询配置(A301-2)
|
||||
*/
|
||||
private ClaimAnnounce claimAnnounce = new ClaimAnnounce();
|
||||
|
||||
/**
|
||||
* 付款结果定时回写配置(A103-2/A103-4,总开关同时控制应收勾稽、开票结果回写)
|
||||
*/
|
||||
private PayResult payResult = new PayResult();
|
||||
|
||||
/**
|
||||
* 收票单结果定时回写开关(A801-3)
|
||||
*/
|
||||
private ReceivedInvoiceResult receivedInvoiceResult = new ReceivedInvoiceResult();
|
||||
|
||||
/**
|
||||
* 收款认领推送配置(A301-3)
|
||||
*/
|
||||
private ReceiptClaimPush receiptClaimPush = new ReceiptClaimPush();
|
||||
|
||||
/**
|
||||
* 全票池发票定时同步配置(A801-1)
|
||||
*/
|
||||
private InvoiceReceipt invoiceReceipt = new InvoiceReceipt();
|
||||
|
||||
/**
|
||||
* 客商金蝶数据定时同步配置(A902-1/A902-2)
|
||||
*/
|
||||
private Customer customer = new Customer();
|
||||
|
||||
/**
|
||||
* 金蝶黑名单查询配置
|
||||
*/
|
||||
private Blacklist blacklist = new Blacklist();
|
||||
|
||||
/**
|
||||
* 开票结果回写锁配置(A303-5)
|
||||
*/
|
||||
private InvoiceResult invoiceResult = new InvoiceResult();
|
||||
|
||||
/**
|
||||
* 应收单勾稽回写锁配置(A302-4)
|
||||
*/
|
||||
private ArResult arResult = new ArResult();
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定补偿配置
|
||||
*/
|
||||
private BillLock billLock = new BillLock();
|
||||
|
||||
/**
|
||||
* 审批通过自动同步金蝶配置(结算单/付款申请/开票申请/收票登记)
|
||||
*/
|
||||
private AutoSync autoSync = new AutoSync();
|
||||
|
||||
/**
|
||||
* 应付/应收/收票单分录物料映射配置(A102-2/A302-2/A303-2 共用):
|
||||
* 运输类按项目业务模式映射——国内运输按运单运输方式映射、国际运输映射国际货代服务;
|
||||
* 装卸大类及手动添加的非运费杂费映射装卸物料
|
||||
*/
|
||||
private Material material = new Material();
|
||||
|
||||
/**
|
||||
* 付款申请推送配置项
|
||||
*/
|
||||
@Data
|
||||
public static class PayApply {
|
||||
|
||||
/**
|
||||
* 单据类型编码:ap_payapply_BT_S 付款申请单、ap_payapply_oth_BT_S 其他付款单,默认其他付款单
|
||||
*/
|
||||
private String billtype = "ap_payapply_oth_BT_S";
|
||||
|
||||
/**
|
||||
* 汇率表编码,默认 01
|
||||
*/
|
||||
private String exratetable = "01";
|
||||
|
||||
/**
|
||||
* 外部单据类型,默认 0KzXs
|
||||
*/
|
||||
private String srcType = "0KzXs";
|
||||
}
|
||||
|
||||
/**
|
||||
* 开票申请推送配置项(A303-1 / A303-5):
|
||||
* 销方名称取申请单开票单位快照、销方税号取客商档案统一社会信用代码、税收分类编码取商品行开票内容快照,均不走配置
|
||||
*/
|
||||
@Data
|
||||
public static class InvoiceApply {
|
||||
|
||||
/**
|
||||
* 分录外部单据类型,默认 0KzXs(与付款申请对齐)
|
||||
*/
|
||||
private String srcType = "0KzXs";
|
||||
}
|
||||
|
||||
/**
|
||||
* 收票单推送配置项(A303-2 / A302-6 / A801-3):
|
||||
* 发票类型编码、税收分类编码、购销方税号地址银行均取票池镜像与明细,不走配置
|
||||
*/
|
||||
@Data
|
||||
public static class ReceivedInvoice {
|
||||
|
||||
/**
|
||||
* A303-2 分录物料编码:联调期临时值(金蝶测试档案),发票明细无金蝶物料概念,
|
||||
* 真实物料口径与金蝶对齐后替换(与应收/应付联调临时值同源)
|
||||
*/
|
||||
private String materialNumber = "SP0324";
|
||||
|
||||
/**
|
||||
* A801-3 推送幂等预检回看窗口天数(金蝶修改时间窗口,契约约束不超过 1 个月)
|
||||
*/
|
||||
private int queryWindowDays = 30;
|
||||
|
||||
/**
|
||||
* A801-3 定时回写轮询窗口天数(同样受金蝶 1 个月窗口约束)
|
||||
*/
|
||||
private int resultWindowDays = 30;
|
||||
|
||||
/**
|
||||
* A801-3 回写 single-flight 锁租约秒数,须大于最坏一轮耗时(批量远程查询 + 逐单回写)
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收款认领公告查询配置项(A301-2)
|
||||
*/
|
||||
@Data
|
||||
public static class ClaimAnnounce {
|
||||
|
||||
/**
|
||||
* 查询时间窗口(月数),默认 6(近半年)
|
||||
*/
|
||||
private int months = 6;
|
||||
|
||||
/**
|
||||
* 认领人(claimusers):配置后公告查询固定使用该值,不再按当前登录用户姓名查询
|
||||
*/
|
||||
private String claimusers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分录物料映射配置项(thirdparty.bfi.material.*):默认值与金蝶测试档案对齐,
|
||||
* 金蝶正式档案确认后直接在 Nacos 覆盖
|
||||
*/
|
||||
@Data
|
||||
public static class Material {
|
||||
|
||||
/**
|
||||
* 国内运输:运单运输方式 -> 金蝶物料编码(公路运输/铁路运输/水路运输/航空运输四种),
|
||||
* 运输方式未配置映射时直接拦截报错
|
||||
*/
|
||||
private Map<String, String> domestic = new LinkedHashMap<>() {{
|
||||
put("公路运输", "XDWL11010001");
|
||||
put("铁路运输", "XDWL11010002");
|
||||
put("水路运输", "XDWL11010003");
|
||||
put("航空运输", "Item-00001082");
|
||||
}};
|
||||
|
||||
/**
|
||||
* 国际运输业务模式物料编码(国际货运代理服务)
|
||||
*/
|
||||
private String international = "Item-00001081";
|
||||
|
||||
/**
|
||||
* 装卸大类、手动添加非运费杂费物料编码(装卸)
|
||||
*/
|
||||
private String loading = "XDWL11002004";
|
||||
}
|
||||
|
||||
/**
|
||||
* 付款结果回写配置项(A103-2/A103-4)
|
||||
*/
|
||||
@Data
|
||||
public static class PayResult {
|
||||
|
||||
/**
|
||||
* 回写任务开关,代码默认关闭(安全兜底),由 Nacos 按环境显式开启
|
||||
*/
|
||||
private boolean syncEnabled = false;
|
||||
|
||||
/**
|
||||
* 回写 single-flight 锁租约秒数,须大于最坏一轮耗时(分批远程查询 + 逐单回写)
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收票单结果回写配置项(A801-3)
|
||||
*/
|
||||
@Data
|
||||
public static class ReceivedInvoiceResult {
|
||||
|
||||
/**
|
||||
* 回写任务开关,默认关闭(联调稳定后开启)
|
||||
*/
|
||||
private boolean syncEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收款认领推送配置项(A301-3)
|
||||
*/
|
||||
@Data
|
||||
public static class ReceiptClaimPush {
|
||||
|
||||
/**
|
||||
* 推送任务开关,默认关闭(联调稳定后开启)
|
||||
*/
|
||||
private boolean syncEnabled = false;
|
||||
|
||||
/**
|
||||
* 推送 single-flight 锁租约秒数,须大于最坏一轮耗时(逐认领人远程查公告 + 逐条推送)
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全票池发票同步配置项(A801-1)
|
||||
*/
|
||||
@Data
|
||||
public static class InvoiceReceipt {
|
||||
|
||||
/**
|
||||
* 同步任务开关,默认关闭(上线先手动跑一次全窗口铺底)
|
||||
*/
|
||||
private boolean syncEnabled = false;
|
||||
|
||||
/**
|
||||
* 增量窗口天数(按票池更新时间滚动)
|
||||
*/
|
||||
private int incrementDays = 30;
|
||||
|
||||
/**
|
||||
* 同步发票种类白名单(逗号分隔票易通编码,如 26,27 数电普票/专票),为空不限制
|
||||
*/
|
||||
private String invoiceTypes = "";
|
||||
|
||||
/**
|
||||
* 同步 single-flight 锁租约秒数,须大于最坏一轮耗时;进程异常退出时锁在租约到期后自动释放
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客商金蝶数据同步配置项(A902-1/A902-2)
|
||||
*/
|
||||
@Data
|
||||
public static class Customer {
|
||||
|
||||
/**
|
||||
* 同步任务开关,默认关闭
|
||||
*/
|
||||
private boolean syncEnabled = false;
|
||||
|
||||
/**
|
||||
* 同步 single-flight 锁租约秒数,须大于最坏一轮耗时(复查 + 批量推送 + 停用联动)
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶黑名单查询配置项
|
||||
*/
|
||||
@Data
|
||||
public static class Blacklist {
|
||||
|
||||
/**
|
||||
* 客商提审命中风险警示类(B)时是否一并拦截;默认 false 仅放行告警,禁止交易类(A)始终拦截
|
||||
*/
|
||||
private boolean blockRiskWarning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开票结果回写锁配置项(A303-5)
|
||||
*/
|
||||
@Data
|
||||
public static class InvoiceResult {
|
||||
|
||||
/**
|
||||
* 回写 single-flight 锁租约秒数
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应收单勾稽回写锁配置项(A302-4)
|
||||
*/
|
||||
@Data
|
||||
public static class ArResult {
|
||||
|
||||
/**
|
||||
* 勾稽 single-flight 锁租约秒数
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定补偿配置项
|
||||
*/
|
||||
@Data
|
||||
public static class BillLock {
|
||||
|
||||
/**
|
||||
* 锁定/解锁总开关(thirdParty.bfi.billLock.enabled):推送后锁定 lockBusiData、
|
||||
* 金蝶解锁回调 unLockNotifyBusiData、锁定补偿重锁三处共用;
|
||||
* 默认开启保持既有锁定行为,Nacos 置 false 一键停用
|
||||
* (推送后不再锁单、解锁回调只应答不处理、补偿跳过),存量锁记录守卫(不可编辑校验)不受影响
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 锁定补偿开关,默认关闭(安全兜底)
|
||||
*/
|
||||
private boolean retryEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过自动同步金蝶配置项(结算单 A102-2/A302-2、付款申请 A103-1/A103-3、开票申请 A303-1、收票 A303-2+A302-6)
|
||||
*/
|
||||
@Data
|
||||
public static class AutoSync {
|
||||
|
||||
/**
|
||||
* 结算单审批后自动推送开关
|
||||
*/
|
||||
private boolean settlement = false;
|
||||
|
||||
/**
|
||||
* 付款申请审批后自动推送开关
|
||||
*/
|
||||
private boolean payment = false;
|
||||
|
||||
/**
|
||||
* 开票申请审批后自动推送开关
|
||||
*/
|
||||
private boolean invoiceApply = false;
|
||||
|
||||
/**
|
||||
* 收票登记审批后自动推送开关
|
||||
*/
|
||||
private boolean receipt = false;
|
||||
|
||||
/**
|
||||
* failed 单据定时重推开关(仅重推生成失败,不扫 unsynced 存量)
|
||||
*/
|
||||
private boolean retryEnabled = false;
|
||||
}
|
||||
|
||||
}
|
||||
+85
-4
@@ -5,28 +5,109 @@
|
||||
package org.springblade.transport.config;
|
||||
|
||||
import io.minio.MinioClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 凭证图片 MinIO 客户端配置。
|
||||
* <p>
|
||||
* 连接参数由 Nacos 的 file.storage.minio 配置提供。
|
||||
* 未配置 {@code file.storage.minio.endpoint} 时不注册该客户端,
|
||||
* 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动;
|
||||
* 此时凭证相关接口会在调用时给出明确提示,而非启动即失败。
|
||||
* <ul>
|
||||
* <li>{@code endpoint}:建议内网地址;若误配为 https://host/oss,会自动剥离 path 并用拦截器补回</li>
|
||||
* <li>{@code transform-endpoint}:外网地址(可带 /oss),用于浏览器预签名</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "file.storage.minio", name = "endpoint")
|
||||
public class VoucherMinioConfig {
|
||||
|
||||
private static final String DEFAULT_REGION = "us-east-1";
|
||||
|
||||
@Bean
|
||||
public MinioClient voucherMinioClient(
|
||||
@Value("${file.storage.minio.endpoint:${minio.endpoint:}}") String endpoint,
|
||||
@Value("${file.storage.minio.access-key-id:${minio.access-key:}}") String accessKey,
|
||||
@Value("${file.storage.minio.access-key-secret:${minio.secret-key:}}") String secretKey) {
|
||||
return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();
|
||||
String origin = resolveClientEndpoint(endpoint);
|
||||
String pathPrefix = resolvePathPrefix(endpoint);
|
||||
if (StringUtil.isNotBlank(pathPrefix)) {
|
||||
log.warn("MinIO endpoint 含 path,将按 origin={} pathPrefix={} 访问;建议 endpoint 使用内网地址",
|
||||
origin, pathPrefix);
|
||||
}
|
||||
return create(endpoint, accessKey, secretKey);
|
||||
}
|
||||
|
||||
public static MinioClient create(String endpoint, String accessKey, String secretKey) {
|
||||
String origin = resolveClientEndpoint(endpoint);
|
||||
String pathPrefix = resolvePathPrefix(endpoint);
|
||||
MinioClient.Builder builder = MinioClient.builder()
|
||||
.endpoint(origin)
|
||||
.credentials(accessKey, secretKey)
|
||||
.region(DEFAULT_REGION);
|
||||
if (StringUtil.isNotBlank(pathPrefix)) {
|
||||
builder.httpClient(buildHttpClient(pathPrefix), true);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* MinIO SDK 不允许 endpoint 带 path(例如 https://host/oss)。
|
||||
*/
|
||||
public static String resolveClientEndpoint(String endpoint) {
|
||||
if (StringUtil.isBlank(endpoint)) {
|
||||
throw new ServiceException("MinIO endpoint 不能为空");
|
||||
}
|
||||
URI uri = URI.create(endpoint.trim());
|
||||
if (StringUtil.isBlank(uri.getScheme()) || StringUtil.isBlank(uri.getHost())) {
|
||||
throw new ServiceException("MinIO endpoint 配置不正确: " + endpoint);
|
||||
}
|
||||
StringBuilder origin = new StringBuilder();
|
||||
origin.append(uri.getScheme()).append("://").append(uri.getHost());
|
||||
if (uri.getPort() > 0) {
|
||||
origin.append(':').append(uri.getPort());
|
||||
}
|
||||
return origin.toString();
|
||||
}
|
||||
|
||||
private static String resolvePathPrefix(String endpoint) {
|
||||
URI uri = URI.create(endpoint.trim());
|
||||
String path = uri.getPath();
|
||||
if (StringUtil.isBlank(path) || "/".equals(path)) {
|
||||
return "";
|
||||
}
|
||||
String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
|
||||
return normalized.startsWith("/") ? normalized : "/" + normalized;
|
||||
}
|
||||
|
||||
private static OkHttpClient buildHttpClient(String pathPrefix) {
|
||||
return new OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(chain -> {
|
||||
Request original = chain.request();
|
||||
HttpUrl originalUrl = original.url();
|
||||
String encodedPath = originalUrl.encodedPath();
|
||||
if (encodedPath.startsWith(pathPrefix + "/") || encodedPath.equals(pathPrefix)) {
|
||||
return chain.proceed(original);
|
||||
}
|
||||
HttpUrl newUrl = originalUrl.newBuilder()
|
||||
.encodedPath(pathPrefix + encodedPath)
|
||||
.build();
|
||||
return chain.proceed(original.newBuilder().url(newUrl).build());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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.constant;
|
||||
|
||||
/**
|
||||
* 客商档案金蝶(星瀚)协同常量
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class CustomerBfiConstants {
|
||||
|
||||
/**
|
||||
* 推送金蝶固定值:业务职能全选(1销售,2结算,3付款,4收货)
|
||||
*/
|
||||
public static final String BIZ_FUNCTION_ALL = "1,2,3,4";
|
||||
|
||||
/**
|
||||
* 客商类型:内部组织(customer_kind 字段值)
|
||||
*/
|
||||
public static final String CUSTOMER_KIND_INTERNAL = "internal";
|
||||
|
||||
/**
|
||||
* 客商类型:外部客商(customer_kind 字段值)
|
||||
*/
|
||||
public static final String CUSTOMER_KIND_EXTERNAL = "external";
|
||||
|
||||
/**
|
||||
* 金蝶伙伴类型编码有效范围上界(BfiCustomerSaveDTO.type 全集 1~17)
|
||||
*/
|
||||
public static final int PARTNER_TYPE_MAX = 17;
|
||||
|
||||
/**
|
||||
* 营业期限类型:长期(TMS 表单文本)
|
||||
*/
|
||||
public static final String BUSINESS_TERM_LONG = "长期";
|
||||
|
||||
private CustomerBfiConstants() {
|
||||
}
|
||||
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 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.constant;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
|
||||
/**
|
||||
* 金蝶附件同步(A902)常量
|
||||
* <p>entityNumber/controlKey 映射以常量维护,二期客商(资质附件)等金蝶确认后补充条目即可,不改架构</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class KingdeeAttachmentConstant {
|
||||
|
||||
// ==================== 业务单据类型(biz_type) ====================
|
||||
|
||||
/**
|
||||
* 付款申请单
|
||||
*/
|
||||
public static final String BIZ_PAY_APPLICATION = "pay_application";
|
||||
/**
|
||||
* 正式结算-财务应付单面板
|
||||
*/
|
||||
public static final String BIZ_FORMAL_SETTLEMENT_AP = "formal_settlement_ap";
|
||||
/**
|
||||
* 正式结算-财务应收单面板
|
||||
*/
|
||||
public static final String BIZ_FORMAL_SETTLEMENT_AR = "formal_settlement_ar";
|
||||
/**
|
||||
* 收票单
|
||||
*/
|
||||
public static final String BIZ_INVOICE_RECEIPT = "invoice_receipt";
|
||||
/**
|
||||
* 开票申请单
|
||||
*/
|
||||
public static final String BIZ_INVOICE_APPLICATION = "invoice_application";
|
||||
|
||||
// ==================== 星瀚 entityNumber(A902 定位单据类型) ====================
|
||||
|
||||
/**
|
||||
* 付款申请单
|
||||
*/
|
||||
public static final String ENTITY_PAY_APPLICATION = "ap_payapply";
|
||||
/**
|
||||
* 财务应付单(正式结算应付面板)
|
||||
*/
|
||||
public static final String ENTITY_AP_BILL = "ap_finapbill";
|
||||
/**
|
||||
* 财务应收单(正式结算应收面板)
|
||||
*/
|
||||
public static final String ENTITY_AR_BILL = "ar_finarbill";
|
||||
/**
|
||||
* 收票单
|
||||
*/
|
||||
public static final String ENTITY_INVOICE = "ap_invoice";
|
||||
/**
|
||||
* 开票申请单
|
||||
*/
|
||||
public static final String ENTITY_INVOICE_APPLICATION = "sim_original_bill";
|
||||
|
||||
/**
|
||||
* 业务单据类型 -> 星瀚 entityNumber 映射;
|
||||
* 客商(资质附件)二期接入:待金蝶确认后补充专用 biz_type 与 entityNumber 条目
|
||||
*/
|
||||
public static final Map<String, String> ENTITY_NUMBERS = Map.of(
|
||||
BIZ_PAY_APPLICATION, ENTITY_PAY_APPLICATION,
|
||||
BIZ_FORMAL_SETTLEMENT_AP, ENTITY_AP_BILL,
|
||||
BIZ_FORMAL_SETTLEMENT_AR, ENTITY_AR_BILL,
|
||||
BIZ_INVOICE_RECEIPT, ENTITY_INVOICE,
|
||||
BIZ_INVOICE_APPLICATION, ENTITY_INVOICE_APPLICATION
|
||||
);
|
||||
|
||||
/**
|
||||
* 星瀚附件面板/附件字段标识(各单据统一)
|
||||
*/
|
||||
public static final String CONTROL_KEY = "attachmentpanel";
|
||||
|
||||
// ==================== 限制 ====================
|
||||
|
||||
/**
|
||||
* 单文件软限制 20MB(待金蝶确认后调整);超限记失败原因跳过,不阻塞同单其余附件
|
||||
*/
|
||||
public static final long MAX_FILE_SIZE_BYTES = 20L * 1024 * 1024;
|
||||
|
||||
// ==================== 同步行状态(sync_status) ====================
|
||||
|
||||
/**
|
||||
* 同步失败
|
||||
*/
|
||||
public static final int SYNC_STATUS_FAILED = 0;
|
||||
/**
|
||||
* 同步成功(终态,幂等跳过)
|
||||
*/
|
||||
public static final int SYNC_STATUS_SUCCESS = 1;
|
||||
|
||||
// ==================== 附件同步聚合态(列表展示,运行时聚合非存储字段) ====================
|
||||
|
||||
/**
|
||||
* 无附件:单据 attachmentsJson 无可上传附件
|
||||
*/
|
||||
public static final String AGGREGATE_NONE = "none";
|
||||
/**
|
||||
* 未上传:有附件且成功数为 0
|
||||
*/
|
||||
public static final String AGGREGATE_PENDING = "pending";
|
||||
/**
|
||||
* 部分未传:有附件且部分上传成功
|
||||
*/
|
||||
public static final String AGGREGATE_PARTIAL = "partial";
|
||||
/**
|
||||
* 已上传:有附件且全部上传成功
|
||||
*/
|
||||
public static final String AGGREGATE_DONE = "done";
|
||||
|
||||
/**
|
||||
* 聚合态 -> 展示名称
|
||||
*/
|
||||
public static final Map<String, String> AGGREGATE_NAMES = Map.of(
|
||||
AGGREGATE_NONE, "无附件",
|
||||
AGGREGATE_PENDING, "未上传",
|
||||
AGGREGATE_PARTIAL, "部分未传",
|
||||
AGGREGATE_DONE, "已上传"
|
||||
);
|
||||
|
||||
/**
|
||||
* 业务单据类型对应星瀚 entityNumber
|
||||
*
|
||||
* @param bizType 业务单据类型
|
||||
* @return entityNumber,未配置时抛业务异常(entityNumber 是硬约束,猜错即传不上)
|
||||
*/
|
||||
public static String entityNumber(String bizType) {
|
||||
String entityNumber = ENTITY_NUMBERS.get(bizType);
|
||||
if (entityNumber == null) {
|
||||
throw new ServiceException("金蝶附件上传失败:业务单据类型[" + bizType + "]未配置 entityNumber 映射");
|
||||
}
|
||||
return entityNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚合态展示名称(未知态兜底返回原文)
|
||||
*
|
||||
* @param aggregateStatus 聚合态
|
||||
* @return 展示名称
|
||||
*/
|
||||
public static String aggregateName(String aggregateStatus) {
|
||||
return AGGREGATE_NAMES.getOrDefault(aggregateStatus, aggregateStatus);
|
||||
}
|
||||
|
||||
private KingdeeAttachmentConstant() {
|
||||
}
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* 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.constant;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 付款同步金蝶(星瀚)固定值。
|
||||
* 金蝶侧档案编码暂以常量维护,待金蝶侧确认取值后直接修改常量重新发包。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class KingdeeBfiConstant {
|
||||
|
||||
/** 往来类型:供应商(付款申请与财务应付单共用)。 */
|
||||
public static final String BFI_SUPPLIER_ASSTACT_TYPE = "bd_supplier";
|
||||
|
||||
/** 往来类型:客户(财务应付单按合同类别为客户合同时使用)。 */
|
||||
public static final String BFI_CUSTOMER_ASSTACT_TYPE = "bd_customer";
|
||||
|
||||
/** 来源单据类型:上游合同(有合同时传 awrp_src_bt)。 */
|
||||
public static final String BFI_CONTRACT_SRC_BT = "awrp_contract_info";
|
||||
|
||||
/** 无项目时传的项目编码占位值。 */
|
||||
public static final String BFI_PROJECT_NUMBER_NONE = "999";
|
||||
|
||||
/** 联调默认物料编码:金蝶测试档案值,真实货物类型档案编码与金蝶对齐后替换。 */
|
||||
public static final String BFI_AP_BILL_MATERIAL_NUMBER_TEST = "SP0324";
|
||||
|
||||
/** 联调默认计量单位编码:金蝶测试档案值,档案维护计量单位编码后替换。 */
|
||||
public static final String BFI_AP_BILL_MEASUREUNIT_NUMBER_TEST = "TON";
|
||||
/** 联调默认银行行号:金蝶测试档案值,档案维护银行行号后替换。 */
|
||||
public static final String BFI_AP_BILL_BANKACCOUNT_NUMBER_TEST = "001110012009";
|
||||
|
||||
/** 联调默认项目编码:金蝶测试档案值,项目档案与金蝶对齐后替换。 */
|
||||
public static final String BFI_AP_BILL_PROJECT_NUMBER_TEST = "999";
|
||||
|
||||
/** 付款类型编码:正式付款(结算付款)。 */
|
||||
public static final String BFI_PAYMENT_TYPE_FORMAL = "201";
|
||||
|
||||
/** 付款类型编码:预付款。 */
|
||||
public static final String BFI_PAYMENT_TYPE_PREPAY = "202";
|
||||
|
||||
/** A102-2 默认单据类型名称:服务采购应付(经 AP_BILL_BILLTYPE_MAP 转换为编码)。待确认:具体类型需金蝶侧最终确认,确认后修改此名称即可。 */
|
||||
public static final String BFI_AP_BILL_BILLTYPE_NAME = "服务采购应付";
|
||||
|
||||
/** A102-2 款项性质编码(暂按 2001 材料采购)。待确认:编码需金蝶侧最终确认,确认后如有调整直接改此常量。 */
|
||||
public static final String BFI_AP_BILL_PAY_PROPERTY = "2001";
|
||||
|
||||
/**
|
||||
* A102-2 付款方式(赊购 CREDIT,允许金蝶侧后续调整付款金额;
|
||||
*/
|
||||
public static final String BFI_AP_BILL_PUR_MODE_CREDIT = "CREDIT";
|
||||
/**
|
||||
* A102-2 付款方式 现购 CASH 会由金蝶自动 100% 推付款)。
|
||||
*/
|
||||
public static final String BFI_AP_BILL_PUR_MODE_CASH = "CASH";
|
||||
|
||||
/** A102-2 必填:外部单据类型(应付单来源标识)。待确认:取值需金蝶侧确认,确认后如有调整直接改此常量。 */
|
||||
public static final String BFI_AP_BILL_SRC_TYPE = "TMS财务应付单";
|
||||
|
||||
/** 财务应付单(ap_finapbill)单据类型映射:中文名称 -> 星瀚编码。 */
|
||||
public static final Map<String, String> AP_BILL_BILLTYPE_MAP = Map.ofEntries(
|
||||
Map.entry("借贷项调整", "ApFin_borr_BT_S"),
|
||||
Map.entry("费用采购应付", "ApFin_fee_BT_S"),
|
||||
Map.entry("其他应付", "ApFin_other_BT_S"),
|
||||
Map.entry("产品委外应付", "ApFin_product_BT_S"),
|
||||
Map.entry("标准采购应付", "ApFin_pur_BT_S"),
|
||||
Map.entry("采购费用应付", "ApFin_purfee_BT_S"),
|
||||
Map.entry("销售费用应付", "ApFin_salefee_BT_S"),
|
||||
Map.entry("服务采购应付", "ApFin_service_BT_S"),
|
||||
Map.entry("资产采购应付", "ap_finapbill_asset_BT_S")
|
||||
);
|
||||
|
||||
/** 财务应收单(ar_finarbill)单据类型映射:中文名称 -> 星瀚编码(当前付款链路未启用,应收结算单推送规则待业务确认)。 */
|
||||
public static final Map<String, String> AR_BILL_BILLTYPE_MAP = Map.ofEntries(
|
||||
Map.entry("借贷项调整", "arfin_borrowar_BT_S"),
|
||||
Map.entry("其他应收", "arfin_other_BT_S"),
|
||||
Map.entry("销售费用应收", "arfin_salefee_BT_S"),
|
||||
Map.entry("服务销售应收", "arfin_sersal_BT_S"),
|
||||
Map.entry("标准销售应收", "arfin_standard_BT_S")
|
||||
);
|
||||
|
||||
/** A302-2 默认单据类型名称:服务销售应收(经 AR_BILL_BILLTYPE_MAP 转换为编码)。待确认:具体类型需金蝶侧最终确认,确认后修改此名称即可。 */
|
||||
public static final String BFI_AR_BILL_BILLTYPE_NAME = "服务销售应收";
|
||||
|
||||
/** A302-2 款项性质编码(暂按 1001 一般销售,与应付 2001 材料采购区分)。待确认:编码需金蝶侧最终确认,确认后如有调整直接改此常量。 */
|
||||
public static final String BFI_AR_BILL_PAY_PROPERTY = "1001";
|
||||
|
||||
/** A302-2 付款方式:现销 CASH(与应付现购对称,由金蝶自动推收款)。待确认:取值需金蝶侧确认。 */
|
||||
public static final String BFI_AR_BILL_PAY_MODE_CASH = "CASH";
|
||||
|
||||
/** A302-2 必填:外部单据类型(应收单来源标识)。待确认:取值需金蝶侧确认,确认后如有调整直接改此常量。 */
|
||||
public static final String BFI_AR_BILL_SRC_TYPE = "TMS财务应收单";
|
||||
|
||||
/** A303-1 上游来源单据类型:财务应收单(awrp_src_bt)。 */
|
||||
public static final String BFI_AR_BILL_SRC_BT = "ar_finarbill";
|
||||
|
||||
/** A301-3 认领推送核心单据类型:其他应付(e_corebilltype)。 */
|
||||
public static final String BFI_CLAIM_CORE_BILL_TYPE = "ar_finarbill";
|
||||
|
||||
/** A303-1 发票种类:数电发票(普通发票)。 */
|
||||
public static final String BFI_INVOICE_TYPE_DIGITAL_GENERAL = "10xdp";
|
||||
|
||||
/** A303-1 发票种类:数电发票(增值税专用发票)。 */
|
||||
public static final String BFI_INVOICE_TYPE_DIGITAL_SPECIAL = "08xdp";
|
||||
|
||||
/** A303-1 特殊票种:货物运输(运费/货物运输类发票)。 */
|
||||
public static final String BFI_INVOICE_SPECIAL_TYPE_FREIGHT = "E04";
|
||||
|
||||
/** A303-1 特殊票种:非特殊票种(其余费用类发票,如其他、非货物运输的运输类)。 */
|
||||
public static final String BFI_INVOICE_SPECIAL_TYPE_NONE = "00";
|
||||
|
||||
/** A303-1 特殊票种货物运输判定关键字:命中商品名称或商品和服务分类简称即视为货物运输类。 */
|
||||
public static final List<String> BFI_INVOICE_FREIGHT_KEYWORDS = List.of("货物运输", "运费");
|
||||
|
||||
/** A303-1 货物运输单据体运输工具种类合法值:字典 transport_type 翻译名称命中则直送,否则兜底其它运输工具。 */
|
||||
public static final List<String> BFI_FREIGHT_TRANSPORT_TYPES = List.of("铁路运输", "公路运输", "水路运输", "航空运输", "管道运输", "其它运输工具");
|
||||
|
||||
/** A303-1 货物运输单据体运输工具种类兜底值。 */
|
||||
public static final String BFI_FREIGHT_TRANSPORT_TYPE_OTHER = "其它运输工具";
|
||||
|
||||
/** 金蝶同步状态:开票完成(A303-5 查到发票,终态,开票申请专用)。 */
|
||||
public static final String KINGDEE_STATUS_INVOICED = "invoiced";
|
||||
|
||||
/** 付款申请单(ap_payapply)单据类型映射:中文名称 -> 星瀚编码(使用规则待业务确认)。 */
|
||||
public static final Map<String, String> PAY_APPLY_BILLTYPE_MAP = Map.of(
|
||||
"采购付款申请", "ap_payapply_BT_S",
|
||||
"其他付款申请", "ap_payapply_oth_BT_S"
|
||||
);
|
||||
|
||||
/** 金蝶同步状态:未生成(新建单据默认)。 */
|
||||
public static final String KINGDEE_STATUS_UNSYNCED = "unsynced";
|
||||
|
||||
/** 金蝶同步状态:已生成(推送成功,等待付款)。 */
|
||||
public static final String KINGDEE_STATUS_SYNCED = "synced";
|
||||
|
||||
/** 金蝶同步状态:付款中(A103-2 paystatus = Inpayment 部分付款)。 */
|
||||
public static final String KINGDEE_STATUS_PAYING = "paying";
|
||||
|
||||
/** 金蝶同步状态:已付款(A103-2 paystatus = Alreadypay,终态)。 */
|
||||
public static final String KINGDEE_STATUS_PAID = "paid";
|
||||
|
||||
/** 金蝶同步状态:已关闭(A103-2 billstatus = D,终态)。 */
|
||||
public static final String KINGDEE_STATUS_CLOSED = "closed";
|
||||
|
||||
/** 金蝶同步状态:生成失败(推送异常/金蝶返回失败,可重推)。 */
|
||||
public static final String KINGDEE_STATUS_FAILED = "failed";
|
||||
|
||||
/** 收票单推送状态:未推送(新建单据默认,票池绑定层的 kingdee_status 与此无关)。 */
|
||||
public static final String RECEIVED_PUSH_UNSYNCED = "unsynced";
|
||||
|
||||
/** 收票单推送状态:金蝶收票单已保存(A303-2 成功,等待/已完成指定应付)。 */
|
||||
public static final String RECEIVED_PUSH_PUSHED = "pushed";
|
||||
|
||||
/** 收票单推送状态:已指定应付(A302-6 成功,一票一应付,终态;金蝶侧异常回收获占)。 */
|
||||
public static final String RECEIVED_PUSH_ASSIGNED = "assigned";
|
||||
|
||||
/** 收票单推送状态:推送失败(保存/指定异常或金蝶侧删除红冲,人工处理后重推)。 */
|
||||
public static final String RECEIVED_PUSH_FAILED = "failed";
|
||||
|
||||
/** A303-2 业务类型(PUR:采购 / FEE:费用,TMS 服务费进项票暂按采购)。待确认:取值需金蝶侧最终确认。 */
|
||||
public static final String BFI_RECEIVED_BIZ_TYPE = "PUR";
|
||||
|
||||
/** A303-2 必填:外部单据类型(收票单来源标识)。待确认:取值需金蝶侧确认,确认后如有调整直接改此常量。 */
|
||||
public static final String BFI_RECEIVED_INVOICE_SRC_TYPE = "TMS收票登记";
|
||||
|
||||
/** A103-2 付款状态:已付款。 */
|
||||
public static final String BFI_PAY_STATUS_ALREADY_PAY = "Alreadypay";
|
||||
|
||||
/** A103-2 付款状态:部分付款。 */
|
||||
public static final String BFI_PAY_STATUS_IN_PAYMENT = "Inpayment";
|
||||
|
||||
/** A103-2 付款状态:未付款。 */
|
||||
public static final String BFI_PAY_STATUS_UNPAID = "Unpaid";
|
||||
|
||||
/** A103-2 单据状态:已关闭。 */
|
||||
public static final String BFI_BILL_STATUS_CLOSED = "D";
|
||||
|
||||
/** A801-1 查询限定发票状态:0 正常(票易通字典),作废/异常票不进镜像、不可补查 */
|
||||
public static final String POOL_INVOICE_STATUS_NORMAL = "0";
|
||||
|
||||
/** A301-3 认领推送:收款类型.编码(示例 101,待金蝶确认后如有调整直接改此常量)。 */
|
||||
public static final String BFI_CLAIM_REC_PAY_TYPE = "999";
|
||||
|
||||
/** A301-3 认领推送:付款人类型(默认客户 bd_customer,待金蝶确认后如有调整直接改此常量)。 */
|
||||
public static final String BFI_CLAIM_PAYMENT_TYPE = "bd_customer";
|
||||
|
||||
/** 收款流水推送状态:未推送(未满足推送条件或尚未轮到推送)。 */
|
||||
public static final String RECEIPT_CLAIM_PUSH_NONE = "none";
|
||||
|
||||
/** 收款流水推送状态:已推送(终态闸门,不允许二次推送)。 */
|
||||
public static final String RECEIPT_CLAIM_PUSH_PUSHED = "pushed";
|
||||
|
||||
/** 收款流水推送状态:推送失败(本地认领保留,可重新推送,重试复用同一来源编码)。 */
|
||||
public static final String RECEIPT_CLAIM_PUSH_FAILED = "failed";
|
||||
|
||||
/** 收款流水金蝶回写状态:未推送(认领完成推送前的初始状态)。 */
|
||||
public static final String RECEIPT_CLAIM_BILL_NONE = "none";
|
||||
|
||||
/** 金蝶同步状态:认领结果已推送(A301-3 成功,等待金蝶收款单审核/收款,可继续回写)。 */
|
||||
public static final String KINGDEE_STATUS_PUSHED = "pushed";
|
||||
|
||||
/** 金蝶同步状态:收款单已收款(A301-1 billstatus = D,终态)。 */
|
||||
public static final String KINGDEE_STATUS_RECEIVED = "received";
|
||||
|
||||
/** 选票金额使用状态可选值:仅保留剩余可用金额>0 且金额使用状态为 0 的可用票,2-全部使用 */
|
||||
public static final String POOL_AMOUNT_USE_STATUS_SELECTABLE = "0";
|
||||
|
||||
/** 单请求远程补页调用上限:金额可用过滤在拉取后进行,过滤率过低时以此封顶防请求无限拉长 */
|
||||
public static final int POOL_MAX_REMOTE_CALLS = 3;
|
||||
|
||||
/** 选票单据使用状态固定为未用(1,票易通字典):仅未使用的票可被认领/登记,前端不提供该筛选条件 */
|
||||
public static final String POOL_EXPENSE_STATUS_UNUSED = "1";
|
||||
|
||||
/**
|
||||
* 按中文名称从映射字典转换单据类型编码。
|
||||
*
|
||||
* @param billtypeMap 单据类型映射字典
|
||||
* @param name 单据类型中文名称
|
||||
* @return 星瀚单据类型编码,未维护时返回 null
|
||||
*/
|
||||
public static String billtypeBy(Map<String, String> billtypeMap, String name) {
|
||||
return billtypeMap.get(name);
|
||||
}
|
||||
|
||||
private KingdeeBfiConstant() {
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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.constant;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定/解锁(lockBusiData / unLockNotifyBusiData)固定值。
|
||||
* <p>lockBusiData 由 WLTMS 调金蝶(源系统 WLTMS、目标系统 XH),
|
||||
* unLockNotifyBusiData 由金蝶调 WLTMS(源系统 XH、目标系统 WLTMS);
|
||||
* WLTMS 单据类型自定义,金蝶目标单据类型为固定值</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class KingdeeLockConstant {
|
||||
|
||||
/** 锁动作:锁定 */
|
||||
public static final String ACTION_LOCK = "LOCK";
|
||||
|
||||
/** 锁动作:解锁 */
|
||||
public static final String ACTION_UNLOCK = "UNLOCK";
|
||||
|
||||
/** 来源操作类型:单据新增 */
|
||||
public static final String OPERATION_SAVE = "SAVE";
|
||||
|
||||
/** 来源操作类型:单据删除(解锁回调携带 DELETE 时 WLTMS 废除源单并还原关联数据) */
|
||||
public static final String OPERATION_DELETE = "DELETE";
|
||||
|
||||
/** 系统标识:WLTMS(运输管理系统,即本系统) */
|
||||
public static final String SYSTEM_WLTMS = "WLTMS";
|
||||
|
||||
/** 系统标识:金蝶星瀚 XH */
|
||||
public static final String SYSTEM_XH = "XH";
|
||||
|
||||
/** 锁状态:锁定失败(补偿定时任务的重试对象) */
|
||||
public static final String STATUS_FAILED = "failed";
|
||||
|
||||
/** 锁状态:金蝶锁定中(源单冻结,不可编辑/作废) */
|
||||
public static final String STATUS_LOCKED = "locked";
|
||||
|
||||
/** 锁状态:已解锁(终态;同单重推生成新锁记录) */
|
||||
public static final String STATUS_UNLOCKED = "unlocked";
|
||||
|
||||
/** WLTMS 单据类型:正式结算单(应付/应收共用,按结算单 settlementType 区分金蝶目标类型) */
|
||||
public static final String SOURCE_SETTLEMENT = "SETTLEMENT";
|
||||
|
||||
/** WLTMS 单据类型:开票申请单 */
|
||||
public static final String SOURCE_INVOICE_APPLY = "INVOICE_APPLY";
|
||||
|
||||
/** WLTMS 单据类型:付款申请单(映射预留,本期不锁) */
|
||||
public static final String SOURCE_PAY_APPLY = "PAY_APPLY";
|
||||
|
||||
/** WLTMS 单据类型:收票单(映射预留,本期不锁) */
|
||||
public static final String SOURCE_RECEIVED_INVOICE = "RECEIVED_INVOICE";
|
||||
|
||||
/** 金蝶目标单据类型:财务应付单(A102-2,应付结算单推送后锁定) */
|
||||
public static final String TARGET_AP_BILL = "ApFin_other_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:财务应收单(A302-2,应收结算单推送后锁定) */
|
||||
public static final String TARGET_AR_BILL = "arfin_other_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:开票单(A303-1 开票申请单,按发票张次逐张一行锁记录) */
|
||||
public static final String TARGET_INVOICE_BILL = "cdm_paybill_bank_elect_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:付款申请单(映射预留,本期不锁) */
|
||||
public static final String TARGET_PAY_APPLY = "ap_payapply_oth_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:收票单(映射预留,本期不锁) */
|
||||
public static final String TARGET_RECEIVED_INVOICE = "cdm_recbill_bank_BT_S";
|
||||
|
||||
/** 锁定重试上限:补偿任务超过该次数不再自动重试,人工排查(失败原因见 fail_reason) */
|
||||
public static final int RETRY_MAX_COUNT = 5;
|
||||
|
||||
/** 金蝶目标单据类型 -> WLTMS 单据类型反查映射(解锁回调按 targetBillType 分发废除还原用) */
|
||||
public static final Map<String, String> TARGET_TO_SOURCE_MAP = Map.ofEntries(
|
||||
Map.entry(TARGET_AP_BILL, SOURCE_SETTLEMENT),
|
||||
Map.entry(TARGET_AR_BILL, SOURCE_SETTLEMENT),
|
||||
Map.entry(TARGET_INVOICE_BILL, SOURCE_INVOICE_APPLY),
|
||||
Map.entry(TARGET_PAY_APPLY, SOURCE_PAY_APPLY),
|
||||
Map.entry(TARGET_RECEIVED_INVOICE, SOURCE_RECEIVED_INVOICE)
|
||||
);
|
||||
|
||||
private KingdeeLockConstant() {
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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.constant;
|
||||
|
||||
/**
|
||||
* 运输业务固定值。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
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";
|
||||
|
||||
/** 审批状态:审批中(审核节点,允许通过/驳回)。 */
|
||||
public static final String REVIEWING = "reviewing";
|
||||
|
||||
/** 审批状态:审批通过。 */
|
||||
public static final String APPROVED = "approved";
|
||||
|
||||
/** 审批状态:已驳回(允许再编辑后重新提交)。 */
|
||||
public static final String RETURNED = "returned";
|
||||
|
||||
/** 审批状态:已作废(终态)。 */
|
||||
public static final String VOIDED = "voided";
|
||||
|
||||
private TransportBusiConstant() {
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiCustomerVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 业财融合(BFI)客商查询控制器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("/bfi")
|
||||
@Tag(name = "BFI客商", description = "业财融合客商查询接口")
|
||||
public class BfiCustomerController extends BladeController {
|
||||
|
||||
private final IBfiService bfiService;
|
||||
|
||||
@GetMapping("/customer/query-by-credit-code")
|
||||
@Operation(summary = "按统一社会信用代码查询金蝶客商", description = "新增客商时查询金蝶是否已有该客商,已审核则回填工商信息")
|
||||
public R<BfiCustomerVO> queryByCreditCode(
|
||||
@Parameter(description = "统一社会信用代码", required = true) @RequestParam String creditCode) {
|
||||
if (StringUtil.isBlank(creditCode)) {
|
||||
return R.fail("统一社会信用代码不能为空");
|
||||
}
|
||||
BfiCustomerQueryDTO param = new BfiCustomerQueryDTO();
|
||||
param.setPageNo(1);
|
||||
param.setPageSize(1);
|
||||
param.setSocietycreditcode(List.of(creditCode.trim().toUpperCase()));
|
||||
BfiPageVO<BfiCustomerVO> page = bfiService.queryCustomer(param);
|
||||
List<BfiCustomerVO> rows = page == null ? null : page.getRows();
|
||||
return R.data(rows == null || rows.isEmpty() ? null : rows.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiExchangeRateVO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 业财融合(BFI)汇率查询控制器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("/bfi")
|
||||
@Tag(name = "BFI汇率", description = "业财融合汇率查询接口")
|
||||
public class BfiExchangeRateController extends BladeController {
|
||||
|
||||
private final IBfiService bfiService;
|
||||
|
||||
@GetMapping("/exchange-rate")
|
||||
@Operation(summary = "BFI汇率查询", description = "根据结算币种编码和汇率日期查询BFI汇率")
|
||||
public R<BfiExchangeRateVO> getBfiExchangeRate(
|
||||
@RequestParam String currencyCode,
|
||||
@RequestParam(required = false) String effectdate) {
|
||||
LocalDate date = StringUtil.isBlank(effectdate) ? LocalDate.now() : LocalDate.parse(effectdate);
|
||||
List<BfiExchangeRateVO> rates = bfiService.queryExchangeRate(date, date);
|
||||
if (rates == null || rates.isEmpty()) {
|
||||
return R.fail("未查询到该日期的BFI汇率数据");
|
||||
}
|
||||
BfiExchangeRateVO matched = rates.stream()
|
||||
.filter(r -> currencyCode.equalsIgnoreCase(r.getOrgcurNumber()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (matched == null) {
|
||||
return R.fail("未查询到币种 " + currencyCode + " 的汇率数据");
|
||||
}
|
||||
return R.data(matched);
|
||||
}
|
||||
}
|
||||
+26
-2
@@ -31,6 +31,7 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.excel.util.ExcelUtil;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
@@ -61,6 +62,7 @@ import java.util.List;
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@PreAuth(menu = "customer_archive")
|
||||
@@ -134,13 +136,25 @@ public class CustomerArchiveController extends BladeController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过
|
||||
* 审核通过(通过后自动推送金蝶,推送失败不影响审批结果,失败原因以警告文案返回)
|
||||
*/
|
||||
@PostMapping("/approve")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "审核通过", description = "传入id")
|
||||
public R approve(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.status(customerArchiveService.approve(id));
|
||||
boolean result = customerArchiveService.approve(id);
|
||||
if (!result) {
|
||||
return R.status(false);
|
||||
}
|
||||
try {
|
||||
customerArchiveService.syncKingdee(id);
|
||||
return R.success("审核通过");
|
||||
} catch (Exception exception) {
|
||||
log.error("客商审批通过后推送金蝶异常,id={}", id, exception);
|
||||
String reason = exception.getMessage() == null ? "金蝶接口调用异常" : exception.getMessage();
|
||||
// 与结算/付款/开票审批口径一致:审批已生效,推送失败原因以 data=false 警告文案返回
|
||||
return R.data(Boolean.FALSE, "审核通过,但推送金蝶失败:" + reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,6 +188,16 @@ public class CustomerArchiveController extends BladeController {
|
||||
return R.status(customerArchiveService.removeDraft(ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送金蝶(审批通过后失败重推入口)
|
||||
*/
|
||||
@PostMapping("/sync-kingdee")
|
||||
@ApiOperationSupport(order = 12)
|
||||
@Operation(summary = "推送金蝶", description = "传入id,仅审批通过的客商可推送,已推送/金蝶已有自动幂等跳过")
|
||||
public R<CustomerArchive> syncKingdee(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.data(customerArchiveService.syncKingdee(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 评分明细模板
|
||||
*/
|
||||
|
||||
+41
-3
@@ -22,17 +22,22 @@ import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
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.FormalSettlementStatusRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementInvoiceClaimRequest;
|
||||
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.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.IKingdeeBillLockService;
|
||||
import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.IReceiptFlowService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -59,8 +64,10 @@ import java.math.RoundingMode;
|
||||
@Tag(name = "正式结算单", description = "正式结算单管理")
|
||||
public class FormalSettlementController extends BladeController {
|
||||
private final IFormalSettlementService formalSettlementService;
|
||||
private final IKingdeeBillLockService kingdeeBillLockService;
|
||||
private final IPreSettlementService preSettlementService;
|
||||
private final IReceiptFlowService receiptFlowService;
|
||||
private final IInvoiceReceiptService invoiceReceiptService;
|
||||
|
||||
@GetMapping("/list")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@@ -190,7 +197,19 @@ public class FormalSettlementController extends BladeController {
|
||||
@PostMapping("/approve")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "审批通过")
|
||||
public R approve(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.approve(request); return R.success("审批通过"); }
|
||||
public R approve(@RequestBody FormalSettlementStatusRequest request) {
|
||||
formalSettlementService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
|
||||
@PostMapping("/return")
|
||||
@ApiOperationSupport(order = 12)
|
||||
@@ -204,11 +223,23 @@ public class FormalSettlementController extends BladeController {
|
||||
|
||||
@PostMapping("/sync-kingdee")
|
||||
@ApiOperationSupport(order = 14)
|
||||
@Operation(summary = "推送金蝶应付单")
|
||||
@Operation(summary = "推送金蝶(应付单/应收单)")
|
||||
public R<String> syncKingdee(@RequestParam Long id) { return R.data(formalSettlementService.syncKingdee(id)); }
|
||||
|
||||
@GetMapping("/detail-fees")
|
||||
@PostMapping("/sync-kingdee-ar-result")
|
||||
@ApiOperationSupport(order = 15)
|
||||
@Operation(summary = "手动勾稽金蝶应收单(A302-4 回写纠偏)")
|
||||
public R<String> syncKingdeeArResult() { return R.data(formalSettlementService.syncKingdeeArResult()); }
|
||||
|
||||
@PostMapping("/retry-kingdee-bill-lock")
|
||||
@ApiOperationSupport(order = 17)
|
||||
@Operation(summary = "手动补偿金蝶单据锁定(扫锁定失败锁记录重调lockBusiData,应付/应收/开票通用)")
|
||||
public R<String> retryKingdeeBillLock() {
|
||||
return R.data(kingdeeBillLockService.retryFailedLocks());
|
||||
}
|
||||
|
||||
@GetMapping("/detail-fees")
|
||||
@ApiOperationSupport(order = 16)
|
||||
@Operation(summary = "正式结算货物费用快照")
|
||||
public R<List<FormalSettlementDetailFee>> detailFees(@RequestParam Long detailId) {
|
||||
return R.data(formalSettlementService.detailFees(detailId));
|
||||
@@ -250,4 +281,11 @@ public class FormalSettlementController extends BladeController {
|
||||
public R<List<Map<String, Object>>> receiptClaims(@RequestParam Long formalSettlementId) {
|
||||
return R.data(receiptFlowService.settlementClaims(formalSettlementId));
|
||||
}
|
||||
|
||||
@GetMapping("/invoice-pool")
|
||||
@ApiOperationSupport(order = 21)
|
||||
@Operation(summary = "金蝶票据池镜像分页查询(认领查询)")
|
||||
public R<IPage<KingdeeInvoicePoolVO>> invoicePool(KingdeeInvoicePoolQueryDTO query) {
|
||||
return R.data(invoiceReceiptService.invoicePoolPage(query));
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -35,6 +35,7 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
import org.springblade.transport.pojo.dto.InvoiceApplicationSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
|
||||
@@ -131,6 +132,15 @@ public class InvoiceApplicationController extends BladeController {
|
||||
@Operation(summary = "审批通过开票申请")
|
||||
public R approve(@RequestBody InvoiceApplicationStatusRequest request) {
|
||||
invoiceApplicationService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
|
||||
@@ -156,4 +166,11 @@ public class InvoiceApplicationController extends BladeController {
|
||||
public R<String> syncKingdee(@RequestParam Long id) {
|
||||
return R.data(invoiceApplicationService.syncKingdee(id));
|
||||
}
|
||||
|
||||
@PostMapping("/sync-kingdee-invoice-result")
|
||||
@ApiOperationSupport(order = 13)
|
||||
@Operation(summary = "手动回写金蝶开票结果(A303-5)")
|
||||
public R<String> syncKingdeeInvoiceResult() {
|
||||
return R.data(invoiceApplicationService.syncKingdeeInvoiceResult());
|
||||
}
|
||||
}
|
||||
|
||||
+43
-9
@@ -29,16 +29,19 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
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 +83,24 @@ 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("/sync-invoice-pool")
|
||||
@ApiOperationSupport(order = 13)
|
||||
@Operation(summary = "手动触发金蝶全票池发票同步(窗口拉取+镜像全列upsert)")
|
||||
public R<String> syncInvoicePool() {
|
||||
return R.data(invoiceReceiptService.syncKingdeeInvoicePool());
|
||||
}
|
||||
|
||||
@GetMapping("/invoice-pool/attachment/download")
|
||||
@ApiOperationSupport(order = 17)
|
||||
@Operation(summary = "金蝶票池发票文件下载(A801-2 流式回源)",
|
||||
description = "按发票号定位金蝶侧文件:本地镜像优先,镜像未落库实时回源 A801-1;成功返回文件流,失败返回 JSON 错误")
|
||||
public void downloadPoolAttachment(@RequestParam String invoiceNo, HttpServletResponse response) {
|
||||
invoiceReceiptService.downloadPoolAttachment(invoiceNo, response);
|
||||
}
|
||||
|
||||
@GetMapping("/settlement-candidates")
|
||||
@@ -129,6 +147,15 @@ public class InvoiceReceiptController extends BladeController {
|
||||
@Operation(summary = "审批通过收票登记")
|
||||
public R approve(@RequestBody InvoiceReceiptStatusRequest request) {
|
||||
invoiceReceiptService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
|
||||
@@ -148,10 +175,17 @@ public class InvoiceReceiptController extends BladeController {
|
||||
return R.success("作废成功");
|
||||
}
|
||||
|
||||
@PostMapping("/sync-kingdee")
|
||||
@ApiOperationSupport(order = 12)
|
||||
@Operation(summary = "同步金蝶发票状态")
|
||||
public R<String> syncKingdee(@RequestParam Long id) {
|
||||
return R.data(invoiceReceiptService.syncKingdee(id));
|
||||
@PostMapping("/push-kingdee")
|
||||
@ApiOperationSupport(order = 15)
|
||||
@Operation(summary = "批量推送金蝶收票单(A303-2 收票单保存 + A302-6 收票指定应付)")
|
||||
public R<String> pushKingdee(@RequestBody List<Long> ids) {
|
||||
return R.data(invoiceReceiptService.pushKingdeeReceivedInvoice(ids));
|
||||
}
|
||||
|
||||
@PostMapping("/sync-kingdee-result")
|
||||
@ApiOperationSupport(order = 16)
|
||||
@Operation(summary = "手动触发金蝶收票单结果回写(A801-3 异常回收/指定补偿)")
|
||||
public R<String> syncKingdeeResult() {
|
||||
return R.data(invoiceReceiptService.syncKingdeeReceivedInvoiceResult());
|
||||
}
|
||||
}
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillLockResultVO;
|
||||
import org.springblade.transport.service.IKingdeeBillLockService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定回调 控制器
|
||||
* <p>
|
||||
* unLockNotifyBusiData:金蝶(星瀚)侧解锁/删单后回调 WLTMS 送结果,
|
||||
* 无登录态公开端点(模式同 CustomerArchivePublicController);
|
||||
* operationType=DELETE 时 WLTMS 废除源单并还原关联数据。
|
||||
* 应答与 lockBusiData 出参同构:{updateCount, updateResultList},逐单处理(1-成功,0-未匹配/失败)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@TenantIgnore
|
||||
// @PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@RequestMapping("kingdee-bfi")
|
||||
@Tag(name = "金蝶单据锁定回调", description = "金蝶星瀚调用:单据解锁/删单通知")
|
||||
public class KingdeeBfiPublicController {
|
||||
|
||||
private final IKingdeeBillLockService kingdeeBillLockService;
|
||||
|
||||
/**
|
||||
* 单据解锁通知(unLockNotifyBusiData):多张单据编号逗号拼接批量回调,逐张处理
|
||||
*
|
||||
* @param param 金蝶回调参数
|
||||
* @return 处理结果,与 lockBusiData 出参同构
|
||||
*/
|
||||
@PostMapping("unLockNotifyBusiData")
|
||||
@Operation(summary = "单据解锁通知", description = "金蝶侧解锁/删单后回调;operationType=DELETE 时废除源单并还原关联数据")
|
||||
public BfiBillLockResultVO unLockNotifyBusiData(@RequestBody BfiBillLockDTO param) {
|
||||
log.info("金蝶单据解锁回调:{}", JSON.toJSONString(param));
|
||||
try {
|
||||
return BfiBillLockResultVO.of(kingdeeBillLockService.onUnlockNotify(param));
|
||||
} catch (Exception exception) {
|
||||
log.error("金蝶单据解锁回调处理失败:lockAction={},operationType={},targetBillType={},targetBillNo={}",
|
||||
param == null ? null : param.getLockAction(), param == null ? null : param.getOperationType(),
|
||||
param == null ? null : param.getTargetBillType(), param == null ? null : param.getTargetBillNo(), exception);
|
||||
return BfiBillLockResultVO.of(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-1
@@ -29,10 +29,12 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
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;
|
||||
@@ -66,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 = "保存付款申请")
|
||||
@@ -81,7 +94,19 @@ public class PaymentApplicationController extends BladeController {
|
||||
@PostMapping("/approve")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "审批通过付款申请")
|
||||
public R approve(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.approve(request); return R.success("审批通过"); }
|
||||
public R approve(@RequestBody PaymentApplicationStatusRequest request) {
|
||||
paymentApplicationService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
@PostMapping("/return")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "驳回付款申请")
|
||||
@@ -100,4 +125,8 @@ public class PaymentApplicationController extends BladeController {
|
||||
public R<List<String>> syncKingdeeBatch(@RequestBody List<Long> ids) {
|
||||
return R.data(paymentApplicationService.syncKingdeeBatch(ids));
|
||||
}
|
||||
@PostMapping("/sync-kingdee-result")
|
||||
@ApiOperationSupport(order = 12)
|
||||
@Operation(summary = "手动回写金蝶付款结果(A103-2/A103-4)")
|
||||
public R<String> syncKingdeeResult() { return R.data(paymentApplicationService.syncKingdeePaymentResult()); }
|
||||
}
|
||||
|
||||
+10
-2
@@ -84,7 +84,15 @@ public class ReceiptClaimRecordController extends BladeController {
|
||||
@PostMapping("/void")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "作废认领记录并生成金蝶认领冲单")
|
||||
public R<String> voidClaim(@RequestParam Long id) {
|
||||
return R.data(receiptClaimRecordService.voidClaim(id));
|
||||
public R<Void> voidClaim(@RequestParam Long id) {
|
||||
receiptClaimRecordService.voidClaim(id);
|
||||
return R.success("作废成功");
|
||||
}
|
||||
|
||||
@PostMapping("/query-kingdee-result")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "查询金蝶认领结果(A301-1 收款单查询回写)")
|
||||
public R<Integer> queryKingdeeResult() {
|
||||
return R.data(receiptClaimRecordService.refreshKingdeeResult());
|
||||
}
|
||||
}
|
||||
|
||||
+12
-4
@@ -35,8 +35,8 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.dto.ReceiptClaimPushResult;
|
||||
import org.springblade.transport.pojo.dto.ReceiptClaimRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceiptFlowSyncRequest;
|
||||
import org.springblade.transport.pojo.vo.ReceiptFlowVO;
|
||||
import org.springblade.transport.service.IReceiptFlowService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -95,8 +95,16 @@ public class ReceiptFlowController extends BladeController {
|
||||
|
||||
@PostMapping("/sync")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "手动同步金蝶收款流水")
|
||||
public R<Integer> sync(@RequestBody(required = false) ReceiptFlowSyncRequest request) {
|
||||
return R.data(receiptFlowService.sync(request));
|
||||
@Operation(summary = "手动同步金蝶收款流水(A301-2 收款认领公告查询)")
|
||||
public R<Integer> sync() {
|
||||
return R.data(receiptFlowService.sync());
|
||||
}
|
||||
|
||||
@PostMapping("/push-kingdee")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "推送金蝶收款认领(A301-3 收款认领结果推送,仅认领完成流水可推送)")
|
||||
public R<ReceiptClaimPushResult> pushKingdee(@RequestParam Long flowId) {
|
||||
ReceiptClaimPushResult result = receiptFlowService.pushClaimToKingdee(flowId);
|
||||
return result.isSuccess() ? R.data(result) : R.fail(result.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -133,13 +133,15 @@ public class ReceivablePayableDetailController extends BladeController {
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "转结算候选明细")
|
||||
public R<IPage<Map<String, Object>>> transferCandidates(Query query,
|
||||
@RequestParam(required = false) String documentNo,
|
||||
@RequestParam(required = false) String contractNo,
|
||||
@RequestParam(required = false) String contractName,
|
||||
@RequestParam(required = false) String batchNo,
|
||||
@RequestParam(required = false) String generateStartDate,
|
||||
@RequestParam(required = false) String generateEndDate,
|
||||
@RequestParam(required = false) String settlementBillType,
|
||||
@RequestParam(required = false) String settlementType) {
|
||||
return R.data(detailService.transferCandidates(Condition.getPage(query), contractName, batchNo,
|
||||
return R.data(detailService.transferCandidates(Condition.getPage(query), documentNo, contractNo, contractName, batchNo,
|
||||
generateStartDate, generateEndDate, settlementBillType, settlementType));
|
||||
}
|
||||
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportBatchRequest;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportVoidRequest;
|
||||
import org.springblade.transport.pojo.vo.SettlementImportBatchVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||
import org.springblade.transport.service.ISettlementImportBatchService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 导入结算单控制器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@PreAuth(menu = "settlement_import")
|
||||
@RequestMapping("settlement-import")
|
||||
@Tag(name = "导入结算单", description = "导入结算单管理")
|
||||
public class SettlementImportController extends BladeController {
|
||||
|
||||
private final ISettlementImportBatchService settlementImportBatchService;
|
||||
|
||||
@GetMapping("/options")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "筛选项与表单选项")
|
||||
public R<Map<String, List<Map<String, Object>>>> options() {
|
||||
return R.data(settlementImportBatchService.options());
|
||||
}
|
||||
|
||||
@GetMapping("/next-code")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "下一个批次号")
|
||||
public R<String> nextCode() {
|
||||
return R.data(settlementImportBatchService.nextBatchNo());
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "导入结算单批次分页")
|
||||
public R<IPage<SettlementImportBatchVO>> list(SettlementImportBatchRequest request, Query query) {
|
||||
return R.data(settlementImportBatchService.page(Condition.getPage(query), request));
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "导入结算单批次详情")
|
||||
public R<SettlementImportBatchVO> detail(@RequestParam Long id) {
|
||||
return R.data(settlementImportBatchService.detail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/export-template")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导出导入模板")
|
||||
public void exportTemplate(HttpServletResponse response) {
|
||||
settlementImportBatchService.exportTemplate(response);
|
||||
}
|
||||
|
||||
@PostMapping("/parse")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "解析导入 Excel")
|
||||
public R<Map<String, Object>> parse(@RequestPart("file") MultipartFile file) {
|
||||
return R.data(settlementImportBatchService.parse(file));
|
||||
}
|
||||
|
||||
@PostMapping("/validate")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "校验导入数据")
|
||||
public R<WaybillImportValidateResultVO> validate(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.validate(request));
|
||||
}
|
||||
|
||||
@PostMapping("/export-failures")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "导出失败明细")
|
||||
public void exportFailures(@RequestBody SettlementImportBatchRequest request, HttpServletResponse response) {
|
||||
settlementImportBatchService.exportFailures(request, null, response);
|
||||
}
|
||||
|
||||
@PostMapping("/draft")
|
||||
@ApiOperationSupport(order = 9)
|
||||
@Operation(summary = "保存草稿")
|
||||
public R<SettlementImportBatchVO> draft(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.saveDraft(request));
|
||||
}
|
||||
|
||||
@PostMapping("/confirm")
|
||||
@ApiOperationSupport(order = 10)
|
||||
@Operation(summary = "确认导入结算单")
|
||||
public R<SettlementImportBatchVO> confirm(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.confirm(request));
|
||||
}
|
||||
|
||||
@PostMapping("/void")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "作废导入结算单批次")
|
||||
public R voidBatch(@RequestBody SettlementImportVoidRequest request) {
|
||||
settlementImportBatchService.voidBatch(request);
|
||||
return R.success("作废成功");
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -29,10 +29,16 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 对外路径:{@code /blade-transport/voucher-manage/**}
|
||||
* <p>
|
||||
* 网关 StripPrefix 后落入 {@code /voucher-manage/**};
|
||||
* 同时兼容未去前缀直连 {@code /blade-transport/voucher-manage/**},避免 404 落到静态资源。
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@PreAuth(menu = "voucher_manage")
|
||||
@RequestMapping("/voucher-manage")
|
||||
@RequestMapping({"/voucher-manage", "/blade-transport/voucher-manage"})
|
||||
@Tag(name = "凭证管理", description = "执行凭证批次管理")
|
||||
public class VoucherManageController extends BladeController {
|
||||
private final IVoucherManageService voucherManageService;
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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.event;
|
||||
|
||||
/**
|
||||
* 单据审批通过事件:审批事务提交后由 {@code KingdeeApprovalSyncListener} 消费,
|
||||
* 自动推送对应单据至金蝶(星瀚),省掉页面手动点「同步金蝶」。
|
||||
* <p>
|
||||
* bizType 复用 MkProcessBizHandlers 既有取值;只在审批状态真正变为 approved 的那次发布,
|
||||
* 重复回调被 approve 状态校验拒绝,天然不重复推送。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public record KingdeeBillApprovedEvent(String bizType, Long id) {
|
||||
|
||||
/** 正式结算单(应付 A102-2 / 应收 A302-2) */
|
||||
public static final String BIZ_FORMAL_SETTLEMENT = "formal-settlement";
|
||||
|
||||
/** 付款申请(A103-1 正式 / A103-3 预付) */
|
||||
public static final String BIZ_PAYMENT_APPLICATION = "payment-application";
|
||||
|
||||
/** 开票申请(A303-1) */
|
||||
public static final String BIZ_INVOICE_APPLICATION = "invoice-application";
|
||||
|
||||
/** 收票登记(A303-2 + A302-6) */
|
||||
public static final String BIZ_INVOICE_RECEIPT = "invoice-receipt";
|
||||
}
|
||||
+3
@@ -111,4 +111,7 @@ public class CustomerArchiveExcel implements Serializable {
|
||||
@ExcelProperty("审核通过时间")
|
||||
private LocalDateTime approvedTime;
|
||||
|
||||
@ExcelProperty("金蝶同步状态")
|
||||
private String kingdeeStatusName;
|
||||
|
||||
}
|
||||
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.springblade.common.excel.ImportCellReadException;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 导入结算单 Excel 读取器(Apache POI)。
|
||||
* <p>
|
||||
* 定位「*运输费」或「运输费」列,其后直到「结算金额」为动态费用列。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class SettlementImportFeeReader {
|
||||
|
||||
private static final DataFormatter DATA_FORMATTER = new DataFormatter(Locale.CHINA);
|
||||
|
||||
private SettlementImportFeeReader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析结果
|
||||
*/
|
||||
public static final class ParseResult {
|
||||
private final List<Map<String, Object>> rows;
|
||||
private final List<String> feeItemNames;
|
||||
private final List<String> warnings;
|
||||
|
||||
public ParseResult(List<Map<String, Object>> rows, List<String> feeItemNames, List<String> warnings) {
|
||||
this.rows = rows;
|
||||
this.feeItemNames = feeItemNames;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getRows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
public List<String> getFeeItemNames() {
|
||||
return feeItemNames;
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
}
|
||||
|
||||
public static ParseResult read(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("上传文件不能为空");
|
||||
}
|
||||
try (InputStream inputStream = file.getInputStream(); Workbook workbook = WorkbookFactory.create(inputStream)) {
|
||||
Sheet sheet = resolveDataSheet(workbook);
|
||||
Row headerRow = sheet.getRow(sheet.getFirstRowNum());
|
||||
if (headerRow == null) {
|
||||
throw new ServiceException("未识别到表头,请使用标准导入结算单模板");
|
||||
}
|
||||
Map<Integer, String> headers = readHeaderMap(headerRow);
|
||||
int transportFeeColumn = findTransportFeeColumn(headers);
|
||||
if (transportFeeColumn < 0) {
|
||||
throw new ServiceException("未找到「*运输费」或「运输费」列");
|
||||
}
|
||||
int settlementAmountColumn = findColumnExact(headers, "结算金额");
|
||||
List<Integer> feeColumns = new ArrayList<>();
|
||||
List<String> feeItemNames = new ArrayList<>();
|
||||
int endColumn = settlementAmountColumn > transportFeeColumn
|
||||
? settlementAmountColumn
|
||||
: headers.keySet().stream().mapToInt(Integer::intValue).max().orElse(transportFeeColumn) + 1;
|
||||
for (int column = transportFeeColumn + 1; column < endColumn; column++) {
|
||||
String name = headers.get(column);
|
||||
if (name == null || name.isBlank() || "结算金额".equals(name)) {
|
||||
continue;
|
||||
}
|
||||
feeColumns.add(column);
|
||||
feeItemNames.add(name);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
int firstDataRow = sheet.getFirstRowNum() + 1;
|
||||
int lastRow = sheet.getLastRowNum();
|
||||
for (int rowIndex = firstDataRow; rowIndex <= lastRow; rowIndex++) {
|
||||
Row row = sheet.getRow(rowIndex);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> mapped = mapRow(headers, feeColumns, row, rowIndex);
|
||||
if (mapped != null) {
|
||||
rows.add(mapped);
|
||||
}
|
||||
}
|
||||
return new ParseResult(rows, feeItemNames, List.of());
|
||||
} catch (ServiceException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("读取导入结算单 Excel 失败:" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 优先取含「*运输费/运输费」表头的工作表,否则取第一个非空表。 */
|
||||
private static Sheet resolveDataSheet(Workbook workbook) {
|
||||
Sheet fallback = null;
|
||||
for (int index = 0; index < workbook.getNumberOfSheets(); index++) {
|
||||
Sheet sheet = workbook.getSheetAt(index);
|
||||
if (sheet == null) {
|
||||
continue;
|
||||
}
|
||||
Row headerRow = sheet.getRow(sheet.getFirstRowNum());
|
||||
if (headerRow == null) {
|
||||
continue;
|
||||
}
|
||||
Map<Integer, String> headers = readHeaderMap(headerRow);
|
||||
if (findTransportFeeColumn(headers) >= 0) {
|
||||
return sheet;
|
||||
}
|
||||
if (fallback == null && !headers.isEmpty()) {
|
||||
fallback = sheet;
|
||||
}
|
||||
}
|
||||
if (fallback != null) {
|
||||
return fallback;
|
||||
}
|
||||
throw new ServiceException("Excel 中未找到有效工作表");
|
||||
}
|
||||
|
||||
private static Map<Integer, String> readHeaderMap(Row headerRow) {
|
||||
Map<Integer, String> headers = new LinkedHashMap<>();
|
||||
short lastCell = headerRow.getLastCellNum();
|
||||
for (int column = 0; column < lastCell; column++) {
|
||||
String value = normalizeHeader(cellString(headerRow.getCell(column)));
|
||||
if (value != null && !value.isBlank()) {
|
||||
headers.put(column, value);
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** 去掉 BOM/不可见空白,全角星号转半角,便于匹配「*运输费」。 */
|
||||
private static String normalizeHeader(String raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
String text = raw.replace("\uFEFF", "")
|
||||
.replace('\u00A0', ' ')
|
||||
.replace('*', '*')
|
||||
.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static Map<String, Object> mapRow(Map<Integer, String> headers, List<Integer> feeColumns,
|
||||
Row row, int excelRowIndex) {
|
||||
Map<String, Object> mapped = new LinkedHashMap<>();
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
for (Map.Entry<Integer, String> entry : headers.entrySet()) {
|
||||
int column = entry.getKey();
|
||||
String trimmedHeader = entry.getValue();
|
||||
String cell = cellString(row.getCell(column));
|
||||
if (feeColumns.contains(column)) {
|
||||
feeItems.put(trimmedHeader, decimal(cell, trimmedHeader, excelRowIndex));
|
||||
continue;
|
||||
}
|
||||
String field = FIXED_HEADER_MAP.get(trimmedHeader);
|
||||
if (field == null) {
|
||||
continue;
|
||||
}
|
||||
if ("transportFee".equals(field) || "unitPrice".equals(field)
|
||||
|| "quantity".equals(field) || "mileage".equals(field)
|
||||
|| "settlementAmount".equals(field)) {
|
||||
mapped.put(field, decimal(cell, trimmedHeader, excelRowIndex));
|
||||
} else if ("serialNumber".equals(field)) {
|
||||
mapped.put(field, intValue(cell));
|
||||
} else {
|
||||
mapped.put(field, blankToNull(cell));
|
||||
}
|
||||
mapped.put(trimmedHeader, mapped.get(field));
|
||||
}
|
||||
|
||||
boolean hasBusinessValue = mapped.values().stream().anyMatch(value -> {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
if (value instanceof String text) {
|
||||
return !text.isBlank();
|
||||
}
|
||||
return true;
|
||||
}) || feeItems.values().stream().anyMatch(amount -> amount != null && amount.compareTo(BigDecimal.ZERO) != 0);
|
||||
if (!hasBusinessValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BigDecimal transportFee = toDecimal(mapped.get("transportFee"));
|
||||
BigDecimal feeItemTotal = feeItems.values().stream()
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
BigDecimal settlementAmount = transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
|
||||
mapped.put("settlementAmount", settlementAmount);
|
||||
mapped.put("结算金额", settlementAmount);
|
||||
mapped.put("feeItems", feeItems);
|
||||
mapped.put("freight", transportFee);
|
||||
mapped.put("运费", transportFee);
|
||||
mapped.put("*运输费", transportFee);
|
||||
mapped.put("运输费", transportFee);
|
||||
mapped.put("otherFeeTotal", feeItemTotal);
|
||||
mapped.put("其他费用合计", feeItemTotal);
|
||||
mapped.put("freightTotal", settlementAmount);
|
||||
mapped.put("运费合计", settlementAmount);
|
||||
if (mapped.get("unitPrice") != null) {
|
||||
mapped.put("单价", mapped.get("unitPrice"));
|
||||
mapped.put("运输单价", mapped.get("unitPrice"));
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
private static int findTransportFeeColumn(Map<Integer, String> headers) {
|
||||
int starred = findColumnExact(headers, "*运输费");
|
||||
if (starred >= 0) {
|
||||
return starred;
|
||||
}
|
||||
return findColumnExact(headers, "运输费");
|
||||
}
|
||||
|
||||
private static int findColumnExact(Map<Integer, String> headers, String header) {
|
||||
return headers.entrySet().stream()
|
||||
.filter(entry -> header.equals(entry.getValue()))
|
||||
.mapToInt(Map.Entry::getKey)
|
||||
.findFirst()
|
||||
.orElse(-1);
|
||||
}
|
||||
|
||||
private static String cellString(Cell cell) {
|
||||
if (cell == null) {
|
||||
return null;
|
||||
}
|
||||
String text = DATA_FORMATTER.formatCellValue(cell);
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
text = text.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static BigDecimal decimal(String cell, String columnName, Integer excelRowIndex) {
|
||||
if (cell == null || cell.isBlank()) {
|
||||
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
String normalized = cell.trim().replace(",", "");
|
||||
try {
|
||||
return new BigDecimal(normalized).setScale(2, RoundingMode.HALF_UP);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new ImportCellReadException(excelRowIndex, columnName, cell.trim());
|
||||
}
|
||||
}
|
||||
|
||||
private static Integer intValue(String cell) {
|
||||
if (cell == null || cell.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(cell.trim().replace(",", "")).intValue();
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String blankToNull(String cell) {
|
||||
if (cell == null) {
|
||||
return null;
|
||||
}
|
||||
String text = cell.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static BigDecimal toDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
if (value instanceof BigDecimal bigDecimal) {
|
||||
return bigDecimal;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(value).trim()).setScale(2, RoundingMode.HALF_UP);
|
||||
} catch (NumberFormatException exception) {
|
||||
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Map<String, String> FIXED_HEADER_MAP = buildFixedHeaderMap();
|
||||
|
||||
private static Map<String, String> buildFixedHeaderMap() {
|
||||
Map<String, String> map = new LinkedHashMap<>();
|
||||
map.put("序号", "serialNumber");
|
||||
map.put("原始单号", "originalNo");
|
||||
map.put("配载标识号", "loadingIdentifier");
|
||||
map.put("*车牌号/船班号/船次号/班次号", "vehicleNo");
|
||||
map.put("车牌号/船班号/船次号/班次号", "vehicleNo");
|
||||
map.put("*车牌号/航班号/船号/班列号", "vehicleNo");
|
||||
map.put("车牌号/航班号/船号/班列号", "vehicleNo");
|
||||
map.put("*运输方式", "transportType");
|
||||
map.put("运输方式", "transportType");
|
||||
map.put("运输类型", "transportType");
|
||||
map.put("*运输类型", "transportType");
|
||||
map.put("司机/船长姓名", "driverName");
|
||||
map.put("司机/船长手机号", "driverPhone");
|
||||
map.put("*发货地址", "departureAddress");
|
||||
map.put("发货地址", "departureAddress");
|
||||
map.put("发货联系人", "departureContact");
|
||||
map.put("发货联系人电话", "departurePhone");
|
||||
map.put("*到货地址", "arrivalAddress");
|
||||
map.put("到货地址", "arrivalAddress");
|
||||
map.put("收货联系人", "arrivalContact");
|
||||
map.put("收货联系人电话", "arrivalPhone");
|
||||
map.put("*货物名称", "cargoName");
|
||||
map.put("货物名称", "cargoName");
|
||||
map.put("*货物类型", "cargoType");
|
||||
map.put("货物类型", "cargoType");
|
||||
map.put("包装", "packageType");
|
||||
map.put("*数量", "quantity");
|
||||
map.put("数量", "quantity");
|
||||
map.put("*数量单位", "quantityUnit");
|
||||
map.put("数量单位", "quantityUnit");
|
||||
map.put("规格", "specification");
|
||||
map.put("型号", "model");
|
||||
map.put("里程(km)", "mileage");
|
||||
map.put("里程", "mileage");
|
||||
map.put("运输单价", "unitPrice");
|
||||
map.put("单价", "unitPrice");
|
||||
map.put("*运输费", "transportFee");
|
||||
map.put("运输费", "transportFee");
|
||||
map.put("结算金额", "settlementAmount");
|
||||
map.put("*实际发货时间", "actualStartDate");
|
||||
map.put("实际发货时间", "actualStartDate");
|
||||
map.put("*实际完成时间", "actualEndDate");
|
||||
map.put("实际完成时间", "actualEndDate");
|
||||
map.put("预计发货时间", "planStartDate");
|
||||
map.put("预计完成时间", "planEndDate");
|
||||
map.put("备注", "remark");
|
||||
map.put("同一运单标识号", "waybillIdentifier");
|
||||
return map;
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnore;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 导入结算单 Excel 模板(固定列 + 运输单价 + *运输费 + 示例费用项)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(28)
|
||||
@ContentRowHeight(18)
|
||||
public class SettlementImportTemplateExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("序号")
|
||||
private Integer serialNumber;
|
||||
@ExcelProperty("原始单号")
|
||||
private String originalNo;
|
||||
@ExcelProperty("配载标识号")
|
||||
private String loadingIdentifier;
|
||||
@ExcelProperty("*车牌号/航班号/船号/班列号")
|
||||
private String vehicleNo;
|
||||
@ExcelProperty("*运输方式")
|
||||
private String transportType;
|
||||
@ExcelProperty("司机/船长姓名")
|
||||
private String driverName;
|
||||
@ExcelProperty("司机/船长手机号")
|
||||
private String driverPhone;
|
||||
@ExcelProperty("*发货地址")
|
||||
private String departureAddress;
|
||||
@ExcelProperty("发货联系人")
|
||||
private String departureContact;
|
||||
@ExcelProperty("发货联系人电话")
|
||||
private String departurePhone;
|
||||
@ExcelProperty("*到货地址")
|
||||
private String arrivalAddress;
|
||||
@ExcelProperty("收货联系人")
|
||||
private String arrivalContact;
|
||||
@ExcelProperty("收货联系人电话")
|
||||
private String arrivalPhone;
|
||||
@ExcelProperty("*货物名称")
|
||||
private String cargoName;
|
||||
@ExcelProperty("*货物类型")
|
||||
private String cargoType;
|
||||
@ExcelProperty("包装")
|
||||
private String packageType;
|
||||
@ExcelProperty("*数量")
|
||||
private BigDecimal quantity;
|
||||
@ExcelProperty("*数量单位")
|
||||
private String quantityUnit;
|
||||
@ExcelProperty("规格")
|
||||
private String specification;
|
||||
@ExcelProperty("型号")
|
||||
private String model;
|
||||
@ExcelProperty("里程(km)")
|
||||
private BigDecimal mileage;
|
||||
@ExcelProperty("运输单价")
|
||||
private BigDecimal unitPrice;
|
||||
@ExcelProperty("*运输费")
|
||||
private BigDecimal transportFee;
|
||||
@ExcelProperty("装卸费")
|
||||
private BigDecimal loadingFee;
|
||||
@ExcelProperty("过路费")
|
||||
private BigDecimal tollFee;
|
||||
@ExcelProperty("结算金额")
|
||||
private BigDecimal settlementAmount;
|
||||
@ExcelProperty("*实际发货时间")
|
||||
private String actualStartDate;
|
||||
@ExcelProperty("*实际完成时间")
|
||||
private String actualEndDate;
|
||||
@ExcelProperty("预计发货时间")
|
||||
private String planStartDate;
|
||||
@ExcelProperty("预计完成时间")
|
||||
private String planEndDate;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("同一运单标识号")
|
||||
private String waybillIdentifier;
|
||||
|
||||
/** 导入失败原因(不导出到模板,仅用于失败明细) */
|
||||
@ExcelIgnore
|
||||
private String errorMessage;
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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.listener;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.event.KingdeeBillApprovedEvent;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IInvoiceApplicationService;
|
||||
import org.springblade.transport.service.IInvoiceReceiptService;
|
||||
import org.springblade.transport.service.IPaymentApplicationService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.event.TransactionPhase;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 单据审批通过自动同步金蝶监听器:审批事务提交(AFTER_COMMIT)后复用手动按钮同一 Service 方法推送,
|
||||
* 与客商档案审批后自动推送同线程模式。
|
||||
* <p>
|
||||
* 不变式:审批结果不受推送影响——监听器整体 try/catch 吞异常,推送异常只落 failed + 日志;
|
||||
* 失败单据由 {@code KingdeeAutoSyncRetryTask} 定时重推、手动按钮兜底。
|
||||
* <p>
|
||||
* 推送失败原因通过 {@link #pollSyncFailure()} 传递给审批接口响应:AFTER_COMMIT 监听与 approve
|
||||
* 同线程同请求,Controller 在 approve 返回后取走原因拼进提示文案;OA 回调路径无人取走,
|
||||
* 由监听入口清空兜底,不会串单。
|
||||
* <p>
|
||||
* ponytail: 同步推送占用审批请求线程,若 BFI 网关大面积变慢导致审批接口可感知变慢,
|
||||
* 再把路由调用提交到守护线程池(MkProcessMessageService 已有同款裸 ScheduledExecutorService 模式)。
|
||||
* <p>
|
||||
* 域开关(Nacos thirdParty.bfi.autoSync.*)默认全部关闭,与既有定时任务开关风格一致;
|
||||
* 关闭某域 = 该域回到现状(审批不自动推送、手动按钮照用)。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeApprovalSyncListener {
|
||||
|
||||
private final IFormalSettlementService formalSettlementService;
|
||||
private final IPaymentApplicationService paymentApplicationService;
|
||||
private final IInvoiceApplicationService invoiceApplicationService;
|
||||
private final IInvoiceReceiptService invoiceReceiptService;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
/** 当前审批请求线程的自动推送失败原因,供 Controller 取走提示前端(见类注释线程说明) */
|
||||
private static final ThreadLocal<String> SYNC_FAILURE = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 取走并清除当前审批请求的自动推送失败原因;null 表示推送成功或未触发推送。
|
||||
* 须在 service.approve 返回后立即调用(监听器在审批事务提交时同步执行完毕)。
|
||||
*/
|
||||
public static String pollSyncFailure() {
|
||||
String failure = SYNC_FAILURE.get();
|
||||
SYNC_FAILURE.remove();
|
||||
return failure;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务提交后同步推送。fallbackExecution=true 兜底:approve 未开启事务时事件不再被默认丢弃。
|
||||
*
|
||||
* @param event 审批通过事件(bizType + 单据id)
|
||||
*/
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
|
||||
public void onBillApproved(KingdeeBillApprovedEvent event) {
|
||||
SYNC_FAILURE.remove();
|
||||
try {
|
||||
switch (event.bizType()) {
|
||||
case KingdeeBillApprovedEvent.BIZ_FORMAL_SETTLEMENT -> pushSettlement(event.id());
|
||||
case KingdeeBillApprovedEvent.BIZ_PAYMENT_APPLICATION -> pushPayment(event.id());
|
||||
case KingdeeBillApprovedEvent.BIZ_INVOICE_APPLICATION -> pushInvoiceApply(event.id());
|
||||
case KingdeeBillApprovedEvent.BIZ_INVOICE_RECEIPT -> pushReceipt(event.id());
|
||||
default -> log.warn("审批通过自动同步金蝶跳过:未知业务类型 bizType={} id={}", event.bizType(), event.id());
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
// 推送失败只记日志(失败状态已在推送方法内独立事务落库),不影响审批结果,交给重推任务与手动按钮兜底;
|
||||
// 原因暂存线程变量,由审批 Controller 取走提示前端
|
||||
log.error("审批通过自动同步金蝶失败 bizType={} id={}", event.bizType(), event.id(), exception);
|
||||
String reason = exception.getMessage() == null ? "金蝶接口调用异常" : exception.getMessage();
|
||||
// 手动按钮同文案已带"推送金蝶失败:"前缀,拼提示时去掉避免重复
|
||||
SYNC_FAILURE.set(reason.startsWith("推送金蝶失败:") ? reason.substring("推送金蝶失败:".length()) : reason);
|
||||
}
|
||||
}
|
||||
|
||||
private void pushSettlement(Long id) {
|
||||
if (!bfiProperties.getAutoSync().isSettlement()) {
|
||||
return;
|
||||
}
|
||||
log.info("正式结算单[{}]审批通过自动同步金蝶开始", id);
|
||||
String billNo = formalSettlementService.syncKingdee(id);
|
||||
log.info("正式结算单[{}]审批通过自动同步金蝶完成:billNo={}", id, billNo);
|
||||
}
|
||||
|
||||
private void pushPayment(Long id) {
|
||||
if (!bfiProperties.getAutoSync().isPayment()) {
|
||||
return;
|
||||
}
|
||||
log.info("付款申请[{}]审批通过自动同步金蝶开始", id);
|
||||
String kingdeeBillId = paymentApplicationService.syncKingdee(id);
|
||||
log.info("付款申请[{}]审批通过自动同步金蝶完成:kingdeeBillId={}", id, kingdeeBillId);
|
||||
}
|
||||
|
||||
private void pushInvoiceApply(Long id) {
|
||||
if (!bfiProperties.getAutoSync().isInvoiceApply()) {
|
||||
return;
|
||||
}
|
||||
log.info("开票申请[{}]审批通过自动同步金蝶开始", id);
|
||||
String billNo = invoiceApplicationService.syncKingdee(id);
|
||||
log.info("开票申请[{}]审批通过自动同步金蝶完成:billNo={}", id, billNo);
|
||||
}
|
||||
|
||||
private void pushReceipt(Long id) {
|
||||
if (!bfiProperties.getAutoSync().isReceipt()) {
|
||||
return;
|
||||
}
|
||||
log.info("收票登记[{}]审批通过自动推送金蝶收票单开始", id);
|
||||
String summary = invoiceReceiptService.pushKingdeeReceivedInvoice(List.of(id), true);
|
||||
log.info("收票登记[{}]审批通过自动推送金蝶收票单完成:{}", id, summary);
|
||||
}
|
||||
}
|
||||
+6
@@ -48,6 +48,9 @@
|
||||
<result column="current_processor" property="currentProcessor"/>
|
||||
<result column="approved_time" property="approvedTime"/>
|
||||
<result column="review_status" property="reviewStatus"/>
|
||||
<result column="kingdee_status" property="kingdeeStatus"/>
|
||||
<result column="kingdee_fail_reason" property="kingdeeFailReason"/>
|
||||
<result column="kingdee_sync_time" property="kingdeeSyncTime"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectCustomerArchivePage" resultMap="customerArchiveResultMap">
|
||||
@@ -96,6 +99,9 @@
|
||||
current_node,
|
||||
current_processor,
|
||||
approved_time,
|
||||
kingdee_status,
|
||||
kingdee_fail_reason,
|
||||
kingdee_sync_time,
|
||||
(
|
||||
SELECT s.review_status
|
||||
FROM blade_customer_credit_score s
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
|
||||
|
||||
/**
|
||||
* 开票申请金蝶发票结果 Mapper
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface InvoiceApplicationInvoiceMapper extends BaseMapper<InvoiceApplicationInvoice> {
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springblade.transport.pojo.entity.KingdeeAttachmentSync;
|
||||
|
||||
/**
|
||||
* 金蝶附件同步 Mapper
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Mapper
|
||||
public interface KingdeeAttachmentSyncMapper extends BaseMapper<KingdeeAttachmentSync> {
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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.springblade.transport.pojo.entity.KingdeeBillLock;
|
||||
|
||||
/** 金蝶单据锁定记录 Mapper @author Chill */
|
||||
@Mapper
|
||||
public interface KingdeeBillLockMapper extends BaseMapper<KingdeeBillLock> {
|
||||
}
|
||||
+15
@@ -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,15 @@ import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
|
||||
*/
|
||||
@Mapper
|
||||
public interface KingdeeInvoicePoolMapper extends BaseMapper<KingdeeInvoicePool> {
|
||||
|
||||
/**
|
||||
* 金蝶票据池镜像统一分页查询:仅出可用票(剩余可用金额>0 且未使用),认领/登记标记由 EXISTS 子查询直出
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
|
||||
* @param tenantId 当前租户
|
||||
* @return 统一视图分页
|
||||
*/
|
||||
IPage<KingdeeInvoicePoolVO> selectInvoicePoolPage(IPage<KingdeeInvoicePoolVO> page,
|
||||
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId);
|
||||
}
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<?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,
|
||||
p.available_amount AS available_tax_amount,
|
||||
<include refid="claimedExists"/> AS claimed,
|
||||
<include refid="registeredExists"/> AS registered
|
||||
</sql>
|
||||
|
||||
<!-- 选票只出可用票:剩余可用金额>0 且金额使用状态未使用(与实时方案 isUsablePoolRow 同口径,本地化执行) -->
|
||||
<sql id="usableFilter">
|
||||
AND p.available_amount > 0 AND p.amount_use_status = '0'
|
||||
</sql>
|
||||
|
||||
<select id="selectInvoicePoolPage" resultType="org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO">
|
||||
SELECT
|
||||
<include refid="invoicePoolVoColumns"/>
|
||||
FROM blade_kingdee_invoice_pool p
|
||||
WHERE p.is_deleted = 0 AND p.status = 1 AND p.tenant_id = #{tenantId}
|
||||
<include refid="usableFilter"/>
|
||||
<if test="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_code = #{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 >= #{query.invoiceDateStart}
|
||||
</if>
|
||||
<if test="query.invoiceDateEnd != null and query.invoiceDateEnd != ''">
|
||||
AND p.invoice_date <= #{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>
|
||||
</mapper>
|
||||
+4
@@ -36,6 +36,7 @@
|
||||
f.summary,
|
||||
f.transaction_time,
|
||||
f.detail_serial_no,
|
||||
f.push_status,
|
||||
(SELECT GROUP_CONCAT(rcs.formal_settlement_no ORDER BY rcs.id SEPARATOR ',')
|
||||
FROM blade_receipt_claim_settlement rcs
|
||||
WHERE rcs.receipt_claim_id = c.id AND rcs.is_deleted = 0) AS associated_settlement_nos
|
||||
@@ -91,6 +92,9 @@
|
||||
<if test="query.kingdeeBillStatus != null and query.kingdeeBillStatus != ''">
|
||||
AND c.kingdee_bill_status = #{query.kingdeeBillStatus}
|
||||
</if>
|
||||
<if test="query.pushStatus != null and query.pushStatus != ''">
|
||||
AND f.push_status = #{query.pushStatus}
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<select id="selectClaimRecordPage" resultType="org.springblade.transport.pojo.vo.ReceiptClaimRecordVO">
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springblade.transport.pojo.entity.SettlementImportBatch;
|
||||
|
||||
/**
|
||||
* 导入结算单批次 Mapper
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Mapper
|
||||
public interface SettlementImportBatchMapper extends BaseMapper<SettlementImportBatch> {
|
||||
|
||||
/**
|
||||
* 查询指定租户、指定日期前缀下已使用的最大批次流水号。
|
||||
* 不过滤逻辑删除数据,避免唯一索引仍占用历史编号时重复生成。
|
||||
*/
|
||||
@Select("SELECT COALESCE(MAX(CAST(SUBSTRING(batch_no, CHAR_LENGTH(#{prefix}) + 1) AS UNSIGNED)), 0) "
|
||||
+ "FROM blade_settlement_import_batch WHERE tenant_id = #{tenantId} "
|
||||
+ "AND batch_no LIKE CONCAT(#{prefix}, '%')")
|
||||
Long selectMaxDailySequence(@Param("tenantId") String tenantId, @Param("prefix") String prefix);
|
||||
}
|
||||
+40
@@ -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);
|
||||
|
||||
}
|
||||
+35
@@ -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, '') <> 'voided')
|
||||
OR (w.settlement_type = 'formal' AND f.id IS NOT NULL AND IFNULL(f.approval_status, '') <> '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>
|
||||
+26
-9
@@ -4,8 +4,10 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.dto.BillPaymentStatusRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest;
|
||||
import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest;
|
||||
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
|
||||
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
|
||||
import org.springblade.transport.pojo.entity.BillPayment;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
@@ -311,11 +313,18 @@ public final class MkProcessBizHandlers {
|
||||
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
entity.setApprovalStatus("approved");
|
||||
entity.setCurrentNode("审核通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
entity.setApprovedTime(LocalDateTime.now());
|
||||
return service.updateById(entity);
|
||||
// 统一走 service.approve:与手动审批同一入口(补变更记录),审批事务提交后自动触发金蝶推送事件;
|
||||
// 仅审批中单据允许通过,重复回调被状态校验拒绝,不重复推送
|
||||
FormalSettlementStatusRequest request = new FormalSettlementStatusRequest();
|
||||
request.setId(id);
|
||||
service.approve(request);
|
||||
FormalSettlement updated = service.getById(id);
|
||||
if (updated != null) {
|
||||
updated.setCurrentNode("审核通过");
|
||||
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
service.updateById(updated);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -374,10 +383,18 @@ public final class MkProcessBizHandlers {
|
||||
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
entity.setApprovalStatus("approved");
|
||||
entity.setCurrentNode("审核通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
return service.updateById(entity);
|
||||
// 统一走 service.approve:与手动审批同一入口(补票据额度联动 useBillBalance),审批事务提交后自动触发金蝶推送事件;
|
||||
// 仅审批中单据允许通过,重复回调被状态校验拒绝,不重复推送
|
||||
PaymentApplicationStatusRequest request = new PaymentApplicationStatusRequest();
|
||||
request.setId(id);
|
||||
service.approve(request);
|
||||
PaymentApplication updated = service.getById(id);
|
||||
if (updated != null) {
|
||||
updated.setCurrentNode("审核通过");
|
||||
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
service.updateById(updated);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+31
@@ -188,6 +188,37 @@ public interface ICustomerArchiveService extends BaseService<CustomerArchive> {
|
||||
*/
|
||||
boolean removeDraft(String ids);
|
||||
|
||||
/**
|
||||
* 推送金蝶(A902-1 复查 + A902-2 保存),幂等:已推送/金蝶已有直接返回
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 更新后的客商档案
|
||||
*/
|
||||
CustomerArchive syncKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 推送失败状态落库(独立事务):推送主事务已回滚,失败状态与原因单独落库供页面展示与手动重推
|
||||
*
|
||||
* @param id 主键
|
||||
* @param reason 失败原因
|
||||
*/
|
||||
void markKingdeeSyncFailed(Long id, String reason);
|
||||
|
||||
/**
|
||||
* 定时同步金蝶客商数据:待推送客商自动推送 + 已同步客商使用状态回写
|
||||
*
|
||||
* @return 同步汇总说明
|
||||
*/
|
||||
String syncKingdeeCustomerData();
|
||||
|
||||
/**
|
||||
* 金蝶侧停用联动:停用客商档案并记录变更(仅当前启用的客商生效)
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 是否执行停用
|
||||
*/
|
||||
boolean disableCustomerFromKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 基于启用评分量化表生成评分记录
|
||||
*
|
||||
|
||||
+46
@@ -18,6 +18,7 @@ import org.springblade.transport.pojo.vo.PreSettlementVO;
|
||||
import org.springblade.transport.pojo.entity.PreSettlement;
|
||||
import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetailFee;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementBatchPaymentRequest;
|
||||
@@ -40,9 +41,54 @@ public interface IFormalSettlementService extends BaseService<FormalSettlement>
|
||||
void returnBill(FormalSettlementStatusRequest request);
|
||||
void voidBill(FormalSettlementStatusRequest request);
|
||||
String syncKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 金蝶删单回调废除(unLockNotifyBusiData DELETE 分发):作废结算单、同步信息还原推送前状态
|
||||
* (kingdeeSyncStatus→unsynced,清单据编号/星瀚内码/同步时间),操作人取金蝶回调报文
|
||||
*
|
||||
* @param id 正式结算单id
|
||||
* @param operator 金蝶侧操作人
|
||||
*/
|
||||
void voidByKingdee(Long id, String operator);
|
||||
|
||||
/**
|
||||
* 单据级事务执行金蝶推送(由 syncKingdee 经自代理调用):
|
||||
* A102-2/A302-2 保存并回写结算单单据编号与星瀚内码(kingdeeBillId)
|
||||
*
|
||||
* @param id 正式结算单id
|
||||
* @return 金蝶单据编号
|
||||
*/
|
||||
String doSyncKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 独立事务落"生成失败"状态并记失败原因(在推送事务回滚、行锁释放后执行,与付款/收票口径统一);
|
||||
* 仅更新审批通过且未同步的单据,未审批/已同步/已删除单据自动跳过,保持原状态
|
||||
*
|
||||
* @param id 正式结算单id
|
||||
* @param reason 失败原因(写变更记录留档)
|
||||
*/
|
||||
void markKingdeeSyncFailed(Long id, String reason);
|
||||
|
||||
/**
|
||||
* 重推金蝶生成失败(kingdeeSyncStatus=failed 且审批通过)的正式结算单:逐单独立成败,失败不中断。
|
||||
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String retryKingdeeFailed();
|
||||
|
||||
String syncKingdeeArResult();
|
||||
List<FormalSettlementDetailFee> detailFees(Long detailId);
|
||||
void adjustDetail(PreSettlementDetailAdjustRequest request);
|
||||
void refreshPaymentSummary(Long settlementId);
|
||||
|
||||
/**
|
||||
* 批量刷新收款汇总:固定数次查询完成 N 张结算单的重算,避免逐单刷新的 N+1
|
||||
*
|
||||
* @param settlementIds 结算单id集合
|
||||
*/
|
||||
void refreshPaymentSummaries(Collection<Long> settlementIds);
|
||||
|
||||
void refreshPaymentSummariesForPreSettlement(Long preSettlementId);
|
||||
String applyPayment(FormalSettlementPaymentRequest request);
|
||||
List<String> applyPayments(FormalSettlementBatchPaymentRequest request);
|
||||
|
||||
+37
@@ -55,4 +55,41 @@ public interface IInvoiceApplicationService extends BaseService<InvoiceApplicati
|
||||
void returnBill(InvoiceApplicationStatusRequest request);
|
||||
void voidBill(InvoiceApplicationStatusRequest request);
|
||||
String syncKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 金蝶删单回调作废(unLockNotifyBusiData DELETE 分发):作废开票申请、同步信息还原
|
||||
* (kingdeeStatus→unsynced,清单据编号/同步时间)并刷新关联结算单开票状态,操作人取金蝶回调报文
|
||||
*
|
||||
* @param id 开票申请id
|
||||
* @param operator 金蝶侧操作人
|
||||
*/
|
||||
void voidByKingdee(Long id, String operator);
|
||||
|
||||
/**
|
||||
* 单据级事务执行金蝶推送(由 syncKingdee 经自代理调用):
|
||||
* A303-1 保存并回写开票申请单单据编号与星瀚内码(kingdeeBillId)
|
||||
*
|
||||
* @param id 开票申请id
|
||||
* @return 金蝶开票申请单编号
|
||||
*/
|
||||
String doSyncKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 独立事务落"生成失败"状态并记失败原因(在推送事务回滚、行锁释放后执行,与付款/收票口径统一);
|
||||
* 仅更新审批通过且未同步/未开票的单据,其余自动跳过,保持原状态
|
||||
*
|
||||
* @param id 开票申请id
|
||||
* @param reason 失败原因(写操作流水留档)
|
||||
*/
|
||||
void markKingdeeSyncFailed(Long id, String reason);
|
||||
|
||||
/**
|
||||
* 重推金蝶生成失败(kingdeeStatus=failed 且审批通过)的开票申请:逐单独立成败,失败不中断。
|
||||
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String retryKingdeeFailed();
|
||||
|
||||
String syncKingdeeInvoiceResult();
|
||||
}
|
||||
|
||||
+77
-3
@@ -26,13 +26,16 @@
|
||||
package org.springblade.transport.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
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.KingdeeInvoicePool;
|
||||
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
|
||||
import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -47,7 +50,43 @@ public interface IInvoiceReceiptService extends BaseService<InvoiceReceipt> {
|
||||
|
||||
InvoiceReceiptVO detail(Long id);
|
||||
|
||||
List<KingdeeInvoicePool> invoicePool(String keyword);
|
||||
/**
|
||||
* 金蝶票据池镜像统一查询(本地真分页,收票登记选票与正式结算单认领共用)。
|
||||
* <p>仅出可用票(剩余可用金额>0 且金额使用状态未用),认领/登记三态过滤在 SQL 内直出;
|
||||
* 票号精确查本地未命中时窄查一次 A801-1 回源入库后重查,兜底增量窗口缝隙内的当日新票。
|
||||
*
|
||||
* @param query 统一查询条件
|
||||
* @return 镜像统一视图分页
|
||||
*/
|
||||
IPage<KingdeeInvoicePoolVO> invoicePoolPage(KingdeeInvoicePoolQueryDTO query);
|
||||
|
||||
/**
|
||||
* 金蝶全票池定时同步(瘦身版):滚动增量窗口分页拉取 A801-1 + 镜像批量全列 upsert。
|
||||
* <p>无绑定回填/漏绑补偿/白名单(保存即写存根绑定,登记侧无需定时绑定);
|
||||
* single-flight 锁防多副本并发,手动触发与定时任务共用。
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String syncKingdeeInvoicePool();
|
||||
|
||||
/**
|
||||
* 结算单认领确认闸门:按票号集合逐张重查 A801-1 并按选票同口径复核可用性,
|
||||
* 选中到确认之间票被认领/使用/红冲在此拦截(与收票保存重查同闸门)
|
||||
*
|
||||
* @param invoiceNos 待认领发票号码集合
|
||||
*/
|
||||
void assertInvoicesClaimable(Collection<String> invoiceNos);
|
||||
|
||||
/**
|
||||
* 金蝶票池发票文件下载(A801-2 流式回源,前端查看/下载代理端点)。
|
||||
* <p>按发票号定位金蝶侧文件:本地镜像优先(票号同租户唯一,租户隔离由 MP 插件保证),
|
||||
* 镜像未落库或无文件时实时回源 A801-1 取 downloadUrl;经 A801-2 下载后流式透传浏览器,
|
||||
* 不落盘不进堆。写流前的业务失败以 JSON(R.fail)写入响应,前端按 Content-Type 识别提示。
|
||||
*
|
||||
* @param invoiceNo 发票号码
|
||||
* @param response 响应对象(文件流写出口)
|
||||
*/
|
||||
void downloadPoolAttachment(String invoiceNo, HttpServletResponse response);
|
||||
|
||||
List<Map<String, Object>> settlementCandidates(String keyword, Long receiptId);
|
||||
|
||||
@@ -65,5 +104,40 @@ public interface IInvoiceReceiptService extends BaseService<InvoiceReceipt> {
|
||||
|
||||
void voidBill(InvoiceReceiptStatusRequest request);
|
||||
|
||||
String syncKingdee(Long id);
|
||||
/**
|
||||
* 批量推送金蝶收票单(图05 环节②③:A303-2 收票单批量保存 + A302-6 收票指定应付)。
|
||||
* <p>逐单独立成败:前置校验(审批通过、已绑定镜像、查验通过、恰好分摊一张应付结算单)→
|
||||
* A102-4 预检财务应付单已生成 → A801-3 幂等预检(命中跳过保存续推指定)→ A303-2 保存 →
|
||||
* 立即 A302-6 指定应付;成功置 assigned 并锁单,失败置 failed 提示人工处理后重推。
|
||||
*
|
||||
* @param ids 收票登记id列表
|
||||
* @return 执行摘要(逐单失败原因)
|
||||
*/
|
||||
String pushKingdeeReceivedInvoice(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 自动推送路径批量推送金蝶收票单(审批通过事件 / 定时重推专用):
|
||||
* 前置拦截(未绑定镜像/查验未通过/前置应付单未生成等)一并置 failed + 原因,纳入定时重推循环
|
||||
*
|
||||
* @param ids 收票登记id列表
|
||||
* @param autoSync 自动推送标记(前置拦截置 failed)
|
||||
* @return 执行摘要(逐单失败原因)
|
||||
*/
|
||||
String pushKingdeeReceivedInvoice(List<Long> ids, boolean autoSync);
|
||||
|
||||
/**
|
||||
* 重推金蝶推送失败(kingdeePushStatus=failed 且审批通过)的收票登记:逐单独立成败,失败不中断。
|
||||
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String retryKingdeeFailed();
|
||||
|
||||
/**
|
||||
* 定时回写金蝶收票单结果(A801-3):金蝶侧删除/红冲/作废 → 回收为 failed 提示人工(不自动作废登记);
|
||||
* pushed(保存成功后中断)→ 指定补偿;assigned 仅补登缺失的收票单号
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String syncKingdeeReceivedInvoiceResult();
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import org.springblade.core.mp.base.BaseService;
|
||||
import org.springblade.transport.pojo.entity.KingdeeAttachmentSync;
|
||||
|
||||
/**
|
||||
* 金蝶附件同步服务
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface IKingdeeAttachmentSyncService extends BaseService<KingdeeAttachmentSync> {
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.springblade.core.mp.base.BaseEntity;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
|
||||
import org.springblade.transport.pojo.entity.KingdeeBillLock;
|
||||
import org.springblade.transport.pojo.vo.KingdeeUnlockMark;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定记录服务
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface IKingdeeBillLockService extends IService<KingdeeBillLock> {
|
||||
|
||||
/**
|
||||
* 推送金蝶成功后锁单(由各推送链路在主单事务提交后调用):
|
||||
* 每张金蝶目标单一行锁记录(先落 failed 再调 lockBusiData,成功翻 locked);
|
||||
* 锁单接口 1对1:逐张调用(单次一个源单号对一个金蝶单号),单张成败互不影响;
|
||||
* 已 locked 幂等跳过,failed 原地重锁并重置补偿计数;
|
||||
* 异常独立捕获,不影响主单同步状态
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型(SETTLEMENT/INVOICE_APPLY)
|
||||
* @param sourceBillId WLTMS 业务主键
|
||||
* @param sourceBillNo WLTMS 单号
|
||||
* @param targetBillType 金蝶目标单据类型
|
||||
* @param targetBillNos 金蝶单据编号集合(开票申请为多张)
|
||||
* @return 锁定失败的金蝶单号摘要(逗号拼接),全部锁定成功返回 null;调用方可拼入页面提示
|
||||
*/
|
||||
String lockAfterPush(String sourceBillType, Long sourceBillId, String sourceBillNo, String targetBillType, List<String> targetBillNos);
|
||||
|
||||
/**
|
||||
* 金蝶解锁回调(unLockNotifyBusiData):校验来源/目标系统与 lockAction。
|
||||
* 注意回调方向与锁定相反:回调 sourceBillNo=金蝶单号(即锁记录 targetBillNo,逗号拼接批量)、
|
||||
* targetBillNo=WLTMS 单号(即锁记录 sourceBillNo)、targetBillType 可能为空,
|
||||
* 故按金蝶单号+我方单号双条件反查最新 locked 锁记录;
|
||||
* operationType=DELETE 时按单据类型分发废除源单并还原关联数据
|
||||
*
|
||||
* @param param 金蝶回调参数
|
||||
* @return 处理单据数(应答 updateCount)
|
||||
*/
|
||||
int onUnlockNotify(BfiBillLockDTO param);
|
||||
|
||||
/**
|
||||
* 编辑守卫:源单存在 locked 锁记录时抛 ServiceException(金蝶锁定中不可修改)
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型
|
||||
* @param sourceBillId WLTMS 业务主键
|
||||
*/
|
||||
void checkEditable(String sourceBillType, Long sourceBillId);
|
||||
|
||||
/**
|
||||
* 批量查源单当前锁状态(供列表/详情页填充展示标识):
|
||||
* 每源单取最新一条非终态锁记录(locked/failed),unlocked 为终态不返回(前端视为无锁/已解锁)
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型
|
||||
* @param sourceBillIds 源单主键集合
|
||||
* @return sourceBillId -> 最新锁记录,无活跃锁的源单不在 Map 中
|
||||
*/
|
||||
Map<Long, KingdeeBillLock> mapActiveLocks(String sourceBillType, List<Long> sourceBillIds);
|
||||
|
||||
/**
|
||||
* 批量查源单解锁终态留痕(供列表/详情页填充解锁标识):
|
||||
* 每源单取最新一条 unlocked 锁记录(付款申请解锁后不作废,页面据此区分「金蝶删单/解锁」与普通推送失败)
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型
|
||||
* @param sourceBillIds 源单主键集合
|
||||
* @return sourceBillId -> 最新 unlocked 锁记录,无解锁留痕的源单不在 Map 中
|
||||
*/
|
||||
Map<Long, KingdeeBillLock> mapUnlockedLocks(String sourceBillType, List<Long> sourceBillIds);
|
||||
|
||||
/**
|
||||
* 批量填充金蝶解锁终态标识(通用):对无活跃锁的行反查最新 unlocked 锁记录并回填解锁标识
|
||||
* (解锁时间/操作人/动作名称),供前端展示明显解锁标识;有活跃锁的单必非解锁终态,跳过。
|
||||
* 与 mapActiveLocks 配对在各单据 fillKingdeeLockStatus 中调用,列表/详情共用;
|
||||
* 后续单据(如收票单)接入锁单时,VO 实现 KingdeeUnlockMark 即可复用
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型
|
||||
* @param rows 已填充活跃锁标识的 VO 行
|
||||
* @param activeLockMap 活跃锁 Map(mapActiveLocks 返回值)
|
||||
* @param <T> 实现 KingdeeUnlockMark 的 VO 类型(须继承 BaseEntity 以取主键)
|
||||
*/
|
||||
<T extends BaseEntity & KingdeeUnlockMark> void fillUnlockMarks(String sourceBillType, List<T> rows, Map<Long, KingdeeBillLock> activeLockMap);
|
||||
|
||||
/**
|
||||
* 锁定补偿:扫 failed 且 try_count 未达重试上限(RETRY_MAX_COUNT)的锁记录,逐张重调 lockBusiData
|
||||
* (接口 1对1:单次一个源单号对一个金蝶单号,单张成败互不影响;定时任务定时触发,亦可人工触发);
|
||||
* 成功翻 locked,失败 try_count+1 并记失败原因;
|
||||
* 计数烧满上限后停止自动补偿(人工重推「同步金蝶」会重置计数重新接管)。锁记录落库均为批量更新
|
||||
*
|
||||
* @return 补偿摘要(待补偿/成功张数),供调用方记日志或页面提示
|
||||
*/
|
||||
String retryFailedLocks();
|
||||
}
|
||||
+88
@@ -21,11 +21,14 @@ package org.springblade.transport.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import org.springblade.core.mp.base.BaseService;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO;
|
||||
import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest;
|
||||
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;
|
||||
|
||||
@@ -34,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);
|
||||
@@ -41,5 +49,85 @@ public interface IPaymentApplicationService extends BaseService<PaymentApplicati
|
||||
void returnBill(PaymentApplicationStatusRequest request);
|
||||
void voidBill(PaymentApplicationStatusRequest request);
|
||||
String syncKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 同步单笔付款申请到金蝶(携带批量共享上下文,消除重复档案查询),编排层无事务:
|
||||
* 单据级事务在 {@link #doSyncKingdee(Long, KingdeeSyncContext)} 中,失败状态落库在事务回滚后执行
|
||||
*
|
||||
* @param id 付款申请id
|
||||
* @param context 批量共享上下文(档案缓存),单笔调用传 {@link KingdeeSyncContext#empty()}
|
||||
* @return 金蝶单据id(存量数据兜底返回金蝶单据编号)
|
||||
*/
|
||||
String syncKingdee(Long id, KingdeeSyncContext context);
|
||||
|
||||
List<String> syncKingdeeBatch(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 单据级事务执行金蝶推送(由 syncKingdee 经自代理调用,保证整批同步不共用大事务)
|
||||
*
|
||||
* @param id 付款申请id
|
||||
* @param context 批量共享上下文(档案缓存)
|
||||
* @return 金蝶单据id
|
||||
*/
|
||||
String doSyncKingdee(Long id, KingdeeSyncContext context);
|
||||
|
||||
/**
|
||||
* 独立事务落"生成失败"状态(在推送事务回滚、行锁释放后执行);
|
||||
* 仅更新审批通过的单据,未审批/已删除单据自动跳过,保持原状态
|
||||
*
|
||||
* @param id 付款申请id
|
||||
*/
|
||||
void markKingdeeSyncFailed(Long id);
|
||||
|
||||
/**
|
||||
* 重推金蝶生成失败(kingdeeStatus=failed 且审批通过)的付款申请:逐单独立成败,失败不中断。
|
||||
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String retryKingdeeFailed();
|
||||
|
||||
/**
|
||||
* 金蝶单据解锁(ADR-0005):金蝶侧单据已被反审核删除时解除本地锁定——
|
||||
* 金蝶状态置生成失败、清空金蝶单据内码与编号(原值日志留档),不自动重推,等待人工核实后重新推送。
|
||||
* 触发来源:① B901 UNLOCK 回调(待建设);② A103-2 定时回写"已同步单据连续两轮查询消失"兜底。
|
||||
* 仅对已生成/付款中单据生效,终态(已付款/已关闭)与生成失败单据自动跳过;
|
||||
* 重推时 A103-2 勾稽预检兜底,误判场景(金蝶单据实际存在)自动回填不产生重复单据
|
||||
*
|
||||
* @param id 付款申请id
|
||||
* @param source 解锁来源标识(B901 / A103-2,用于日志追溯)
|
||||
*/
|
||||
void unlockBill(Long id, String source);
|
||||
|
||||
/**
|
||||
* 定时回写金蝶付款结果(A103-2 付款申请单查询 / A103-4 付款处理查询)
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String syncKingdeePaymentResult();
|
||||
|
||||
/**
|
||||
* 回写单笔付款申请的金蝶付款申请单查询结果(A103-2),单据级事务
|
||||
*
|
||||
* @param entity 付款申请实体
|
||||
* @param payApply A103-2 查询返回的付款申请单
|
||||
* @return 是否付款完成(paystatus = Alreadypay)
|
||||
*/
|
||||
boolean applyPayApplyResult(PaymentApplication entity, BfiPayApplyVO payApply);
|
||||
|
||||
/**
|
||||
* 回填单笔付款申请的金蝶付款处理记录(A103-4),单据级事务
|
||||
*
|
||||
* @param entity 付款申请实体
|
||||
* @param payBill A103-4 查询返回的付款处理单
|
||||
*/
|
||||
void applyPayBillRecord(PaymentApplication entity, BfiPayBillVO payBill);
|
||||
|
||||
/**
|
||||
* 付款方式是否为汇票付款(按业务字典 pay_method 条目文本匹配"汇票")
|
||||
*
|
||||
* @param paymentMethod 付款方式(字典键或值)
|
||||
* @return true 表示汇票类付款
|
||||
*/
|
||||
boolean isBillPayment(String paymentMethod);
|
||||
}
|
||||
|
||||
+8
-1
@@ -45,5 +45,12 @@ public interface IReceiptClaimRecordService extends BaseService<ReceiptClaim> {
|
||||
|
||||
void updateAttachments(ReceiptClaimAttachmentsRequest request);
|
||||
|
||||
String voidClaim(Long id);
|
||||
void voidClaim(Long id);
|
||||
|
||||
/**
|
||||
* A301-1 收款单查询回写认领结果:按来源单据编码匹配金蝶收款单,推进金蝶单据状态
|
||||
*
|
||||
* @return 回写条数
|
||||
*/
|
||||
int refreshKingdeeResult();
|
||||
}
|
||||
|
||||
+23
-2
@@ -27,8 +27,8 @@ package org.springblade.transport.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import org.springblade.core.mp.base.BaseService;
|
||||
import org.springblade.transport.pojo.dto.ReceiptClaimPushResult;
|
||||
import org.springblade.transport.pojo.dto.ReceiptClaimRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceiptFlowSyncRequest;
|
||||
import org.springblade.transport.pojo.entity.KingdeeReceiptFlow;
|
||||
import org.springblade.transport.pojo.vo.ReceiptFlowVO;
|
||||
|
||||
@@ -52,5 +52,26 @@ public interface IReceiptFlowService extends BaseService<KingdeeReceiptFlow> {
|
||||
|
||||
Long claim(ReceiptClaimRequest request);
|
||||
|
||||
int sync(ReceiptFlowSyncRequest request);
|
||||
/**
|
||||
* 手动同步金蝶收款流水:直连 A301-2 收款认领公告查询,按明细流水号幂等 upsert 收款流水表
|
||||
*
|
||||
* @return 本次同步条数
|
||||
*/
|
||||
int sync();
|
||||
|
||||
/**
|
||||
* 手动推送金蝶(A301-3 收款认领结果推送):认领完成且未推送过的流水才可推送,
|
||||
* 推送前以当前用户姓名重查 A301-2 最新公告对账。与定时任务共用同一推送实现。
|
||||
*
|
||||
* @param flowId 收款流水id
|
||||
* @return 推送结果(失败不抛异常,原因随结果返回)
|
||||
*/
|
||||
ReceiptClaimPushResult pushClaimToKingdee(Long flowId);
|
||||
|
||||
/**
|
||||
* 定时推送金蝶:扫描认领完成且未推送过的流水,按认领人姓名分组重查 A301-2 最新公告后逐条推送。
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String pushClaimByTask();
|
||||
}
|
||||
|
||||
+17
-1
@@ -70,7 +70,8 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
|
||||
|
||||
void transferSettlement(ReceivablePayableTransferRequest request);
|
||||
|
||||
IPage<Map<String, Object>> transferCandidates(IPage<?> page, String contractName, String batchNo,
|
||||
IPage<Map<String, Object>> transferCandidates(IPage<?> page, String documentNo, String contractNo,
|
||||
String contractName, String batchNo,
|
||||
String generateStartDate, String generateEndDate, String settlementBillType,
|
||||
String settlementType);
|
||||
|
||||
@@ -100,4 +101,19 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
|
||||
|
||||
/** 关闭总单调度后生成总单客户合同应收,并按所属运单记录的承运商合同生成应付。 */
|
||||
void generateForClosedMasterOrder(MasterOrder masterOrder);
|
||||
|
||||
/**
|
||||
* 结算导入:按导入单据构建/刷新「结算导入」来源的应收应付明细(运输费+费用项金额以导入数据为准)。
|
||||
* <p>
|
||||
* 历史运单若已有系统生成明细,不覆盖原明细,另建结算导入明细;运单已进入结算则拒绝重复导入。
|
||||
* feeRows 每项含 waybillId、transportFee、unitPrice、feeItems(Map)、remark。
|
||||
* </p>
|
||||
*
|
||||
* @param settlementType 结算类型 receivable/payable
|
||||
* @param contractId 结算合同ID
|
||||
* @param feeRows 费用行
|
||||
* @return 明细主键列表(新建或刷新后的结算导入明细)
|
||||
*/
|
||||
List<Long> createDetailsFromSettlementImport(String settlementType, Long contractId,
|
||||
List<Map<String, Object>> feeRows);
|
||||
}
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springblade.core.mp.base.BaseService;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportBatchRequest;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportVoidRequest;
|
||||
import org.springblade.transport.pojo.entity.SettlementImportBatch;
|
||||
import org.springblade.transport.pojo.vo.SettlementImportBatchVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 导入结算单批次服务
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface ISettlementImportBatchService extends BaseService<SettlementImportBatch> {
|
||||
|
||||
Map<String, List<Map<String, Object>>> options();
|
||||
|
||||
String nextBatchNo();
|
||||
|
||||
IPage<SettlementImportBatchVO> page(IPage<SettlementImportBatch> page, SettlementImportBatchRequest request);
|
||||
|
||||
SettlementImportBatchVO detail(Long id);
|
||||
|
||||
void exportTemplate(HttpServletResponse response);
|
||||
|
||||
Map<String, Object> parse(MultipartFile file);
|
||||
|
||||
WaybillImportValidateResultVO validate(SettlementImportBatchRequest request);
|
||||
|
||||
void exportFailures(SettlementImportBatchRequest request, MultipartFile file, HttpServletResponse response);
|
||||
|
||||
SettlementImportBatchVO saveDraft(SettlementImportBatchRequest request);
|
||||
|
||||
SettlementImportBatchVO confirm(SettlementImportBatchRequest request);
|
||||
|
||||
void voidBatch(SettlementImportVoidRequest request);
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 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);
|
||||
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.CustomerReceiptAccount;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
|
||||
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 金蝶同步上下文:批量同步时全批共享的档案缓存,消除逐单重复的远程/数据库查询(N+1)。
|
||||
* <p>单笔同步使用 {@link #empty()},各缓存为空时自动回退为逐单查询,行为与原逻辑一致。</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public record KingdeeSyncContext(Map<Long, ContractManage> contractCache,
|
||||
Map<String, String> customerCodeCache, Map<Long, ProjectApply> projectCache,
|
||||
Map<String, String> apBillNoCache,
|
||||
Map<Long, List<FormalSettlementDetail>> settlementDetailCache,
|
||||
Map<Long, FormalSettlement> settlementCache,
|
||||
Map<Long, CustomerReceiptAccount> receiptAccountCache,
|
||||
Map<String, String> feeCategoryByName) {
|
||||
|
||||
/** 空上下文:缓存可写但初始为空,首条记录命中后全批复用 */
|
||||
public static KingdeeSyncContext empty() {
|
||||
return new KingdeeSyncContext(new HashMap<>(), new HashMap<>(), new HashMap<>(),
|
||||
new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
|
||||
}
|
||||
}
|
||||
+15
-19
@@ -35,6 +35,7 @@ import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.SysCache;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.mapper.BillLedgerMapper;
|
||||
import org.springblade.transport.mapper.BillLedgerUsageMapper;
|
||||
import org.springblade.transport.mapper.BillPaymentMapper;
|
||||
@@ -61,11 +62,6 @@ import java.util.Objects;
|
||||
@RequiredArgsConstructor
|
||||
public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, BillPayment>
|
||||
implements IBillPaymentService {
|
||||
private static final String DRAFT = "draft";
|
||||
private static final String REVIEWING = "reviewing";
|
||||
private static final String APPROVED = "approved";
|
||||
private static final String RETURNED = "returned";
|
||||
private static final String VOIDED = "voided";
|
||||
private static final String RELEASED = "released";
|
||||
|
||||
private final BillLedgerMapper billLedgerMapper;
|
||||
@@ -103,7 +99,7 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
|
||||
public Long saveDraft(BillPaymentSaveRequest request) {
|
||||
validateRequest(request);
|
||||
BillPayment entity = request.getId() == null ? new BillPayment() : locked(request.getId());
|
||||
if (entity.getId() != null && !List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) {
|
||||
if (entity.getId() != null && !List.of(TransportBusiConstant.DRAFT, TransportBusiConstant.RETURNED).contains(entity.getApprovalStatus())) {
|
||||
throw new ServiceException("当前状态不可编辑");
|
||||
}
|
||||
Long deptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
@@ -119,7 +115,7 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
|
||||
validateLedgerAmount(ledger, usedAmount, deptId);
|
||||
if (entity.getId() == null) {
|
||||
entity.setPaymentNo(nextNo());
|
||||
entity.setApprovalStatus(DRAFT);
|
||||
entity.setApprovalStatus(TransportBusiConstant.DRAFT);
|
||||
entity.setCurrentNode("草稿");
|
||||
}
|
||||
entity.setBillLedgerId(ledger.getId());
|
||||
@@ -141,7 +137,7 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void removeDraft(Long id) {
|
||||
BillPayment entity = locked(id);
|
||||
if (!DRAFT.equals(entity.getApprovalStatus())) {
|
||||
if (!TransportBusiConstant.DRAFT.equals(entity.getApprovalStatus())) {
|
||||
throw new ServiceException("仅草稿状态的汇票付款允许删除");
|
||||
}
|
||||
removeById(entity);
|
||||
@@ -151,11 +147,11 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void submit(BillPaymentStatusRequest request) {
|
||||
BillPayment entity = locked(requiredId(request));
|
||||
if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) {
|
||||
if (!List.of(TransportBusiConstant.DRAFT, TransportBusiConstant.RETURNED).contains(entity.getApprovalStatus())) {
|
||||
throw new ServiceException("当前状态不允许提交");
|
||||
}
|
||||
validateStoredAmount(entity);
|
||||
entity.setApprovalStatus(REVIEWING);
|
||||
entity.setApprovalStatus(TransportBusiConstant.REVIEWING);
|
||||
entity.setCurrentNode("财务审核");
|
||||
entity.setCurrentProcessor(AuthUtil.getUserName());
|
||||
updateById(entity);
|
||||
@@ -165,14 +161,14 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void approve(BillPaymentStatusRequest request) {
|
||||
BillPayment entity = locked(requiredId(request));
|
||||
if (!REVIEWING.equals(entity.getApprovalStatus())) {
|
||||
if (!TransportBusiConstant.REVIEWING.equals(entity.getApprovalStatus())) {
|
||||
throw new ServiceException("仅审批中的汇票付款允许审核");
|
||||
}
|
||||
BillLedger ledger = lockedBill(entity.getBillLedgerId());
|
||||
validateLedgerAmount(ledger, entity.getUsedAmount(), entity.getDeptId());
|
||||
BillLedgerUsage exists = usageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
|
||||
.eq(BillLedgerUsage::getBillPaymentId, entity.getId())
|
||||
.eq(BillLedgerUsage::getUsageStatus, APPROVED)
|
||||
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED)
|
||||
.last("FOR UPDATE"));
|
||||
if (exists != null) {
|
||||
throw new ServiceException("该汇票付款已生成使用记录");
|
||||
@@ -188,11 +184,11 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
|
||||
usage.setUsedAmount(usedAmount);
|
||||
usage.setUseDeptId(entity.getDeptId());
|
||||
usage.setUseDeptName(entity.getDeptName());
|
||||
usage.setUsageStatus(APPROVED);
|
||||
usage.setUsageStatus(TransportBusiConstant.APPROVED);
|
||||
usage.setStatus(1);
|
||||
usageMapper.insert(usage);
|
||||
entity.setAvailableBalance(ledger.getAvailableBalance());
|
||||
entity.setApprovalStatus(APPROVED);
|
||||
entity.setApprovalStatus(TransportBusiConstant.APPROVED);
|
||||
entity.setCurrentNode("审批通过");
|
||||
entity.setCurrentProcessor(AuthUtil.getUserName());
|
||||
updateById(entity);
|
||||
@@ -202,10 +198,10 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void returnBill(BillPaymentStatusRequest request) {
|
||||
BillPayment entity = locked(requiredId(request));
|
||||
if (!REVIEWING.equals(entity.getApprovalStatus())) {
|
||||
if (!TransportBusiConstant.REVIEWING.equals(entity.getApprovalStatus())) {
|
||||
throw new ServiceException("仅审批中的汇票付款允许驳回");
|
||||
}
|
||||
entity.setApprovalStatus(RETURNED);
|
||||
entity.setApprovalStatus(TransportBusiConstant.RETURNED);
|
||||
entity.setCurrentNode("已驳回");
|
||||
entity.setCurrentProcessor(AuthUtil.getUserName());
|
||||
entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200, "驳回原因"));
|
||||
@@ -216,12 +212,12 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void voidBill(BillPaymentStatusRequest request) {
|
||||
BillPayment entity = locked(requiredId(request));
|
||||
if (!APPROVED.equals(entity.getApprovalStatus())) {
|
||||
if (!TransportBusiConstant.APPROVED.equals(entity.getApprovalStatus())) {
|
||||
throw new ServiceException("仅审批通过的汇票付款允许作废");
|
||||
}
|
||||
BillLedgerUsage usage = usageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
|
||||
.eq(BillLedgerUsage::getBillPaymentId, entity.getId())
|
||||
.eq(BillLedgerUsage::getUsageStatus, APPROVED)
|
||||
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED)
|
||||
.last("FOR UPDATE"));
|
||||
if (usage == null) {
|
||||
throw new ServiceException("未找到汇票使用记录,无法作废");
|
||||
@@ -233,7 +229,7 @@ public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, B
|
||||
usage.setUsageStatus(RELEASED);
|
||||
usageMapper.updateById(usage);
|
||||
entity.setAvailableBalance(ledger.getAvailableBalance());
|
||||
entity.setApprovalStatus(VOIDED);
|
||||
entity.setApprovalStatus(TransportBusiConstant.VOIDED);
|
||||
entity.setCurrentNode("已作废");
|
||||
entity.setCurrentProcessor(AuthUtil.getUserName());
|
||||
entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200, "作废原因"));
|
||||
|
||||
+23
-2
@@ -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) {
|
||||
|
||||
+4
-2
@@ -437,9 +437,11 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
} else if (Func.isNotEmpty(contractManage.getOrganizationId())) {
|
||||
queryWrapper.eq(ContractManage::getOrganizationId, contractManage.getOrganizationId());
|
||||
}
|
||||
queryWrapper.like(Func.isNotEmpty(contractManage.getContractNo()), ContractManage::getContractNo, contractManage.getContractNo())
|
||||
queryWrapper.eq(Func.isNotEmpty(contractManage.getProjectId()), ContractManage::getProjectId, contractManage.getProjectId())
|
||||
.like(Func.isNotEmpty(contractManage.getContractNo()), ContractManage::getContractNo, contractManage.getContractNo())
|
||||
.like(Func.isNotEmpty(contractManage.getContractName()), ContractManage::getContractName, contractManage.getContractName())
|
||||
.like(Func.isNotEmpty(contractManage.getProjectName()), ContractManage::getProjectName, contractManage.getProjectName())
|
||||
.like(Func.isNotEmpty(contractManage.getProjectName()) && Func.isEmpty(contractManage.getProjectId()),
|
||||
ContractManage::getProjectName, contractManage.getProjectName())
|
||||
.like(Func.isNotEmpty(contractManage.getOrganizationName()), ContractManage::getOrganizationName, contractManage.getOrganizationName())
|
||||
.eq(Func.isNotEmpty(contractManage.getContractCategory()), ContractManage::getContractCategory, contractManage.getContractCategory())
|
||||
.eq(Func.isNotEmpty(contractManage.getSignType()), ContractManage::getSignType, contractManage.getSignType())
|
||||
|
||||
+601
-15
@@ -28,20 +28,34 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
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;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
import org.springblade.transport.mk.MkProcessNodeHelper;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.pojo.entity.UserCustomerScope;
|
||||
import org.springblade.thirdparty.bfi.constant.BfiConstant;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultErrorVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBlacklistVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiCustomerVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springblade.thirdparty.bfi.util.BfiUtil;
|
||||
import org.springblade.system.cache.SysCache;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.excel.CustomerArchiveExcel;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.mapper.CreditRatingStandardMapper;
|
||||
import org.springblade.transport.mapper.CreditScoreCategoryMapper;
|
||||
import org.springblade.transport.mapper.CreditScoreItemMapper;
|
||||
@@ -71,18 +85,19 @@ import org.springblade.transport.pojo.entity.CustomerInvoiceContact;
|
||||
import org.springblade.transport.pojo.entity.CustomerInvoiceInfo;
|
||||
import org.springblade.transport.pojo.entity.CustomerReceiptAccount;
|
||||
import org.springblade.transport.pojo.entity.PaymentApplication;
|
||||
import org.springblade.transport.pojo.vo.CreditRatingStandardVO;
|
||||
import org.springblade.transport.pojo.vo.CustomerArchiveVO;
|
||||
import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
|
||||
import org.springblade.transport.pojo.vo.CustomerContactVO;
|
||||
import org.springblade.transport.pojo.vo.CustomerCreditScoreDetailVO;
|
||||
import org.springblade.transport.pojo.vo.CustomerCreditScoreVO;
|
||||
import org.springblade.transport.pojo.vo.CreditRatingStandardVO;
|
||||
import org.springblade.transport.pojo.vo.CustomerInvoiceContactVO;
|
||||
import org.springblade.transport.pojo.vo.CustomerInvoiceInfoVO;
|
||||
import org.springblade.transport.pojo.vo.CustomerReceiptAccountVO;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
import org.springblade.system.pojo.entity.UserCustomerScope;
|
||||
import org.springblade.transport.support.KingdeeSyncLockSupport;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -101,6 +116,7 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 客商档案 服务实现类
|
||||
@@ -112,17 +128,76 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveMapper, CustomerArchive> implements ICustomerArchiveService {
|
||||
|
||||
/**
|
||||
* 状态:启用
|
||||
*/
|
||||
private static final int STATUS_ENABLED = 1;
|
||||
|
||||
/**
|
||||
* 状态:停用
|
||||
*/
|
||||
private static final int STATUS_DISABLED = 2;
|
||||
|
||||
/**
|
||||
* 准入类型:临时
|
||||
*/
|
||||
private static final String ACCESS_TEMPORARY = "temporary";
|
||||
|
||||
/**
|
||||
* 准入类型:正式
|
||||
*/
|
||||
private static final String ACCESS_FORMAL = "formal";
|
||||
|
||||
/**
|
||||
* 审批状态:草稿
|
||||
*/
|
||||
private static final String APPROVAL_DRAFT = "draft";
|
||||
|
||||
/**
|
||||
* 审批状态:审核中
|
||||
*/
|
||||
private static final String APPROVAL_REVIEWING = "reviewing";
|
||||
|
||||
/**
|
||||
* 审批状态:审批通过
|
||||
*/
|
||||
private static final String APPROVAL_APPROVED = "approved";
|
||||
|
||||
/**
|
||||
* 审批状态:审批驳回
|
||||
*/
|
||||
private static final String APPROVAL_REJECTED = "rejected";
|
||||
/** 客商所属组织固定为顶层组织 */
|
||||
private static final String FIXED_OWNER_DEPT_NAME = "桂物物流集团";
|
||||
private static final String FIXED_OWNER_DEPT_NAME = "广西现代物流集团有限公司";
|
||||
|
||||
/**
|
||||
* 金蝶协同状态:未推送(新增客商默认值,审批通过后参与推送)
|
||||
*/
|
||||
private static final String KINGDEE_STATUS_NONE = "none";
|
||||
|
||||
/**
|
||||
* 金蝶协同状态:金蝶已有档案(A902-1 查到已审核,审批通过后跳过推送,终态)
|
||||
*/
|
||||
private static final String KINGDEE_STATUS_EXIST = "exist";
|
||||
|
||||
/**
|
||||
* 金蝶协同状态:已推送成功(终态,重推时幂等跳过)
|
||||
*/
|
||||
private static final String KINGDEE_STATUS_SYNCED = "synced";
|
||||
|
||||
/**
|
||||
* 金蝶协同状态:推送失败 / 金蝶审核中(可列表「同步金蝶」人工重推)
|
||||
*/
|
||||
private static final String KINGDEE_STATUS_FAILED = "failed";
|
||||
|
||||
/**
|
||||
* 客商编号前缀
|
||||
*/
|
||||
private static final String CUSTOMER_CODE_PREFIX = "KS";
|
||||
/**
|
||||
* A902-1 批量查询分批大小
|
||||
*/
|
||||
private static final int KINGDEE_CUSTOMER_QUERY_BATCH = 50;
|
||||
/**
|
||||
* 变更快照忽略字段:明细每次保存会重建主键/审计字段,不能作为“是否有修改”的依据
|
||||
*/
|
||||
@@ -147,6 +222,13 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
private final CreditRatingStandardMapper standardMapper;
|
||||
private final UserCustomerScopeMapper userCustomerScopeMapper;
|
||||
private final PaymentApplicationMapper paymentApplicationMapper;
|
||||
private final IBfiService bfiService;
|
||||
private final CustomerBfiConverter customerBfiConverter;
|
||||
private final KingdeeSyncLockSupport kingdeeSyncLockSupport;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
/** 金蝶客商数据同步 single-flight 锁键前缀(多副本定时任务与手动触发共用) */
|
||||
private static final String CUSTOMER_SYNC_LOCK_PREFIX = "transport:customer-archive:kingdee-customer-sync:";
|
||||
|
||||
@Override
|
||||
public IPage<CustomerArchiveVO> selectCustomerArchivePage(IPage<CustomerArchiveVO> page, CustomerArchiveVO customer) {
|
||||
@@ -222,6 +304,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
validateBase(detail);
|
||||
validateScoreForApproval(detail);
|
||||
validateApproval(detail);
|
||||
validateBlacklist(detail);
|
||||
CustomerArchive before = Objects.requireNonNull(BeanUtil.copyProperties(detail, CustomerArchive.class));
|
||||
CustomerArchive after = copyCustomer(before);
|
||||
after.setApprovalStatus(APPROVAL_REVIEWING);
|
||||
@@ -417,6 +500,411 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
return updateById(after);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CustomerArchive syncKingdee(Long id) {
|
||||
CustomerArchive customer = ensureCustomerAccessible(id);
|
||||
if (!Objects.equals(customer.getApprovalStatus(), APPROVAL_APPROVED)) {
|
||||
throw new ServiceException("仅审批通过的客商允许推送金蝶");
|
||||
}
|
||||
if (List.of(KINGDEE_STATUS_SYNCED, KINGDEE_STATUS_EXIST).contains(customer.getKingdeeStatus())) {
|
||||
// 幂等:已推送或金蝶已有档案,直接返回
|
||||
return customer;
|
||||
}
|
||||
CustomerArchive after = copyCustomer(customer);
|
||||
after.setKingdeeSyncTime(LocalDateTime.now());
|
||||
BfiCustomerVO kingdeeCustomer = queryKingdeeCustomer(customer.getUnifiedCreditCode());
|
||||
if (kingdeeCustomer != null && BfiConstant.BILL_STATUS_AUDITED.equals(kingdeeCustomer.getStatus())) {
|
||||
// 金蝶已有已审核档案:跳过推送
|
||||
after.setKingdeeStatus(KINGDEE_STATUS_EXIST);
|
||||
after.setKingdeeFailReason(null);
|
||||
addChangeRecord(id, "金蝶已存在该客商档案,跳过推送", customer, after);
|
||||
updateById(after);
|
||||
return after;
|
||||
}
|
||||
if (kingdeeCustomer != null) {
|
||||
// 金蝶侧暂存/已提交:审核中,置失败待重推
|
||||
after.setKingdeeStatus(KINGDEE_STATUS_FAILED);
|
||||
after.setKingdeeFailReason("金蝶侧该客商审核中,请稍后重推");
|
||||
addChangeRecord(id, "推送金蝶失败:金蝶侧客商审核中", customer, after);
|
||||
updateById(after);
|
||||
return after;
|
||||
}
|
||||
try {
|
||||
pushCustomerToKingdee(id, customer, after, detail(id));
|
||||
} catch (Exception exception) {
|
||||
// 推送事务已回滚,此处独立事务落"推送失败"状态与原因,供页面展示与手动重推(与结算/付款/开票口径一致)
|
||||
self().markKingdeeSyncFailed(id, exception.getMessage());
|
||||
throw exception;
|
||||
}
|
||||
updateById(after);
|
||||
return after;
|
||||
}
|
||||
|
||||
/** 自代理:跨方法调用确保事务注解生效(失败状态独立事务) */
|
||||
private ICustomerArchiveService self() {
|
||||
return SpringUtil.getBean(ICustomerArchiveService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
|
||||
public void markKingdeeSyncFailed(Long id, String reason) {
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
String failReason = truncateText(Func.toStrWithEmpty(reason, "金蝶接口调用异常"));
|
||||
CustomerArchive before = getById(id);
|
||||
// 条件更新:仅审批通过且未推送/推送失败的客商落失败状态,已推送/金蝶已有自动跳过保持原状态
|
||||
int updated = baseMapper.update(null, Wrappers.<CustomerArchive>lambdaUpdate()
|
||||
.eq(CustomerArchive::getId, id)
|
||||
.eq(CustomerArchive::getApprovalStatus, APPROVAL_APPROVED)
|
||||
.and(wrapper -> wrapper.isNull(CustomerArchive::getKingdeeStatus)
|
||||
.or()
|
||||
.notIn(CustomerArchive::getKingdeeStatus, List.of(KINGDEE_STATUS_SYNCED, KINGDEE_STATUS_EXIST)))
|
||||
.set(CustomerArchive::getKingdeeStatus, KINGDEE_STATUS_FAILED)
|
||||
.set(CustomerArchive::getKingdeeFailReason, failReason));
|
||||
if (updated > 0 && before != null) {
|
||||
// 变更记录留档(推送主事务已回滚,此处独立事务落状态与记录)
|
||||
CustomerArchive after = copyCustomer(before);
|
||||
after.setKingdeeStatus(KINGDEE_STATUS_FAILED);
|
||||
after.setKingdeeFailReason(failReason);
|
||||
addChangeRecord(id, "推送金蝶失败:" + failReason, before, after);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 A902-2 报文推送金蝶并回写协同状态;detail 由调用方准备,
|
||||
* 定时同步批量组装报文数据,避免逐条 detail 查询的 N+1 问题;
|
||||
* 推送失败先落 failed 状态与变更记录,再抛出异常由调用方提示前端(与结算/付款/开票口径一致)
|
||||
*/
|
||||
private void pushCustomerToKingdee(Long id, CustomerArchive customer, CustomerArchive after, CustomerArchiveVO detail) {
|
||||
try {
|
||||
BfiBillSaveResultVO result = bfiService.saveCustomer(customerBfiConverter.buildSaveDTO(detail));
|
||||
BfiBillSaveResultItemVO item = firstSaveItem(result);
|
||||
if (item != null && Boolean.TRUE.equals(item.getBillStatus())) {
|
||||
after.setKingdeeStatus(KINGDEE_STATUS_SYNCED);
|
||||
after.setKingdeeFailReason(null);
|
||||
addChangeRecord(id, "推送金蝶成功,金蝶客户编码与客商编号一致:" + customer.getCustomerCode(), customer, after);
|
||||
} else {
|
||||
after.setKingdeeStatus(KINGDEE_STATUS_FAILED);
|
||||
after.setKingdeeFailReason(buildSaveFailReason(result, item));
|
||||
addChangeRecord(id, "推送金蝶失败:" + after.getKingdeeFailReason(), customer, after);
|
||||
throw new ServiceException("推送金蝶失败:" + after.getKingdeeFailReason());
|
||||
}
|
||||
} catch (ServiceException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
log.error("客商推送金蝶失败,id={},unifiedCreditCode={}", id, customer.getUnifiedCreditCode(), exception);
|
||||
after.setKingdeeStatus(KINGDEE_STATUS_FAILED);
|
||||
after.setKingdeeFailReason(truncateText(exception.getMessage() == null ? "金蝶接口调用异常" : exception.getMessage()));
|
||||
addChangeRecord(id, "推送金蝶失败:" + after.getKingdeeFailReason(), customer, after);
|
||||
throw exception instanceof RuntimeException runtimeException
|
||||
? runtimeException
|
||||
: new ServiceException("推送金蝶失败:" + after.getKingdeeFailReason());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A902-1 按统一社会信用代码查询金蝶客商,无数据返回 null
|
||||
*/
|
||||
private BfiCustomerVO queryKingdeeCustomer(String unifiedCreditCode) {
|
||||
BfiCustomerQueryDTO param = new BfiCustomerQueryDTO();
|
||||
param.setPageNo(1);
|
||||
param.setPageSize(1);
|
||||
param.setSocietycreditcode(List.of(unifiedCreditCode));
|
||||
BfiPageVO<BfiCustomerVO> page = bfiService.queryCustomer(param);
|
||||
List<BfiCustomerVO> rows = page == null ? null : page.getRows();
|
||||
return rows == null || rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
|
||||
private BfiBillSaveResultItemVO firstSaveItem(BfiBillSaveResultVO result) {
|
||||
if (result == null || result.getResult() == null || result.getResult().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return result.getResult().get(0);
|
||||
}
|
||||
|
||||
private String buildSaveFailReason(BfiBillSaveResultVO result, BfiBillSaveResultItemVO item) {
|
||||
if (item != null && Func.isNotEmpty(item.getErrors())) {
|
||||
String messages = item.getErrors().stream()
|
||||
.map(BfiBillSaveResultErrorVO::getRowMsg)
|
||||
.filter(Func::isNotEmpty)
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.joining(";"));
|
||||
if (Func.isEmpty(messages)) {
|
||||
messages = item.getErrors().stream()
|
||||
.map(error -> String.valueOf(error.getEntityKey()))
|
||||
.collect(Collectors.joining(";"));
|
||||
}
|
||||
return truncateText(messages);
|
||||
}
|
||||
if (result != null && Func.isNotEmpty(result.getFailCount())) {
|
||||
return truncateText("金蝶返回失败数量:" + result.getFailCount());
|
||||
}
|
||||
return "金蝶返回结果为空";
|
||||
}
|
||||
|
||||
private String truncateText(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
return text.length() > 500 ? text.substring(0, 500) : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时同步推送报文组装:主表字段 + 批量查询的收款账户,不含联系人/发票/评分等推送无关明细,
|
||||
* 替代逐条 detail 查询,避免 N+1
|
||||
*/
|
||||
private CustomerArchiveVO buildSyncArchiveVO(CustomerArchive customer, List<CustomerReceiptAccountVO> receiptAccounts) {
|
||||
CustomerArchiveVO detail = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchiveVO.class));
|
||||
detail.setReceiptAccounts(receiptAccounts);
|
||||
return detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时同步变更集中落库:档案批量更新 + 变更记录批量插入,失败仅记录日志,不影响汇总返回
|
||||
*/
|
||||
private void flushSyncChanges(List<CustomerArchive> updateList, List<CustomerChangeRecord> changeRecords) {
|
||||
if (!updateList.isEmpty()) {
|
||||
try {
|
||||
updateBatchById(updateList);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时同步金蝶客商批量更新档案异常,size={}", updateList.size(), exception);
|
||||
}
|
||||
}
|
||||
if (!changeRecords.isEmpty()) {
|
||||
try {
|
||||
Db.saveBatch(changeRecords);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时同步金蝶客商批量写入变更记录异常,size={}", changeRecords.size(), exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String syncKingdeeCustomerData() {
|
||||
return kingdeeSyncLockSupport.runSingleFlight(CUSTOMER_SYNC_LOCK_PREFIX, bfiProperties.getCustomer().getLockLeaseSeconds(),
|
||||
"金蝶客商数据同步", this::executeCustomerDataSync);
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶客商数据同步主体:A902-1 复查 + A902-2 推送 + 金蝶禁用联动停用客商档案,
|
||||
* 由 {@link #syncKingdeeCustomerData()} 在分布式锁保护下调用
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
private String executeCustomerDataSync() {
|
||||
log.info("金蝶客商数据定时同步开始");
|
||||
// 客商数据量不大,一次查询审批通过客商,内存按金蝶协同状态分组
|
||||
List<CustomerArchive> approvedList = list(Wrappers.<CustomerArchive>lambdaQuery()
|
||||
.eq(CustomerArchive::getApprovalStatus, APPROVAL_APPROVED)
|
||||
.orderByAsc(CustomerArchive::getId));
|
||||
// ① 待推送客商:未推送/推送失败,单条失败不中断批次
|
||||
List<CustomerArchive> pendingList = approvedList.stream()
|
||||
.filter(customer -> KINGDEE_STATUS_NONE.equals(customer.getKingdeeStatus())
|
||||
|| KINGDEE_STATUS_FAILED.equals(customer.getKingdeeStatus()))
|
||||
.toList();
|
||||
// ② 停用联动候选:已同步/金蝶已有且启用的客商
|
||||
List<CustomerArchive> enabledList = approvedList.stream()
|
||||
.filter(customer -> (KINGDEE_STATUS_SYNCED.equals(customer.getKingdeeStatus())
|
||||
|| KINGDEE_STATUS_EXIST.equals(customer.getKingdeeStatus()))
|
||||
&& Objects.equals(customer.getStatus(), STATUS_ENABLED))
|
||||
.toList();
|
||||
// ③ 已推送客商:A902-2 成功仅代表金蝶受理(status=B),复查 status=C 已审核才是最终成功
|
||||
List<CustomerArchive> syncedList = approvedList.stream()
|
||||
.filter(customer -> KINGDEE_STATUS_SYNCED.equals(customer.getKingdeeStatus()))
|
||||
.toList();
|
||||
log.info("金蝶客商数据定时同步查询到审批通过客商 {} 条(待推送 {},已推送 {},可停用联动 {})",
|
||||
approvedList.size(), pendingList.size(), syncedList.size(), enabledList.size());
|
||||
int pushed = 0;
|
||||
int existed = 0;
|
||||
int pushFailed = 0;
|
||||
// 变更集中落库:档案批量更新、变更记录批量插入,避免逐条 update/insert 的 N+1 写入
|
||||
List<CustomerArchive> updateList = new ArrayList<>();
|
||||
List<CustomerChangeRecord> changeRecords = new ArrayList<>();
|
||||
// 批量 A902-1 复查待推送客商:金蝶已审核直接建档(exist)、审核中置 failed,仅不存在金蝶档案的才逐条推送,避免逐条复查的 N+1 调用
|
||||
Map<String, BfiCustomerVO> pendingKingdeeMap = queryKingdeeCustomerMap(pendingList.stream()
|
||||
.map(CustomerArchive::getUnifiedCreditCode)
|
||||
.filter(Func::isNotEmpty)
|
||||
.map(code -> code.trim().toUpperCase())
|
||||
.distinct()
|
||||
.toList());
|
||||
List<CustomerArchive> pushList = new ArrayList<>();
|
||||
for (CustomerArchive customer : pendingList) {
|
||||
BfiCustomerVO kingdeeCustomer = pendingKingdeeMap.get(customer.getUnifiedCreditCode().trim().toUpperCase());
|
||||
if (kingdeeCustomer == null) {
|
||||
pushList.add(customer);
|
||||
continue;
|
||||
}
|
||||
CustomerArchive after = copyCustomer(customer);
|
||||
after.setKingdeeSyncTime(LocalDateTime.now());
|
||||
if (BfiConstant.BILL_STATUS_AUDITED.equals(kingdeeCustomer.getStatus())) {
|
||||
// 金蝶已存在已审核档案:跳过推送,直接建档
|
||||
after.setKingdeeStatus(KINGDEE_STATUS_EXIST);
|
||||
after.setKingdeeFailReason(null);
|
||||
existed++;
|
||||
collectChangeRecord(changeRecords, customer.getId(), "金蝶已存在该客商档案,跳过推送", customer, after);
|
||||
} else {
|
||||
// 金蝶侧暂存/已提交:审核中,置失败待重推
|
||||
after.setKingdeeStatus(KINGDEE_STATUS_FAILED);
|
||||
after.setKingdeeFailReason("金蝶侧该客商审核中,请稍后重推");
|
||||
pushFailed++;
|
||||
collectChangeRecord(changeRecords, customer.getId(), "推送金蝶失败:金蝶侧客商审核中", customer, after);
|
||||
}
|
||||
log.info("金蝶客商数据定时同步复查处理完成,id={},unifiedCreditCode={},kingdeeStatus={}",
|
||||
customer.getId(), customer.getUnifiedCreditCode(), after.getKingdeeStatus());
|
||||
updateList.add(after);
|
||||
}
|
||||
// 金蝶不存在档案的逐条推送(A902-2 保存):
|
||||
// 报文仅需主表字段 + 收款账户,按客商ID批量一次查询收款账户组装报文,避免逐条 detail 的 N+1 查询
|
||||
Map<Long, List<CustomerReceiptAccountVO>> receiptAccountsMap = loadReceiptAccountsMap(pushList.stream()
|
||||
.map(CustomerArchive::getId)
|
||||
.toList());
|
||||
for (CustomerArchive customer : pushList) {
|
||||
CustomerArchive after = copyCustomer(customer);
|
||||
after.setKingdeeSyncTime(LocalDateTime.now());
|
||||
try {
|
||||
CustomerArchiveVO detail = buildSyncArchiveVO(customer,
|
||||
receiptAccountsMap.getOrDefault(customer.getId(), new ArrayList<>()));
|
||||
pushCustomerToKingdee(customer.getId(), customer, after, detail);
|
||||
updateList.add(after);
|
||||
if (Objects.equals(after.getKingdeeStatus(), KINGDEE_STATUS_SYNCED)) {
|
||||
pushed++;
|
||||
log.info("金蝶客商数据定时同步推送成功,id={},unifiedCreditCode={}",
|
||||
customer.getId(), customer.getUnifiedCreditCode());
|
||||
} else {
|
||||
pushFailed++;
|
||||
log.warn("金蝶客商数据定时同步推送失败,id={},unifiedCreditCode={},kingdeeStatus={},failReason={}",
|
||||
customer.getId(), customer.getUnifiedCreditCode(), after.getKingdeeStatus(), after.getKingdeeFailReason());
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
pushFailed++;
|
||||
log.error("定时同步金蝶客商推送异常,id={},unifiedCreditCode={}",
|
||||
customer.getId(), customer.getUnifiedCreditCode(), exception);
|
||||
// 失败状态与原因已在推送方法内回填 after,落库供页面展示与重推
|
||||
updateList.add(after);
|
||||
}
|
||||
}
|
||||
// ②③ 共用:对已推送(synced)/金蝶已有(exist)客商按信用代码批量复查金蝶档案(审核状态 + 使用状态),仅调用一次 A902-1
|
||||
int disabled = 0;
|
||||
int disableFailed = 0;
|
||||
int confirmed = 0;
|
||||
int auditing = 0;
|
||||
int unknown = 0;
|
||||
if (!enabledList.isEmpty() || !syncedList.isEmpty()) {
|
||||
List<String> reviewCreditCodes = Stream.concat(syncedList.stream(), enabledList.stream())
|
||||
.map(CustomerArchive::getUnifiedCreditCode)
|
||||
.filter(Func::isNotEmpty)
|
||||
.map(code -> code.trim().toUpperCase())
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<String, BfiCustomerVO> kingdeeMap = reviewCreditCodes.isEmpty()
|
||||
? Map.of()
|
||||
: queryKingdeeCustomerMap(reviewCreditCodes);
|
||||
// ③ 审核确认:A902-1 复查 status=C 已审核才是最终成功,synced 升级为 exist(终态);
|
||||
// A/B 审核中保持 synced 等待下轮复查(不重推,避免金蝶侧重复建档)
|
||||
for (CustomerArchive customer : syncedList) {
|
||||
BfiCustomerVO kingdeeCustomer = kingdeeMap.get(customer.getUnifiedCreditCode().trim().toUpperCase());
|
||||
if (kingdeeCustomer == null) {
|
||||
unknown++;
|
||||
log.warn("金蝶客商审核状态复查未查询到档案,id={},unifiedCreditCode={}",
|
||||
customer.getId(), customer.getUnifiedCreditCode());
|
||||
continue;
|
||||
}
|
||||
if (!BfiConstant.BILL_STATUS_AUDITED.equals(kingdeeCustomer.getStatus())) {
|
||||
auditing++;
|
||||
log.info("金蝶客商审核中,等待下轮复查,id={},unifiedCreditCode={},kingdeeStatus={}",
|
||||
customer.getId(), customer.getUnifiedCreditCode(), kingdeeCustomer.getStatus());
|
||||
continue;
|
||||
}
|
||||
confirmed++;
|
||||
CustomerArchive after = copyCustomer(customer);
|
||||
after.setKingdeeStatus(KINGDEE_STATUS_EXIST);
|
||||
after.setKingdeeFailReason(null);
|
||||
updateList.add(after);
|
||||
collectChangeRecord(changeRecords, customer.getId(), "金蝶客商已审核,推送最终成功", customer, after);
|
||||
log.info("金蝶客商已审核,推送最终成功并升级为金蝶已有档案,id={},unifiedCreditCode={}",
|
||||
customer.getId(), customer.getUnifiedCreditCode());
|
||||
}
|
||||
// ② 停用联动:金蝶禁用(enable=0)→ TMS 自动停用;金蝶恢复可用不自动启用(TMS 人工控制);
|
||||
// 直接基于已查询档案内存处理,避免逐条自代理调用重复查询档案与权限
|
||||
for (CustomerArchive customer : enabledList) {
|
||||
BfiCustomerVO kingdeeCustomer = kingdeeMap.get(customer.getUnifiedCreditCode().trim().toUpperCase());
|
||||
if (kingdeeCustomer == null || !"0".equals(kingdeeCustomer.getEnable())) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
CustomerArchive after = copyCustomer(customer);
|
||||
after.setStatus(STATUS_DISABLED);
|
||||
collectChangeRecord(changeRecords, customer.getId(), "金蝶侧客商已停用,同步停用客商档案", customer, after);
|
||||
updateList.add(after);
|
||||
disabled++;
|
||||
log.info("金蝶侧客商已停用,联动停用客商档案成功,id={},unifiedCreditCode={}",
|
||||
customer.getId(), customer.getUnifiedCreditCode());
|
||||
} catch (Exception exception) {
|
||||
disableFailed++;
|
||||
log.error("定时同步金蝶客商停用异常,id={},unifiedCreditCode={}",
|
||||
customer.getId(), customer.getUnifiedCreditCode(), exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
flushSyncChanges(updateList, changeRecords);
|
||||
String summary = "待推送 " + pendingList.size() + " 条(推送成功 " + pushed + ",金蝶已有 " + existed
|
||||
+ ",失败 " + pushFailed + "),停用联动 " + disabled + " 条(失败 " + disableFailed + ")"
|
||||
+ ",已推送复查 " + syncedList.size() + " 条(金蝶已审核 " + confirmed + ",审核中 " + auditing
|
||||
+ ",未查询到 " + unknown + ")";
|
||||
log.info("金蝶客商数据定时同步结束:{}", summary);
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean disableCustomerFromKingdee(Long id) {
|
||||
CustomerArchive customer = ensureCustomerAccessible(id);
|
||||
if (!Objects.equals(customer.getStatus(), STATUS_ENABLED)) {
|
||||
return false;
|
||||
}
|
||||
CustomerArchive after = copyCustomer(customer);
|
||||
after.setStatus(STATUS_DISABLED);
|
||||
addChangeRecord(id, "金蝶侧客商已停用,同步停用客商档案", customer, after);
|
||||
return updateById(after);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按统一社会信用代码分批批量查询金蝶客商,键为大写信用代码;
|
||||
* societycreditcode 支持数组一次查询多个,单批使用 BfiUtil.queryAllPages 自动翻页取全,
|
||||
* 避免同一信用代码多条记录时漏查,单批查询异常不中断其余批次
|
||||
*
|
||||
* @param creditCodes 统一社会信用代码集合
|
||||
* @return 大写信用代码 -> 金蝶客商
|
||||
*/
|
||||
private Map<String, BfiCustomerVO> queryKingdeeCustomerMap(List<String> creditCodes) {
|
||||
Map<String, BfiCustomerVO> result = new HashMap<>();
|
||||
for (int start = 0; start < creditCodes.size(); start += KINGDEE_CUSTOMER_QUERY_BATCH) {
|
||||
List<String> batch = creditCodes.subList(start, Math.min(start + KINGDEE_CUSTOMER_QUERY_BATCH, creditCodes.size()));
|
||||
if (batch.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
BfiCustomerQueryDTO param = new BfiCustomerQueryDTO();
|
||||
param.setSocietycreditcode(batch);
|
||||
List<BfiCustomerVO> rows = BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(KINGDEE_CUSTOMER_QUERY_BATCH);
|
||||
return bfiService.queryCustomer(param);
|
||||
});
|
||||
for (BfiCustomerVO vo : rows) {
|
||||
if (Func.isNotEmpty(vo.getSocietycreditcode())) {
|
||||
result.put(vo.getSocietycreditcode().trim().toUpperCase(), vo);
|
||||
}
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
log.error("定时同步金蝶客商批量查询异常,batchSize={}", batch.size(), exception);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean changeStatus(Long id, Integer status) {
|
||||
@@ -485,6 +973,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
applyFixedOwnerDept(customer);
|
||||
customer.setAccessType(Func.isEmpty(customer.getAccessType()) ? ACCESS_TEMPORARY : customer.getAccessType());
|
||||
customer.setApprovalStatus(Func.isEmpty(customer.getApprovalStatus()) ? APPROVAL_DRAFT : customer.getApprovalStatus());
|
||||
customer.setKingdeeStatus(Func.isEmpty(customer.getKingdeeStatus()) ? KINGDEE_STATUS_NONE : customer.getKingdeeStatus());
|
||||
if (Func.isEmpty(customer.getStatus())) {
|
||||
customer.setStatus(STATUS_ENABLED);
|
||||
}
|
||||
@@ -591,6 +1080,47 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 国资委黑名单校验(A801-5):客商提审进入准入审批前实时查询金蝶黑名单,
|
||||
* 以客商发票信息中的纳税人识别号(number)作为查询条件按税号精确匹配;命中禁止交易类(A)硬拦截,
|
||||
* 风险警示类(B)默认放行告警、可经 {@code thirdparty.bfi.blacklist.blockRiskWarning} 配置拦截,
|
||||
* 其他类别放行告警;金蝶黑名单查询异常时向上抛出,阻断提审
|
||||
*
|
||||
* @param customer 客商
|
||||
*/
|
||||
private void validateBlacklist(CustomerArchiveVO customer) {
|
||||
List<CustomerInvoiceInfoVO> invoices = customer.getInvoices();
|
||||
if (Func.isEmpty(invoices)) {
|
||||
return;
|
||||
}
|
||||
List<String> taxNos = invoices.stream()
|
||||
.map(CustomerInvoiceInfoVO::getTaxNo)
|
||||
.filter(Func::isNotEmpty)
|
||||
.map(taxNo -> taxNo.trim().toUpperCase())
|
||||
.distinct()
|
||||
.toList();
|
||||
if (Func.isEmpty(taxNos)) {
|
||||
return;
|
||||
}
|
||||
List<BfiBlacklistVO> blacklist = bfiService.queryBlacklist("1", taxNos);
|
||||
BfiBlacklistVO hit = blacklist.stream()
|
||||
.filter(item -> Func.isNotEmpty(item.getNumber()) && taxNos.contains(item.getNumber().trim().toUpperCase()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (hit == null) {
|
||||
return;
|
||||
}
|
||||
String category = Func.isEmpty(hit.getAwrpCombofieldTitle()) ? hit.getAwrpCombofield() : hit.getAwrpCombofieldTitle();
|
||||
boolean forbidden = "A".equals(hit.getAwrpCombofield());
|
||||
boolean riskWarning = "B".equals(hit.getAwrpCombofield());
|
||||
if (forbidden || (riskWarning && bfiProperties.getBlacklist().isBlockRiskWarning())) {
|
||||
throw new ServiceException(String.format("客商【%s】在国资委黑名单中(失信类别:%s,列入事由:%s),不允许提交准入审批",
|
||||
customer.getFullName(), category, hit.getAwrpTextareafield()));
|
||||
}
|
||||
log.warn("客商命中金蝶黑名单风险警示类,放行,fullName={},taxNos={},失信类别={},列入事由={}",
|
||||
customer.getFullName(), taxNos, category, hit.getAwrpTextareafield());
|
||||
}
|
||||
|
||||
private void validateUnique(CustomerArchiveVO customer) {
|
||||
LambdaQueryWrapper<CustomerArchive> unifiedCreditCodeQuery = Wrappers.<CustomerArchive>lambdaQuery()
|
||||
.eq(CustomerArchive::getUnifiedCreditCode, customer.getUnifiedCreditCode())
|
||||
@@ -854,12 +1384,26 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
}
|
||||
|
||||
private List<CustomerReceiptAccountVO> loadReceiptAccounts(Long customerId) {
|
||||
return loadReceiptAccountsMap(List.of(customerId)).getOrDefault(customerId, new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按客商ID集合批量查询收款账户(键为客商ID),供定时同步批量组装金蝶报文等场景使用,避免逐条查询
|
||||
*
|
||||
* @param customerIds 客商ID集合
|
||||
* @return 客商ID -> 收款账户列表(默认一条扣款排序)
|
||||
*/
|
||||
private Map<Long, List<CustomerReceiptAccountVO>> loadReceiptAccountsMap(List<Long> customerIds) {
|
||||
if (Func.isEmpty(customerIds)) {
|
||||
return Map.of();
|
||||
}
|
||||
return receiptAccountMapper.selectList(Wrappers.<CustomerReceiptAccount>lambdaQuery()
|
||||
.eq(CustomerReceiptAccount::getCustomerId, customerId)
|
||||
.in(CustomerReceiptAccount::getCustomerId, customerIds)
|
||||
.eq(CustomerReceiptAccount::getIsDeleted, 0)
|
||||
.orderByDesc(CustomerReceiptAccount::getIsDefault)
|
||||
.orderByAsc(CustomerReceiptAccount::getCreateTime))
|
||||
.stream().map(account -> Objects.requireNonNull(BeanUtil.copyProperties(account, CustomerReceiptAccountVO.class))).toList();
|
||||
.stream().map(account -> Objects.requireNonNull(BeanUtil.copyProperties(account, CustomerReceiptAccountVO.class)))
|
||||
.collect(Collectors.groupingBy(CustomerReceiptAccountVO::getCustomerId));
|
||||
}
|
||||
|
||||
private List<CustomerInvoiceInfoVO> loadInvoices(Long customerId) {
|
||||
@@ -1306,11 +1850,10 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
}
|
||||
|
||||
private void addChangeRecord(Long customerId, String content, CustomerArchive before, CustomerArchive after) {
|
||||
Map<String, Object> beforeSnapshot = customerSnapshot(before);
|
||||
Map<String, Object> afterSnapshot = customerSnapshot(after);
|
||||
addDetailSnapshot(beforeSnapshot, null);
|
||||
addDetailSnapshot(afterSnapshot, null);
|
||||
writeChangeRecord(customerId, content, beforeSnapshot, afterSnapshot);
|
||||
CustomerChangeRecord record = buildChangeRecord(customerId, content, before, after);
|
||||
if (record != null) {
|
||||
changeRecordMapper.insert(record);
|
||||
}
|
||||
}
|
||||
|
||||
private void addChangeRecord(Long customerId, String content, CustomerArchive before, CustomerArchive after,
|
||||
@@ -1319,10 +1862,35 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
Map<String, Object> afterSnapshot = customerSnapshot(after);
|
||||
addDetailSnapshot(beforeSnapshot, beforeDetail);
|
||||
addDetailSnapshot(afterSnapshot, afterDetail);
|
||||
writeChangeRecord(customerId, content, beforeSnapshot, afterSnapshot);
|
||||
CustomerChangeRecord record = buildChangeRecord(customerId, content, beforeSnapshot, afterSnapshot);
|
||||
if (record != null) {
|
||||
changeRecordMapper.insert(record);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeChangeRecord(Long customerId, String content, Map<String, Object> beforeSnapshot,
|
||||
/**
|
||||
* 收集变更记录供定时同步集中批量插入(无实质变更不收集),避免逐条 insert
|
||||
*/
|
||||
private void collectChangeRecord(List<CustomerChangeRecord> records, Long customerId, String content,
|
||||
CustomerArchive before, CustomerArchive after) {
|
||||
CustomerChangeRecord record = buildChangeRecord(customerId, content, before, after);
|
||||
if (record != null) {
|
||||
records.add(record);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建变更记录(无实质变更返回 null),供单条写入与定时同步批量收集共用
|
||||
*/
|
||||
private CustomerChangeRecord buildChangeRecord(Long customerId, String content, CustomerArchive before, CustomerArchive after) {
|
||||
Map<String, Object> beforeSnapshot = customerSnapshot(before);
|
||||
Map<String, Object> afterSnapshot = customerSnapshot(after);
|
||||
addDetailSnapshot(beforeSnapshot, null);
|
||||
addDetailSnapshot(afterSnapshot, null);
|
||||
return buildChangeRecord(customerId, content, beforeSnapshot, afterSnapshot);
|
||||
}
|
||||
|
||||
private CustomerChangeRecord buildChangeRecord(Long customerId, String content, Map<String, Object> beforeSnapshot,
|
||||
Map<String, Object> afterSnapshot) {
|
||||
for (String field : afterSnapshot.keySet()) {
|
||||
beforeSnapshot.putIfAbsent(field, null);
|
||||
@@ -1343,7 +1911,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
}
|
||||
}
|
||||
if (changedFields.isEmpty()) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
CustomerChangeRecord record = new CustomerChangeRecord();
|
||||
record.setId(IdWorker.getId());
|
||||
@@ -1355,7 +1923,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
record.setAfterData(JsonUtil.toJson(afterData));
|
||||
record.setChangeUserName(AuthUtil.getUserName());
|
||||
record.setStatus(STATUS_ENABLED);
|
||||
changeRecordMapper.insert(record);
|
||||
return record;
|
||||
}
|
||||
|
||||
private boolean isSameSnapshotValue(Object beforeValue, Object afterValue) {
|
||||
@@ -1486,6 +2054,10 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
snapshot.put("当前节点", customer.getCurrentNode());
|
||||
snapshot.put("当前处理人", customer.getCurrentProcessor());
|
||||
snapshot.put("审核通过时间", customer.getApprovedTime());
|
||||
// 金蝶协同字段纳入快照:金蝶推送/审核确认/失败重推等状态变更需要落变更记录,否则前后快照一致会被判定为无实质变更而跳过
|
||||
snapshot.put("金蝶协同状态", customer.getKingdeeStatus());
|
||||
snapshot.put("金蝶失败原因", customer.getKingdeeFailReason());
|
||||
snapshot.put("金蝶同步时间", customer.getKingdeeSyncTime());
|
||||
snapshot.put("状态", customer.getStatus());
|
||||
return snapshot;
|
||||
}
|
||||
@@ -1572,6 +2144,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
excel.setLegalPerson(customer.getLegalPerson());
|
||||
excel.setCreateUserName(UserCache.getUserRealName(customer.getCreateUser()));
|
||||
excel.setCreateTime(customer.getCreateTime());
|
||||
excel.setKingdeeStatusName(kingdeeStatusName(customer.getKingdeeStatus()));
|
||||
return excel;
|
||||
}
|
||||
|
||||
@@ -1592,6 +2165,19 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
return "草稿";
|
||||
}
|
||||
|
||||
private String kingdeeStatusName(String kingdeeStatus) {
|
||||
if (Objects.equals(kingdeeStatus, KINGDEE_STATUS_SYNCED)) {
|
||||
return "已推送";
|
||||
}
|
||||
if (Objects.equals(kingdeeStatus, KINGDEE_STATUS_EXIST)) {
|
||||
return "金蝶已有";
|
||||
}
|
||||
if (Objects.equals(kingdeeStatus, KINGDEE_STATUS_FAILED)) {
|
||||
return "推送失败";
|
||||
}
|
||||
return "未推送";
|
||||
}
|
||||
|
||||
private void validateLength(String value, int maxLength, String message) {
|
||||
if (Func.isNotEmpty(value) && value.length() > maxLength) {
|
||||
throw new ServiceException(message);
|
||||
@@ -1624,7 +2210,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
}
|
||||
|
||||
/**
|
||||
* 客商所属组织固定为顶层「桂物物流集团」,忽略前端传入的其他组织。
|
||||
* 客商所属组织固定为顶层「广西现代物流集团有限公司」,忽略前端传入的其他组织。
|
||||
*/
|
||||
private void applyFixedOwnerDept(CustomerArchiveVO customer) {
|
||||
customer.setDeptName(FIXED_OWNER_DEPT_NAME);
|
||||
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 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.service.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.DictBizCache;
|
||||
import org.springblade.system.cache.SysCache;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.system.pojo.entity.DictBiz;
|
||||
import org.springblade.thirdparty.bfi.constant.BfiConstant;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerBankEntryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerGroupEntryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerLinkmanEntryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerSaveDTO;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.constant.CustomerBfiConstants;
|
||||
import org.springblade.transport.pojo.vo.CustomerArchiveVO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 客商档案金蝶(星瀚)报文转换器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerBfiConverter {
|
||||
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
/**
|
||||
* 组装 A902-2 客户保存参数
|
||||
*
|
||||
* @param customer 客商档案
|
||||
* @return 客户保存参数
|
||||
*/
|
||||
public BfiCustomerSaveDTO buildSaveDTO(CustomerArchiveVO customer) {
|
||||
BfiCustomerSaveDTO param = new BfiCustomerSaveDTO();
|
||||
// 编码:不传由星瀚生成;金蝶客户编码与 TMS 客商编号保持一致
|
||||
param.setNumber(customer.getCustomerCode());
|
||||
// 数据状态:固定传 B 已提交,金蝶校验工商信息通过后由机器人自动审核
|
||||
// param.setStatus(BfiConstant.BILL_STATUS_SUBMITTED);
|
||||
// 【必填】名称:客商名称
|
||||
param.setName(customer.getFullName());
|
||||
// 【必填】统一社会信用代码(候选键):个人为身份证号或手机号
|
||||
param.setSocietycreditcode(customer.getUnifiedCreditCode());
|
||||
|
||||
// 【必填】创建组织:客商所属组织编码对应的金蝶组织编码
|
||||
// param.setCreateorgNumber(this.resolveOrgNumber(customer));
|
||||
|
||||
// 纳税人识别号:与统一社会信用代码一致时推送
|
||||
param.setTxRegisterNo(customer.getUnifiedCreditCode());
|
||||
// 法人代表:企业法人
|
||||
param.setArtificialperson(customer.getLegalPerson());
|
||||
// 【必填】伙伴类型:客商性质字典 dictKey 即伙伴类型编码,直接推送,无效编码抛异常
|
||||
param.setType(this.resolvePartnerType(customer.getCustomerNature()));
|
||||
// 详细地址:注册地址(省市区 + 详细地址拼接文本)
|
||||
param.setBizpartnerAddress(customer.getRegisteredAddress());
|
||||
// 经营范围:多选编码 → 字典文本「、」拼接
|
||||
param.setBusinessscope(this.buildBusinessScopeText(customer.getBusinessScope()));
|
||||
// 营业期限:长期传「长期」,固定期限传「至 yyyy-MM-dd」(TMS 未采集起始日)
|
||||
param.setBusinessterm(this.buildBusinessTerm(customer.getBusinessTermType(), customer.getBusinessEndDate()));
|
||||
// 联系电话:客商联系电话
|
||||
param.setBizpartnerPhone(customer.getContactPhone());
|
||||
// 备注:客商备注,空时不传
|
||||
param.setFcusbeizhu(customer.getRemark());
|
||||
// 业务职能:固定全选(1销售,2结算,3付款,4收货)
|
||||
param.setBizfunction(CustomerBfiConstants.BIZ_FUNCTION_ALL);
|
||||
// 数据来源系统:thirdParty.bfi.srcSys(公共参数),默认 WLTMS(运输TMS)
|
||||
param.setAwrpDatasourcesys(bfiProperties.getSrcSys());
|
||||
// 【必填】是否为贸易客商:固定传 A 是
|
||||
param.setAwrpCusfortrade(BfiConstant.CUS_FOR_TRADE_YES);
|
||||
// 【必填】分类标准分录:外部客商 1102、内部组织 1101
|
||||
BfiCustomerGroupEntryDTO groupEntry = new BfiCustomerGroupEntryDTO();
|
||||
groupEntry.setStandardidNumber(BfiConstant.GROUP_STANDARD_BASIC);
|
||||
groupEntry.setGroupidNumber(this.resolveGroupId(customer.getCustomerKind()));
|
||||
param.setEntryGroupstandard(List.of(groupEntry));
|
||||
// 银行信息分录:收款账户明细(账户名称传开户行,币种默认 RMB)
|
||||
param.setEntryBank(this.buildBankEntries(customer));
|
||||
// 联系人信息分录:页面填写的客商负责人与联系电话
|
||||
param.setEntryLinkman(this.buildLinkmanEntries(customer));
|
||||
return param;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶创建组织编码:客商所属组织部门编码,与金蝶组织编码一致;
|
||||
* 所属组织不存在或未维护部门编码时抛出异常,阻断推送
|
||||
*
|
||||
* @param customer 客商档案
|
||||
* @return 金蝶创建组织编码
|
||||
*/
|
||||
private String resolveOrgNumber(CustomerArchiveVO customer) {
|
||||
Dept dept = customer.getDeptId() == null ? null : SysCache.getDept(customer.getDeptId());
|
||||
if (dept == null) {
|
||||
throw new ServiceException("推送金蝶失败,客商所属组织不存在,请检查客商所属组织配置");
|
||||
}
|
||||
if (Func.isBlank(dept.getDeptCode())) {
|
||||
throw new ServiceException("推送金蝶失败,客商所属组织编码不能为空,请检查组织[" + dept.getDeptName() + "]是否维护部门编码");
|
||||
}
|
||||
return dept.getDeptCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析分类标准分录客户分组编码:内部组织 1101,其余(外部客商)1102
|
||||
*
|
||||
* @param customerKind 客商类型(internal 内部组织 / external 外部客商)
|
||||
* @return 客户分组编码
|
||||
*/
|
||||
private String resolveGroupId(String customerKind) {
|
||||
if (CustomerBfiConstants.CUSTOMER_KIND_INTERNAL.equals(customerKind)) {
|
||||
return BfiConstant.CUSTOMER_GROUP_INTERNAL;
|
||||
}
|
||||
return BfiConstant.CUSTOMER_GROUP_EXTERNAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 A902-2 银行信息分录:每条收款账户一行分录,账户名称传开户行、币种默认 RMB,收款账户未填写时返回空分录
|
||||
*
|
||||
* @param customer 客商档案
|
||||
* @return 银行信息分录
|
||||
*/
|
||||
private List<BfiCustomerBankEntryDTO> buildBankEntries(CustomerArchiveVO customer) {
|
||||
if (Func.isEmpty(customer.getReceiptAccounts())) {
|
||||
return List.of();
|
||||
}
|
||||
return customer.getReceiptAccounts().stream()
|
||||
.filter(account -> Func.isNotBlank(account.getBankAccount()) || Func.isNotBlank(account.getBankName()))
|
||||
.map(account -> {
|
||||
BfiCustomerBankEntryDTO entry = new BfiCustomerBankEntryDTO();
|
||||
// 银行账号
|
||||
entry.setBankaccount(account.getBankAccount());
|
||||
// 账户名称:传开户行
|
||||
entry.setAccountname(account.getBankName());
|
||||
// 开户银行.编码(行号) todo
|
||||
String cnapsCode = account.getCnapsCode();
|
||||
// cnapsCode = "001110012009";
|
||||
entry.setBankNumber(cnapsCode);
|
||||
// 币种:人民币(CNY)
|
||||
entry.setCurrencyNumber(BfiConstant.CURRENCY_CNY);
|
||||
// 是否默认银行
|
||||
entry.setIsdefaultBank(true);
|
||||
return entry;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 A902-2 联系人信息分录:页面填写的客商负责人与联系电话,未填负责人时返回空分录
|
||||
*
|
||||
* @param customer 客商档案
|
||||
* @return 联系人信息分录
|
||||
*/
|
||||
private List<BfiCustomerLinkmanEntryDTO> buildLinkmanEntries(CustomerArchiveVO customer) {
|
||||
if (Func.isBlank(customer.getPrincipal())) {
|
||||
return List.of();
|
||||
}
|
||||
BfiCustomerLinkmanEntryDTO entry = new BfiCustomerLinkmanEntryDTO();
|
||||
// 联系人:客商负责人
|
||||
entry.setContactperson(customer.getPrincipal());
|
||||
// 电话:联系电话
|
||||
entry.setPhone(customer.getContactPhone());
|
||||
// 默认联系人
|
||||
entry.setIsdefaultLinkman(Boolean.TRUE);
|
||||
return List.of(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶伙伴类型编码:客商性质(nature_of_client 字典)——
|
||||
* 值为 dictKey(即伙伴类型编码,1~17)时直接使用;
|
||||
* 值为 dictValue 文本(前端字典下拉存文本,含存量数据,如「我区国企/国企控股」)时按字典反查对应 dictKey;
|
||||
* 均无法解析时抛出异常,阻断推送
|
||||
*
|
||||
* @param customerNature 客商性质(dictKey 编码或 dictValue 文本)
|
||||
* @return 金蝶伙伴类型编码
|
||||
*/
|
||||
public String resolvePartnerType(String customerNature) {
|
||||
String nature = Func.isEmpty(customerNature) ? "" : customerNature.trim();
|
||||
if (nature.matches("\\d{1,2}")) {
|
||||
int partnerType = Integer.parseInt(nature);
|
||||
if (partnerType >= 1 && partnerType <= CustomerBfiConstants.PARTNER_TYPE_MAX) {
|
||||
return nature;
|
||||
}
|
||||
}
|
||||
String partnerType = this.resolvePartnerTypeFromDictValue(nature);
|
||||
if (Func.isBlank(partnerType)) {
|
||||
throw new ServiceException("客商性质未配置有效的金蝶伙伴类型编码(nature_of_client 字典 dictKey):" + customerNature);
|
||||
}
|
||||
return partnerType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按客商性质文本(dictValue)反查 nature_of_client 字典,返回对应 dictKey(即伙伴类型编码)
|
||||
*
|
||||
* @param natureValue 客商性质文本
|
||||
* @return 金蝶伙伴类型编码,未命中返回 null
|
||||
*/
|
||||
private String resolvePartnerTypeFromDictValue(String natureValue) {
|
||||
if (Func.isEmpty(natureValue)) {
|
||||
return null;
|
||||
}
|
||||
List<DictBiz> dictList = DictBizCache.getList("nature_of_client");
|
||||
if (dictList == null) {
|
||||
return null;
|
||||
}
|
||||
return dictList.stream()
|
||||
.filter(dict -> natureValue.equals(dict.getDictValue()))
|
||||
.map(DictBiz::getDictKey)
|
||||
.map(key -> Func.isEmpty(key) ? "" : key.trim())
|
||||
.filter(key -> {
|
||||
if (!key.matches("\\d{1,2}")) {
|
||||
return false;
|
||||
}
|
||||
int partnerType = Integer.parseInt(key);
|
||||
return partnerType >= 1 && partnerType <= CustomerBfiConstants.PARTNER_TYPE_MAX;
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 经营范围多选编码 → 字典文本「、」拼接,字典缺失时回退原编码
|
||||
*
|
||||
* @param businessScope 逗号拼接的经营范围编码
|
||||
* @return 金蝶经营范围文本
|
||||
*/
|
||||
public String buildBusinessScopeText(String businessScope) {
|
||||
if (Func.isEmpty(businessScope)) {
|
||||
return "";
|
||||
}
|
||||
List<DictBiz> dictList = DictBizCache.getList("scope_of_business");
|
||||
Map<String, String> scopeNames = dictList == null ? Map.of() : dictList.stream()
|
||||
.collect(Collectors.toMap(DictBiz::getDictKey, DictBiz::getDictValue, (first, second) -> first));
|
||||
return Func.toStrList(businessScope.replace(",", ",")).stream()
|
||||
.map(code -> scopeNames.getOrDefault(code, code))
|
||||
.collect(Collectors.joining("、"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 营业期限:长期传「长期」;固定期限传「至 yyyy-MM-dd」(TMS 未采集起始日)
|
||||
*
|
||||
* @param businessTermType 营业期限类型
|
||||
* @param businessEndDate 营业期限截止日
|
||||
* @return 金蝶营业期限文本
|
||||
*/
|
||||
public String buildBusinessTerm(String businessTermType, LocalDate businessEndDate) {
|
||||
if (CustomerBfiConstants.BUSINESS_TERM_LONG.equals(businessTermType)) {
|
||||
return CustomerBfiConstants.BUSINESS_TERM_LONG;
|
||||
}
|
||||
return businessEndDate == null ? "" : "至 " + businessEndDate;
|
||||
}
|
||||
|
||||
}
|
||||
+652
-67
@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
@@ -27,6 +28,9 @@ import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementPaymentMapper;
|
||||
import org.springblade.transport.mapper.InvoiceReceiptMapper;
|
||||
import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationInvoiceMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationSettlementMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementDetailMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementDetailFeeMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementMapper;
|
||||
@@ -35,6 +39,12 @@ import org.springblade.transport.mapper.PaymentApplicationMapper;
|
||||
import org.springblade.transport.mapper.PaymentApplicationInvoiceMapper;
|
||||
import org.springblade.transport.mapper.PaymentApplicationSettlementMapper;
|
||||
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
|
||||
import org.springblade.transport.constant.KingdeeAttachmentConstant;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.constant.KingdeeLockConstant;
|
||||
import org.springblade.transport.pojo.entity.KingdeeBillLock;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.mapper.ReceivablePayableDetailMapper;
|
||||
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
|
||||
import org.springblade.transport.mapper.SettlementAdjustmentMapper;
|
||||
@@ -54,6 +64,9 @@ import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementPayment;
|
||||
import org.springblade.transport.pojo.entity.InvoiceReceipt;
|
||||
import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplication;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.PreSettlement;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementDetail;
|
||||
@@ -71,14 +84,25 @@ import org.springblade.transport.pojo.vo.FormalSettlementVO;
|
||||
import org.springblade.transport.pojo.vo.PreSettlementVO;
|
||||
import org.springblade.transport.pojo.vo.SettlementAdjustmentVO;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
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;
|
||||
import org.springblade.transport.support.KingdeeArBillSyncSupport;
|
||||
import org.springblade.transport.support.KingdeeSyncLockSupport;
|
||||
import org.springblade.transport.support.KingdeeAttachmentSyncSupport;
|
||||
import org.springblade.transport.wrapper.PreSettlementWrapper;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.transport.event.KingdeeBillApprovedEvent;
|
||||
import org.springblade.transport.wrapper.FormalSettlementWrapper;
|
||||
import org.springblade.transport.wrapper.PaymentApplicationWrapper;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -88,6 +112,8 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@@ -104,20 +130,31 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlementMapper, FormalSettlement>
|
||||
implements IFormalSettlementService {
|
||||
|
||||
private static final String DRAFT = "draft";
|
||||
private static final String REVIEWING = "reviewing";
|
||||
private static final String APPROVED = "approved";
|
||||
private static final String RETURNED = "returned";
|
||||
private static final String VOIDED = "voided";
|
||||
/** 金蝶票池认领闸门(收票服务承载 A801-1 重查与可用性口径) */
|
||||
private final IInvoiceReceiptService invoiceReceiptService;
|
||||
|
||||
/** 金蝶单据锁定记录服务(lockBusiData 锁定/补偿/解锁守卫) */
|
||||
private final IKingdeeBillLockService kingdeeBillLockService;
|
||||
|
||||
/** 金蝶应收单勾稽 single-flight 锁键前缀(多副本定时任务与手动触发共用) */
|
||||
private static final String AR_RESULT_SYNC_LOCK_PREFIX = "transport:formal-settlement:kingdee-ar-result-sync:";
|
||||
|
||||
/** 金蝶应收单勾稽结算明细分批查询单次条数上限(in 条件分批,避免单次过滤条件过长) */
|
||||
private static final int AR_RESULT_DETAIL_QUERY_BATCH = 500;
|
||||
|
||||
private final FormalSettlementSourceMapper sourceMapper;
|
||||
private final FormalSettlementSummaryFeeMapper summaryFeeMapper;
|
||||
private final FormalSettlementPaymentMapper paymentMapper;
|
||||
private final FormalSettlementInvoiceMapper invoiceMapper;
|
||||
private final InvoiceReceiptMapper invoiceReceiptMapper;
|
||||
private final InvoiceReceiptSettlementMapper invoiceReceiptSettlementMapper;
|
||||
private final InvoiceApplicationMapper invoiceApplicationMapper;
|
||||
private final InvoiceApplicationInvoiceMapper invoiceApplicationInvoiceMapper;
|
||||
private final InvoiceApplicationSettlementMapper invoiceApplicationSettlementMapper;
|
||||
private final FormalSettlementChangeRecordMapper changeRecordMapper;
|
||||
private final FormalSettlementDetailMapper detailMapper;
|
||||
private final FormalSettlementDetailFeeMapper detailFeeMapper;
|
||||
@@ -134,7 +171,14 @@ 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;
|
||||
private final KingdeeAttachmentSyncSupport kingdeeAttachmentSyncSupport;
|
||||
private final KingdeeSyncLockSupport kingdeeSyncLockSupport;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
@Override
|
||||
public IPage<FormalSettlementVO> selectPage(IPage<FormalSettlement> page, FormalSettlementVO query) {
|
||||
@@ -163,13 +207,68 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
if (ids.isEmpty()) wrapper.eq(FormalSettlement::getId, -1L); else wrapper.in(FormalSettlement::getId, ids);
|
||||
}
|
||||
IPage<FormalSettlement> result = page(page, wrapper.orderByDesc(FormalSettlement::getCreateTime));
|
||||
return result.convert(this::toVO);
|
||||
IPage<FormalSettlementVO> voPage = result.convert(this::toVO);
|
||||
fillAttachmentSyncStatus(voPage.getRecords());
|
||||
fillKingdeeLockStatus(voPage.getRecords());
|
||||
return voPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充金蝶锁状态标识(locked-锁定中/failed-锁定失败;unlocked 终态不返回):
|
||||
* 锁表按本页源单一次批量查询,避免 N+1;locked 时前端可禁用编辑/作废,
|
||||
* failed 时展示失败原因(tryCount 达上限 5 提示重推同步金蝶)
|
||||
*
|
||||
* @param rows 本页 VO 列表
|
||||
*/
|
||||
private void fillKingdeeLockStatus(List<FormalSettlementVO> rows) {
|
||||
if (Func.isEmpty(rows)) {
|
||||
return;
|
||||
}
|
||||
Map<Long, KingdeeBillLock> lockMap = kingdeeBillLockService.mapActiveLocks(KingdeeLockConstant.SOURCE_SETTLEMENT,
|
||||
rows.stream().map(FormalSettlementVO::getId).toList());
|
||||
for (FormalSettlementVO vo : rows) {
|
||||
KingdeeBillLock lockRecord = lockMap.get(vo.getId());
|
||||
if (lockRecord == null) {
|
||||
continue;
|
||||
}
|
||||
vo.setKingdeeLockStatus(lockRecord.getLockStatus());
|
||||
vo.setKingdeeLockFailReason(lockRecord.getFailReason());
|
||||
vo.setKingdeeTryCount(lockRecord.getTryCount());
|
||||
}
|
||||
// 金蝶删单解锁会作废源单,按 unlocked 锁记录留痕补「金蝶删单」标识,区分正常作废与金蝶删单
|
||||
kingdeeBillLockService.fillUnlockMarks(KingdeeLockConstant.SOURCE_SETTLEMENT, rows, lockMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充附件同步聚合态(无附件/未上传/部分未传/已上传):按结算类型分业务类型一次 GROUP BY 查询,避免 N+1
|
||||
*
|
||||
* @param rows 本页 VO 列表
|
||||
*/
|
||||
private void fillAttachmentSyncStatus(List<FormalSettlementVO> rows) {
|
||||
if (Func.isEmpty(rows)) {
|
||||
return;
|
||||
}
|
||||
Map<Long, Integer> apSuccessCounts = kingdeeAttachmentSyncSupport.successCountByBizId(
|
||||
KingdeeAttachmentConstant.BIZ_FORMAL_SETTLEMENT_AP, rows.stream()
|
||||
.filter(row -> !"receivable".equals(row.getSettlementType())).map(FormalSettlementVO::getId).toList());
|
||||
Map<Long, Integer> arSuccessCounts = kingdeeAttachmentSyncSupport.successCountByBizId(
|
||||
KingdeeAttachmentConstant.BIZ_FORMAL_SETTLEMENT_AR, rows.stream()
|
||||
.filter(row -> "receivable".equals(row.getSettlementType())).map(FormalSettlementVO::getId).toList());
|
||||
for (FormalSettlementVO vo : rows) {
|
||||
int attachmentCount = kingdeeAttachmentSyncSupport.countAttachments(vo.getAttachmentsJson());
|
||||
Map<Long, Integer> successCounts = "receivable".equals(vo.getSettlementType()) ? arSuccessCounts : apSuccessCounts;
|
||||
String aggregateStatus = kingdeeAttachmentSyncSupport.aggregateStatus(attachmentCount,
|
||||
successCounts.getOrDefault(vo.getId(), 0));
|
||||
vo.setAttachmentCount(attachmentCount);
|
||||
vo.setAttachmentSyncStatus(aggregateStatus);
|
||||
vo.setAttachmentSyncStatusName(KingdeeAttachmentConstant.aggregateName(aggregateStatus));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<PreSettlementVO> candidatePreSettlements(IPage<PreSettlement> page, PreSettlementVO query) {
|
||||
IPage<PreSettlement> result = preSettlementMapper.selectPage(page, Wrappers.<PreSettlement>lambdaQuery()
|
||||
.eq(PreSettlement::getApprovalStatus, APPROVED)
|
||||
.eq(PreSettlement::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.and(w -> w.isNull(PreSettlement::getFormalSettlementNo).or().eq(PreSettlement::getFormalSettlementNo, ""))
|
||||
.eq(query.getContractId() != null, PreSettlement::getContractId, query.getContractId())
|
||||
.like(Func.isNotEmpty(query.getPreSettlementNo()), PreSettlement::getPreSettlementNo, query.getPreSettlementNo())
|
||||
@@ -208,6 +307,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
public FormalSettlementVO detail(Long id) {
|
||||
FormalSettlement settlement = existing(id);
|
||||
FormalSettlementVO vo = toVO(settlement);
|
||||
fillKingdeeLockStatus(List.of(vo));
|
||||
List<FormalSettlementSource> sources = sourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery()
|
||||
.eq(FormalSettlementSource::getFormalSettlementId, id).orderByAsc(FormalSettlementSource::getCreateTime));
|
||||
vo.setSources(sources);
|
||||
@@ -219,6 +319,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
vo.setPayments(paymentMapper.selectList(Wrappers.<FormalSettlementPayment>lambdaQuery()
|
||||
.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)
|
||||
@@ -233,7 +336,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
query.or(wrapper -> wrapper.in(PaymentApplication::getId, relationApplicationIds));
|
||||
}
|
||||
})
|
||||
.eq(PaymentApplication::getPaymentType, "settlement_payment")
|
||||
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT)
|
||||
.eq(PaymentApplication::getIsDeleted, 0);
|
||||
vo.setPaymentApplications(paymentApplicationMapper.selectList(paymentApplicationQuery
|
||||
.orderByDesc(PaymentApplication::getCreateTime)).stream()
|
||||
@@ -304,7 +407,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
.toList();
|
||||
Map<Long, InvoiceReceipt> receiptMap = receiptIds.isEmpty() ? Map.of()
|
||||
: invoiceReceiptMapper.selectByIds(receiptIds).stream()
|
||||
.filter(receipt -> !VOIDED.equals(receipt.getApprovalStatus())
|
||||
.filter(receipt -> !TransportBusiConstant.VOIDED.equals(receipt.getApprovalStatus())
|
||||
&& !Objects.equals(receipt.getIsDeleted(), 1))
|
||||
.collect(Collectors.toMap(InvoiceReceipt::getId, Function.identity()));
|
||||
for (InvoiceReceiptSettlement relation : receiptRelations) {
|
||||
@@ -336,6 +439,31 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
return normalizedInvoiceNo.isEmpty() ? fallback : normalizedInvoiceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开票申请经金蝶回写的发票结果(A303-5 落库,含板式文件/数电PDF等文件地址):
|
||||
* 通过结算单与开票申请的关联关系查出,仅保留有效(非作废未删除)开票申请的发票,
|
||||
* 只读展示于详情页,不落 blade_formal_settlement_invoice、不参与匹配金额计算
|
||||
*/
|
||||
private List<InvoiceApplicationInvoice> listIssuedInvoices(Long settlementId) {
|
||||
List<Long> applicationIds = invoiceApplicationSettlementMapper.selectList(
|
||||
Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
|
||||
.select(InvoiceApplicationSettlement::getInvoiceApplicationId)
|
||||
.eq(InvoiceApplicationSettlement::getFormalSettlementId, settlementId)
|
||||
).stream().map(InvoiceApplicationSettlement::getInvoiceApplicationId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
if (applicationIds.isEmpty()) return List.of();
|
||||
Set<Long> activeApplicationIds = invoiceApplicationMapper.selectBatchIds(applicationIds).stream()
|
||||
.filter(application -> !TransportBusiConstant.VOIDED.equals(application.getApprovalStatus())
|
||||
&& !Objects.equals(application.getIsDeleted(), 1))
|
||||
.map(InvoiceApplication::getId).collect(Collectors.toSet());
|
||||
if (activeApplicationIds.isEmpty()) return List.of();
|
||||
return invoiceApplicationInvoiceMapper.selectList(
|
||||
Wrappers.<InvoiceApplicationInvoice>lambdaQuery()
|
||||
.in(InvoiceApplicationInvoice::getInvoiceApplicationId, activeApplicationIds)
|
||||
.orderByAsc(InvoiceApplicationInvoice::getInvoiceApplicationId)
|
||||
.orderByAsc(InvoiceApplicationInvoice::getCreateTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long saveDraft(FormalSettlementSaveRequest request) {
|
||||
@@ -387,7 +515,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
}
|
||||
if (creating || restoring) {
|
||||
if (Func.isEmpty(settlement.getFormalSettlementNo())) settlement.setFormalSettlementNo(nextNo(settlementType));
|
||||
settlement.setApprovalStatus(DRAFT);
|
||||
settlement.setApprovalStatus(TransportBusiConstant.DRAFT);
|
||||
settlement.setCurrentNode("草稿");
|
||||
settlement.setSourceType(sources.isEmpty() ? "应收应付" : directDetails.isEmpty() ? "预结算合并" : "混合来源");
|
||||
settlement.setInvoiceStatus("unreceived");
|
||||
@@ -417,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());
|
||||
@@ -438,32 +572,36 @@ 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);
|
||||
}
|
||||
|
||||
@Override public void submit(FormalSettlementStatusRequest request) { changeStatus(request.getId(), DRAFT, REVIEWING, "财务审核", null); }
|
||||
@Override public void returnBill(FormalSettlementStatusRequest request) { changeStatus(request.getId(), REVIEWING, RETURNED, "已驳回", request.getReason()); }
|
||||
@Override public void submit(FormalSettlementStatusRequest request) { changeStatus(request.getId(), TransportBusiConstant.DRAFT, TransportBusiConstant.REVIEWING, "财务审核", null); }
|
||||
@Override public void returnBill(FormalSettlementStatusRequest request) { changeStatus(request.getId(), TransportBusiConstant.REVIEWING, TransportBusiConstant.RETURNED, "已驳回", request.getReason()); }
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void approve(FormalSettlementStatusRequest request) {
|
||||
FormalSettlement settlement = existing(request.getId());
|
||||
if (!REVIEWING.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批中的正式结算单允许审核");
|
||||
settlement.setApprovalStatus(APPROVED);
|
||||
if (!TransportBusiConstant.REVIEWING.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批中的正式结算单允许审核");
|
||||
settlement.setApprovalStatus(TransportBusiConstant.APPROVED);
|
||||
settlement.setCurrentNode("审批通过");
|
||||
settlement.setCurrentProcessor(AuthUtil.getUserName());
|
||||
settlement.setApprovedTime(LocalDateTime.now());
|
||||
updateById(settlement);
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "正式结算审批通过", request.getReason());
|
||||
// 审批事务提交后自动推送金蝶(AFTER_COMMIT 监听器消费,推送失败不影响审批结果)
|
||||
eventPublisher.publishEvent(new KingdeeBillApprovedEvent(KingdeeBillApprovedEvent.BIZ_FORMAL_SETTLEMENT, settlement.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void voidBill(FormalSettlementStatusRequest request) {
|
||||
FormalSettlement settlement = existing(request.getId());
|
||||
if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许作废");
|
||||
if (!TransportBusiConstant.APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许作废");
|
||||
if ("synced".equals(settlement.getKingdeeSyncStatus())) throw new ServiceException("已同步金蝶的正式结算单不能直接作废");
|
||||
settlement.setApprovalStatus(VOIDED);
|
||||
settlement.setApprovalStatus(TransportBusiConstant.VOIDED);
|
||||
settlement.setCurrentNode("已作废");
|
||||
settlement.setVoidReason(required(limit(request.getReason(), 200), "作废原因"));
|
||||
updateById(settlement);
|
||||
@@ -471,17 +609,345 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
}
|
||||
|
||||
@Override
|
||||
public String syncKingdee(Long id) {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void voidByKingdee(Long id, String operator) {
|
||||
FormalSettlement settlement = existing(id);
|
||||
if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许同步金蝶");
|
||||
log.info("正式结算单[{}]金蝶删单回调废除:金蝶操作人={}", settlement.getFormalSettlementNo(), operator);
|
||||
// 作废并还原同步信息为推送前状态:updateById 忽略 null 字段,清空字段须显式 set null
|
||||
this.update(Wrappers.<FormalSettlement>lambdaUpdate()
|
||||
.eq(FormalSettlement::getId, id)
|
||||
.set(FormalSettlement::getApprovalStatus, TransportBusiConstant.VOIDED)
|
||||
.set(FormalSettlement::getCurrentNode, "已作废")
|
||||
.set(FormalSettlement::getCurrentProcessor, operator)
|
||||
.set(FormalSettlement::getVoidReason, "金蝶侧删单回调自动废除")
|
||||
.set(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_UNSYNCED)
|
||||
.set(FormalSettlement::getKingdeeBillNo, null)
|
||||
.set(FormalSettlement::getKingdeeBillId, null)
|
||||
.set(FormalSettlement::getSyncedTime, null));
|
||||
// 变更流水:回调无登录态,操作人取金蝶报文(兜底 system,与 saveChange 同款)
|
||||
FormalSettlementChangeRecord record = new FormalSettlementChangeRecord();
|
||||
record.setFormalSettlementId(id);
|
||||
record.setChangeType("结算单基本信息");
|
||||
record.setOperationType("调整");
|
||||
record.setChangeContent("金蝶删单回调自动废除并还原同步信息(金蝶单据已删除,可还原重推)");
|
||||
record.setChangeReason("金蝶操作人:" + Func.toStr(operator));
|
||||
record.setOperatorName(Func.isEmpty(operator) ? "system" : operator);
|
||||
record.setChangeTime(LocalDateTime.now());
|
||||
changeRecordMapper.insert(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String syncKingdee(Long id) {
|
||||
String billNo;
|
||||
try {
|
||||
// 单据级事务经自代理调用:单条失败只回滚自身
|
||||
billNo = SpringUtil.getBean(IFormalSettlementService.class).doSyncKingdee(id);
|
||||
} catch (Exception exception) {
|
||||
log.error("正式结算单[{}]推送金蝶异常", id, exception);
|
||||
// 推送事务已回滚、行锁已释放,此处独立事务落"生成失败"状态(与付款/收票口径统一,失败可被重推任务续推)
|
||||
self().markKingdeeSyncFailed(id, exception.getMessage());
|
||||
throw exception;
|
||||
}
|
||||
// 主单事务提交后锁定金蝶单据(lockBusiData):失败只留 failed 锁记录由补偿任务重锁,不污染主单同步状态;
|
||||
// 锁定失败摘要拼入返回值,页面直接可见
|
||||
String lockFailSummary = lockKingdeeBillAfterPush(id, billNo);
|
||||
// 主单事务提交后联带上传附件(A902):失败只记附件同步状态,不回滚不污染主单同步状态,重推自动补传
|
||||
syncAttachmentsAfterPush(id);
|
||||
return Func.isBlank(lockFailSummary) ? billNo
|
||||
: billNo + ";注意:金蝶单据锁定未成功(" + lockFailSummary + "),系统将自动补偿重锁,重推同步金蝶可立即补锁";
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定(lockBusiData):A102-2/A302-2 推送成功事务提交后调用,目标单据类型按结算类型路由;
|
||||
* 锁定异常独立捕获,不影响主单同步结果
|
||||
*
|
||||
* @param id 正式结算单id
|
||||
* @param billNo 金蝶单据编号
|
||||
* @return 锁定失败的金蝶单号摘要,全部锁定成功返回 null
|
||||
*/
|
||||
private String lockKingdeeBillAfterPush(Long id, String billNo) {
|
||||
try {
|
||||
if (Func.isBlank(billNo)) {
|
||||
return null;
|
||||
}
|
||||
FormalSettlement settlement = getById(id);
|
||||
if (settlement == null) {
|
||||
return null;
|
||||
}
|
||||
return kingdeeBillLockService.lockAfterPush(KingdeeLockConstant.SOURCE_SETTLEMENT, id, settlement.getFormalSettlementNo(),
|
||||
"receivable".equals(settlement.getSettlementType())
|
||||
? KingdeeLockConstant.TARGET_AR_BILL : KingdeeLockConstant.TARGET_AP_BILL,
|
||||
Func.toStrList(billNo));
|
||||
} catch (Exception exception) {
|
||||
log.error("正式结算单[{}]金蝶单据锁定联带动作异常(锁记录已落库待补偿)", id, exception);
|
||||
return "锁定动作异常,已留档待补偿";
|
||||
}
|
||||
}
|
||||
|
||||
/** 自代理:跨方法调用确保事务注解生效(失败状态独立事务) */
|
||||
private IFormalSettlementService self() {
|
||||
return SpringUtil.getBean(IFormalSettlementService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void markKingdeeSyncFailed(Long id, String reason) {
|
||||
if (id == null) return;
|
||||
// 条件更新:仅审批通过且未同步的单据落"生成失败"状态,未审批/已同步/已删除单据自动跳过保持原状态
|
||||
int updated = baseMapper.update(null, Wrappers.<FormalSettlement>lambdaUpdate()
|
||||
.eq(FormalSettlement::getId, id)
|
||||
.eq(FormalSettlement::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.ne(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED)
|
||||
.set(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
|
||||
if (updated > 0) {
|
||||
// 失败原因写变更记录留档(推送事务已回滚,此处独立事务);原因过长截断留档,不抛错
|
||||
String failReason = Func.toStrWithEmpty(reason, "未知异常");
|
||||
saveChange(id, "结算单基本信息", null, "调整",
|
||||
"同步金蝶失败:" + (failReason.length() > 200 ? failReason.substring(0, 200) : failReason), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retryKingdeeFailed() {
|
||||
List<Long> failedIds = list(Wrappers.<FormalSettlement>lambdaQuery()
|
||||
.eq(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
|
||||
.eq(FormalSettlement::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.orderByAsc(FormalSettlement::getCreateTime))
|
||||
.stream().map(FormalSettlement::getId).toList();
|
||||
return executeKingdeeRetry(failedIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐单重推失败单据:单条失败独立 try/catch 不中断整批,返回执行摘要。
|
||||
* 推送前预检(A102-4/A302-4 幂等回填)保证重推不重复生成金蝶单据。
|
||||
*
|
||||
* @param failedIds 失败单据id集合
|
||||
* @return 执行摘要
|
||||
*/
|
||||
private String executeKingdeeRetry(List<Long> failedIds) {
|
||||
if (Func.isEmpty(failedIds)) {
|
||||
return "无生成失败单据";
|
||||
}
|
||||
int success = 0;
|
||||
List<String> failures = new ArrayList<>();
|
||||
for (Long id : failedIds) {
|
||||
try {
|
||||
syncKingdee(id);
|
||||
success++;
|
||||
} catch (Exception exception) {
|
||||
failures.add(id + ":" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
String summary = "重推生成失败单据:本批 " + failedIds.size() + " 条,成功 " + success + " 条";
|
||||
if (!failures.isEmpty()) {
|
||||
summary = summary + ",失败 " + failures.size() + " 条:" + String.join(";", failures);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 附件联带上传(A902):主单推送成功(含幂等预检命中)后调用,以本地 kingdeeBillId 定位金蝶单据;
|
||||
* 本地缺失(存量数据/幂等跳过路径未带回内码)时经 A102-4/A302-4 按防重键反查回填;
|
||||
* 附件上传异常独立捕获,绝不影响主单同步结果
|
||||
*
|
||||
* @param id 正式结算单id
|
||||
*/
|
||||
private void syncAttachmentsAfterPush(Long id) {
|
||||
try {
|
||||
FormalSettlement settlement = getById(id);
|
||||
if (settlement == null || !"synced".equals(settlement.getKingdeeSyncStatus())) {
|
||||
return;
|
||||
}
|
||||
String billPkId = settlement.getKingdeeBillId();
|
||||
if (Func.isBlank(billPkId)) {
|
||||
billPkId = resolveKingdeeBillId(settlement);
|
||||
if (Func.isNotBlank(billPkId)) {
|
||||
log.info("正式结算单[{}]经 A102-4/A302-4 反查回填星瀚单据内码:{}", settlement.getFormalSettlementNo(), billPkId);
|
||||
FormalSettlement patch = new FormalSettlement();
|
||||
patch.setId(settlement.getId());
|
||||
patch.setKingdeeBillId(billPkId);
|
||||
updateById(patch);
|
||||
}
|
||||
}
|
||||
if (Func.isBlank(billPkId)) {
|
||||
log.warn("正式结算单[{}]金蝶附件联带上传跳过:星瀚单据内码未落库且反查未命中,待重推补传",
|
||||
settlement.getFormalSettlementNo());
|
||||
return;
|
||||
}
|
||||
String bizType = "receivable".equals(settlement.getSettlementType())
|
||||
? KingdeeAttachmentConstant.BIZ_FORMAL_SETTLEMENT_AR : KingdeeAttachmentConstant.BIZ_FORMAL_SETTLEMENT_AP;
|
||||
kingdeeAttachmentSyncSupport.syncUserAttachments(bizType, id, billPkId, settlement.getAttachmentsJson());
|
||||
} catch (Exception exception) {
|
||||
log.error("正式结算单[{}]金蝶附件联带上传异常(不影响主单同步状态,重推自动补传)", id, exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反查星瀚单据内码:按结算类型组装防重键(与 A102-2/A302-2 传参一致)后调用对应查询接口
|
||||
*
|
||||
* @param settlement 正式结算单
|
||||
* @return 星瀚单据内码,未查得返回 null
|
||||
*/
|
||||
private String resolveKingdeeBillId(FormalSettlement settlement) {
|
||||
List<FormalSettlementDetail> details = detailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
|
||||
.eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())
|
||||
.eq(FormalSettlementDetail::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetail::getLineNo));
|
||||
if (Func.isEmpty(details)) {
|
||||
return null;
|
||||
}
|
||||
if ("receivable".equals(settlement.getSettlementType())) {
|
||||
return kingdeeArBillSyncSupport.queryArBillExistId(kingdeeArBillSyncSupport.arBillSrcCode(settlement, details));
|
||||
}
|
||||
return kingdeeApBillSyncSupport.queryApBillExistId(kingdeeApBillSyncSupport.apBillSrcCode(settlement, details));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String doSyncKingdee(Long id) {
|
||||
FormalSettlement settlement = existing(id);
|
||||
if (!TransportBusiConstant.APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许同步金蝶");
|
||||
// 已同步幂等返回(含历史由付款申请同步联动回写的存量已同步数据);星瀚内码由附件补传分支反查回填
|
||||
if ("synced".equals(settlement.getKingdeeSyncStatus())) return settlement.getKingdeeBillNo();
|
||||
String kingdeeNo = "K3AP" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
|
||||
settlement.setKingdeeBillNo(kingdeeNo);
|
||||
settlement.setKingdeeSyncStatus("synced");
|
||||
// 流程拆分:正式结算单「同步金蝶」负责推送财务应付单(A102-2)并回写结算单单据编号;
|
||||
// 付款申请「同步金蝶」仅负责付款申请单(A103-1 应付下推),推送前经 A102-4 预检本单应付单已生成
|
||||
List<FormalSettlementDetail> details = detailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
|
||||
.eq(FormalSettlementDetail::getFormalSettlementId, id)
|
||||
.eq(FormalSettlementDetail::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetail::getLineNo));
|
||||
if (Func.isEmpty(details)) {
|
||||
throw new ServiceException("推送金蝶失败:正式结算单无结算明细,无法解析物料与计量单位");
|
||||
}
|
||||
KingdeeSyncContext context = KingdeeSyncContext.empty();
|
||||
// 单据类型路由:应付对应财务应付单(A102-2),应收对应财务应收单(A302-2),两链路同一套幂等预检与推送逻辑
|
||||
String billNo;
|
||||
String billPkId;
|
||||
String changeDesc;
|
||||
if ("receivable".equals(settlement.getSettlementType())) {
|
||||
// 应收结算:财务应收单防重键(awrp_src_code):正式结算单号_结算明细单据号下划线拼接
|
||||
String arBillSrcCode = kingdeeArBillSyncSupport.arBillSrcCode(settlement, details);
|
||||
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups = kingdeeArBillSyncSupport.resolveArEntryGroups(settlement, details, context);
|
||||
KingdeeApBillSyncSupport.BfiApBillPushOutcome outcome = kingdeeArBillSyncSupport.pushArBill(
|
||||
arBillSrcCode, settlement, entryGroups, settlement.getFormalSettlementNo(), context);
|
||||
billNo = outcome.billNo();
|
||||
billPkId = outcome.billId();
|
||||
changeDesc = "同步金蝶财务应收单" + billNo;
|
||||
} else {
|
||||
// 应付结算:财务应付单防重键,付款申请「同步金蝶」(A103-1 应付下推)推送前按该键经 A102-4 预检本单应付单已生成
|
||||
// 付款申请 A103-1 下推前的前置
|
||||
String apBillSrcCode = kingdeeApBillSyncSupport.apBillSrcCode(settlement, details);
|
||||
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups = kingdeeApBillSyncSupport.resolveEntryGroups(settlement, details, context);
|
||||
KingdeeApBillSyncSupport.BfiApBillPushOutcome outcome = kingdeeApBillSyncSupport.pushApBill(
|
||||
apBillSrcCode, settlement, entryGroups, settlement.getFormalSettlementNo(), context);
|
||||
billNo = outcome.billNo();
|
||||
billPkId = outcome.billId();
|
||||
changeDesc = "同步金蝶财务应付单" + billNo;
|
||||
}
|
||||
// 回写金蝶单据编号(A102-2/A302-2 单据编号)、星瀚单据内码(A902 附件上传定位用)与同步状态;
|
||||
// 付款申请侧回填的付款申请单编号(A103-2)不是同一单据
|
||||
settlement.setKingdeeBillNo(billNo);
|
||||
settlement.setKingdeeBillId(billPkId);
|
||||
settlement.setKingdeeSyncStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
|
||||
settlement.setSyncedTime(LocalDateTime.now());
|
||||
updateById(settlement);
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "同步金蝶单据" + kingdeeNo, "");
|
||||
return kingdeeNo;
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", changeDesc, "");
|
||||
log.info("正式结算单[{}]同步金蝶完成:settlementType={},billNo={},billPkId={}",
|
||||
settlement.getFormalSettlementNo(), settlement.getSettlementType(), billNo, billPkId);
|
||||
return billNo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String syncKingdeeArResult() {
|
||||
return kingdeeSyncLockSupport.runSingleFlight(AR_RESULT_SYNC_LOCK_PREFIX, bfiProperties.getArResult().getLockLeaseSeconds(),
|
||||
"金蝶应收单勾稽", this::executeArResultSync);
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶应收单勾稽主体:A302-4 批量查询财务应收单回填单据编号、生成失败纠偏为已同步,
|
||||
* 由 {@link #syncKingdeeArResult()} 在分布式锁保护下调用。
|
||||
* <p>分批与事务说明:
|
||||
* ① 结算明细按 {@link #AR_RESULT_DETAIL_QUERY_BATCH} 分批 in 查询后内存分组,消除逐单查询 N+1;
|
||||
* ② 回写聚合后经 {@code updateBatchById} 批量落库——本方法为 this 自调用,MP 自带的
|
||||
* {@code @Transactional} 不生效,但其 executeBatch 会独立开启 BATCH SqlSession 并在结束时统一提交、
|
||||
* 异常整体回滚,等效单次原子写;③ 整轮任务不包外层事务(远程星瀚查询不占库连接,勾稽按防重键
|
||||
* 幂等可重跑),private + lambda 自调用上补加 {@code @Transactional} 会静默失效,禁止添加。
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
private String executeArResultSync() {
|
||||
long startMillis = System.currentTimeMillis();
|
||||
// 待勾稽应收结算单:审批通过 + 结算类型应收 + 金蝶状态为 已同步/生成失败(已同步一并轮询为补齐单号,生成失败纳入勾稽纠偏:
|
||||
// 金蝶侧已生成说明上次推送实际成功,查到即回填纠正为已同步,避免人工重新推送产生重复单据)
|
||||
List<FormalSettlement> settlements = list(Wrappers.<FormalSettlement>lambdaQuery()
|
||||
.eq(FormalSettlement::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.eq(FormalSettlement::getSettlementType, "receivable")
|
||||
.in(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED,
|
||||
KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
|
||||
.orderByAsc(FormalSettlement::getId));
|
||||
if (Func.isEmpty(settlements)) {
|
||||
log.info("金蝶应收单勾稽:无待勾稽应收结算单");
|
||||
return "金蝶应收单勾稽:无待勾稽应收结算单";
|
||||
}
|
||||
long syncedCount = settlements.stream()
|
||||
.filter(item -> KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(item.getKingdeeSyncStatus())).count();
|
||||
log.info("金蝶应收单勾稽开始:待勾稽应收结算单{}笔(已同步{}笔,生成失败{}笔)",
|
||||
settlements.size(), syncedCount, settlements.size() - syncedCount);
|
||||
// 结算明细分批一次查全(消除逐单查询 N+1),按结算单 id 分组
|
||||
List<Long> settlementIds = settlements.stream().map(FormalSettlement::getId).toList();
|
||||
Map<Long, List<FormalSettlementDetail>> detailsBySettlement = new HashMap<>();
|
||||
kingdeeApBillSyncSupport.batchForeach(settlementIds, AR_RESULT_DETAIL_QUERY_BATCH, batch ->
|
||||
detailsBySettlement.putAll(detailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
|
||||
.in(FormalSettlementDetail::getFormalSettlementId, batch)
|
||||
.eq(FormalSettlementDetail::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetail::getLineNo)).stream()
|
||||
.collect(Collectors.groupingBy(FormalSettlementDetail::getFormalSettlementId))));
|
||||
// 组装防重键(与 A302-2 保存传参一致),同一防重键对应一张财务应收单
|
||||
Map<String, FormalSettlement> settlementBySrcCode = new LinkedHashMap<>();
|
||||
for (FormalSettlement settlement : settlements) {
|
||||
String srcCode = kingdeeArBillSyncSupport.arBillSrcCode(settlement,
|
||||
detailsBySettlement.getOrDefault(settlement.getId(), List.of()));
|
||||
if (Func.isNotBlank(srcCode)) {
|
||||
settlementBySrcCode.putIfAbsent(srcCode, settlement);
|
||||
}
|
||||
}
|
||||
// A302-4 批量查询:查到 → 回填金蝶单据编号(缺失时)、生成失败纠偏为已同步;查不到 → 状态保持不变
|
||||
Map<String, String> existBillNos = kingdeeArBillSyncSupport.queryArBillExistNos(new ArrayList<>(settlementBySrcCode.keySet()));
|
||||
// 勾稽回写聚合后经 updateBatchById 批量更新,避免逐单 update
|
||||
// (this 自调用下 MP 事务注解不生效,executeBatch 自开 BATCH SqlSession 统一提交、异常整体回滚,等效原子写)
|
||||
List<FormalSettlement> toUpdateSettlements = new ArrayList<>();
|
||||
int corrected = 0;
|
||||
for (Map.Entry<String, FormalSettlement> entry : settlementBySrcCode.entrySet()) {
|
||||
String billNo = existBillNos.get(entry.getKey());
|
||||
if (Func.isBlank(billNo)) {
|
||||
continue;
|
||||
}
|
||||
FormalSettlement settlement = entry.getValue();
|
||||
boolean failed = KingdeeBfiConstant.KINGDEE_STATUS_FAILED.equals(settlement.getKingdeeSyncStatus());
|
||||
if (!failed && Func.isNotBlank(settlement.getKingdeeBillNo())) {
|
||||
continue;
|
||||
}
|
||||
if (failed) {
|
||||
corrected++;
|
||||
}
|
||||
if (Func.isBlank(settlement.getKingdeeBillNo())) {
|
||||
settlement.setKingdeeBillNo(billNo);
|
||||
}
|
||||
settlement.setKingdeeSyncStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
|
||||
settlement.setSyncedTime(LocalDateTime.now());
|
||||
toUpdateSettlements.add(settlement);
|
||||
// 生成失败纠偏与缺失单号补齐逐笔留痕,便于对账核查
|
||||
log.info("正式结算单[{}]金蝶应收单勾稽回写:{},billNo={}",
|
||||
settlement.getFormalSettlementNo(), failed ? "生成失败纠偏为已同步" : "补齐单据编号",
|
||||
settlement.getKingdeeBillNo());
|
||||
}
|
||||
if (Func.isNotEmpty(toUpdateSettlements)) {
|
||||
updateBatchById(toUpdateSettlements);
|
||||
log.info("金蝶应收单勾稽批量回写完成:本批更新{}笔", toUpdateSettlements.size());
|
||||
}
|
||||
String summary = "金蝶应收单勾稽完成:待查应收结算单" + settlementBySrcCode.size() + "笔,回写单据信息" + toUpdateSettlements.size()
|
||||
+ "笔(生成失败纠偏" + corrected + "笔)";
|
||||
log.info("{},耗时{}ms", summary, System.currentTimeMillis() - startMillis);
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -533,6 +999,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
if (!invoiceNumbers.add(invoiceNo)) throw new ServiceException("发票号" + invoiceNo + "重复");
|
||||
matchedTotal = matchedTotal.add(money(item.getMatchedAmount()));
|
||||
}
|
||||
// 认领闸门:确认前按票号重查金蝶复核可用性,选中到确认之间票被认领/使用/红冲在此拦截
|
||||
invoiceReceiptService.assertInvoicesClaimable(invoiceNumbers);
|
||||
if (matchedTotal.compareTo(money(settlement.getSettlementAmount())) > 0) {
|
||||
throw new ServiceException("发票匹配结算单金额合计不能超过结算金额");
|
||||
}
|
||||
@@ -594,12 +1062,12 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
}
|
||||
|
||||
private String createPayment(FormalSettlement settlement, BigDecimal appliedAmount, String remark) {
|
||||
if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许发起付款申请");
|
||||
if (!TransportBusiConstant.APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许发起付款申请");
|
||||
if (!"payable".equals(settlement.getSettlementType())) throw new ServiceException("仅应付正式结算单允许发起付款申请");
|
||||
long activePaymentCount = paymentMapper.selectCount(Wrappers.<FormalSettlementPayment>lambdaQuery()
|
||||
.eq(FormalSettlementPayment::getFormalSettlementId, settlement.getId())
|
||||
.eq(FormalSettlementPayment::getIsDeleted, 0)
|
||||
.notIn(FormalSettlementPayment::getBillStatus, RETURNED, VOIDED));
|
||||
.notIn(FormalSettlementPayment::getBillStatus, TransportBusiConstant.RETURNED, TransportBusiConstant.VOIDED));
|
||||
if (activePaymentCount > 0) throw new ServiceException("该结算单正在申请结算尾款,不允许再次提交申请付款");
|
||||
BigDecimal amount = positive(appliedAmount, "申请付款金额");
|
||||
BigDecimal available = money(settlement.getSettlementAmount()).subtract(money(settlement.getAppliedPaymentAmount()));
|
||||
@@ -610,7 +1078,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
payment.setPaymentType("final");
|
||||
payment.setAppliedAmount(amount);
|
||||
payment.setPaidAmount(BigDecimal.ZERO);
|
||||
payment.setBillStatus(REVIEWING);
|
||||
payment.setBillStatus(TransportBusiConstant.REVIEWING);
|
||||
payment.setRemark(limit(remark, 200));
|
||||
paymentMapper.insert(payment);
|
||||
refreshPaymentSummary(settlement.getId());
|
||||
@@ -962,15 +1430,104 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
updateById(settlement);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量刷新收款汇总:固定数次查询完成 N 张结算单的重算,避免逐单 refreshPaymentSummary 的 N+1。
|
||||
* 计算口径与单笔版一致(数据由批量查询供给,纯计算复用 calculatePaymentSummary 重载)。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void refreshPaymentSummaries(Collection<Long> settlementIds) {
|
||||
List<Long> ids = settlementIds == null ? List.of() : settlementIds.stream()
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 结算单一次锁行查询替代逐单 selectOne(id 升序加锁,与逐单版加锁顺序一致防死锁)
|
||||
List<FormalSettlement> settlements = baseMapper.selectList(Wrappers.<FormalSettlement>lambdaQuery()
|
||||
.in(FormalSettlement::getId, ids)
|
||||
.orderByAsc(FormalSettlement::getId)
|
||||
.last("FOR UPDATE"));
|
||||
if (settlements.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<Long> settledIds = settlements.stream().map(FormalSettlement::getId).toList();
|
||||
boolean hasPayable = settlements.stream().anyMatch(item -> "payable".equals(item.getSettlementType()));
|
||||
boolean hasReceivable = settlements.stream().anyMatch(item -> "receivable".equals(item.getSettlementType()));
|
||||
|
||||
// 结算-付款申请关系(全量)
|
||||
Map<Long, List<PaymentApplicationSettlement>> relationsBySettlement = paymentApplicationSettlementMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.in(PaymentApplicationSettlement::getFormalSettlementId, settledIds)
|
||||
.eq(PaymentApplicationSettlement::getIsDeleted, 0)).stream()
|
||||
.collect(Collectors.groupingBy(PaymentApplicationSettlement::getFormalSettlementId));
|
||||
// 付款申请(按结算单直挂,payment_type=结算)
|
||||
Map<Long, List<PaymentApplication>> applicationsBySettlement = paymentApplicationMapper.selectList(
|
||||
Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.in(PaymentApplication::getSettlementId, settledIds)
|
||||
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT)
|
||||
.eq(PaymentApplication::getIsDeleted, 0)).stream()
|
||||
.collect(Collectors.groupingBy(PaymentApplication::getSettlementId));
|
||||
// 关系表引用的付款申请状态(全量一次)
|
||||
Set<Long> relationApplicationIds = relationsBySettlement.values().stream()
|
||||
.flatMap(List::stream).map(PaymentApplicationSettlement::getPaymentApplicationId)
|
||||
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
Map<Long, String> relationStatuses = relationApplicationIds.isEmpty() ? Map.of()
|
||||
: paymentApplicationMapper.selectList(Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.select(PaymentApplication::getId, PaymentApplication::getApprovalStatus)
|
||||
.in(PaymentApplication::getId, relationApplicationIds)
|
||||
.eq(PaymentApplication::getIsDeleted, 0)).stream()
|
||||
.collect(Collectors.toMap(PaymentApplication::getId, PaymentApplication::getApprovalStatus,
|
||||
(first, second) -> first));
|
||||
// 预收来源(仅应付结算单需要)
|
||||
Map<Long, List<FormalSettlementSource>> sourcesBySettlement = hasPayable
|
||||
? sourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery()
|
||||
.in(FormalSettlementSource::getFormalSettlementId, settledIds)
|
||||
.eq(FormalSettlementSource::getIsDeleted, 0)).stream()
|
||||
.collect(Collectors.groupingBy(FormalSettlementSource::getFormalSettlementId))
|
||||
: Map.of();
|
||||
// 收款认领分摊聚合(仅应收结算单需要)
|
||||
Map<Long, BigDecimal> receiptClaimPaidBySettlement = hasReceivable
|
||||
? receiptClaimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
|
||||
.in(ReceiptClaimSettlement::getFormalSettlementId, settledIds)
|
||||
.eq(ReceiptClaimSettlement::getStatus, 1)).stream()
|
||||
.collect(Collectors.groupingBy(ReceiptClaimSettlement::getFormalSettlementId,
|
||||
Collectors.reducing(BigDecimal.ZERO,
|
||||
item -> money(item.getAllocatedReceiptAmount()), BigDecimal::add)))
|
||||
: Map.of();
|
||||
|
||||
for (FormalSettlement settlement : settlements) {
|
||||
List<PaymentApplicationSettlement> relations = relationsBySettlement
|
||||
.getOrDefault(settlement.getId(), List.of());
|
||||
// 直挂付款申请需排除该结算单关系表已引用的申请(与单笔版 notIn 语义一致)
|
||||
Set<Long> ownRelationApplicationIds = relations.stream()
|
||||
.map(PaymentApplicationSettlement::getPaymentApplicationId)
|
||||
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
List<PaymentApplication> directApplications = applicationsBySettlement
|
||||
.getOrDefault(settlement.getId(), List.of()).stream()
|
||||
.filter(item -> !ownRelationApplicationIds.contains(item.getId()))
|
||||
.toList();
|
||||
PaymentSummary summary = calculatePaymentSummary(settlement, new PaymentSummaryData(
|
||||
relations, directApplications, relationStatuses,
|
||||
sourcesBySettlement.getOrDefault(settlement.getId(), List.of()),
|
||||
receiptClaimPaidBySettlement.getOrDefault(settlement.getId(), BigDecimal.ZERO)));
|
||||
settlement.setAppliedPaymentAmount(summary.appliedAmount());
|
||||
settlement.setPaidAmount(summary.paidAmount());
|
||||
settlement.setRemainingPayableAmount(money(settlement.getSettlementAmount())
|
||||
.subtract(summary.paidAmount()).max(BigDecimal.ZERO));
|
||||
settlement.setPaymentStatus(paymentStatus(summary.paidAmount(), settlement.getSettlementAmount()));
|
||||
}
|
||||
updateBatchById(settlements);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void refreshPaymentSummariesForPreSettlement(Long preSettlementId) {
|
||||
if (preSettlementId == null) return;
|
||||
sourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery()
|
||||
.eq(FormalSettlementSource::getPreSettlementId, preSettlementId)
|
||||
.eq(FormalSettlementSource::getIsDeleted, 0)).stream()
|
||||
.map(FormalSettlementSource::getFormalSettlementId).distinct()
|
||||
.forEach(this::refreshPaymentSummary);
|
||||
List<Long> settlementIds = sourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery()
|
||||
.eq(FormalSettlementSource::getPreSettlementId, preSettlementId)
|
||||
.eq(FormalSettlementSource::getIsDeleted, 0)).stream()
|
||||
.map(FormalSettlementSource::getFormalSettlementId).filter(Objects::nonNull).distinct().toList();
|
||||
refreshPaymentSummaries(settlementIds);
|
||||
}
|
||||
|
||||
private List<FormalSettlementSummaryFee> listSummaryFees(Long settlementId) {
|
||||
@@ -1056,7 +1613,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
sourceMapper.insert(relation);
|
||||
int reserved = preSettlementMapper.update(null, Wrappers.<PreSettlement>lambdaUpdate()
|
||||
.eq(PreSettlement::getId, source.getId())
|
||||
.eq(PreSettlement::getApprovalStatus, APPROVED)
|
||||
.eq(PreSettlement::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.and(w -> w.isNull(PreSettlement::getFormalSettlementNo).or().eq(PreSettlement::getFormalSettlementNo, ""))
|
||||
.set(PreSettlement::getFormalSettlementNo, settlement.getFormalSettlementNo())
|
||||
.set(PreSettlement::getFormalSettledTime, LocalDateTime.now())
|
||||
@@ -1173,54 +1730,65 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
}
|
||||
|
||||
private PaymentSummary calculatePaymentSummary(FormalSettlement settlement) {
|
||||
LambdaQueryWrapper<PaymentApplication> query = Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.eq(PaymentApplication::getSettlementId, settlement.getId())
|
||||
.eq(PaymentApplication::getPaymentType, "settlement_payment")
|
||||
.eq(PaymentApplication::getIsDeleted, 0);
|
||||
List<PaymentApplicationSettlement> relations = paymentApplicationSettlementMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getFormalSettlementId, settlement.getId())
|
||||
.eq(PaymentApplicationSettlement::getIsDeleted, 0));
|
||||
Set<Long> relationApplicationIds = relations.stream().map(PaymentApplicationSettlement::getPaymentApplicationId)
|
||||
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
LambdaQueryWrapper<PaymentApplication> query = Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.eq(PaymentApplication::getSettlementId, settlement.getId())
|
||||
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT)
|
||||
.eq(PaymentApplication::getIsDeleted, 0);
|
||||
if (!relationApplicationIds.isEmpty()) query.notIn(PaymentApplication::getId, relationApplicationIds);
|
||||
List<PaymentApplication> applications = paymentApplicationMapper.selectList(query);
|
||||
BigDecimal appliedAmount = applications.stream()
|
||||
.filter(item -> REVIEWING.equals(item.getApprovalStatus()))
|
||||
.map(PaymentApplication::getAppliedAmount).map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal paidAmount = applications.stream()
|
||||
.filter(item -> APPROVED.equals(item.getApprovalStatus()))
|
||||
.map(PaymentApplication::getPaidAmount).map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
Map<Long, String> relationStatuses = relationApplicationIds.isEmpty() ? Map.of() : paymentApplicationMapper.selectList(
|
||||
Wrappers.<PaymentApplication>lambdaQuery().select(PaymentApplication::getId, PaymentApplication::getApprovalStatus)
|
||||
.in(PaymentApplication::getId, relationApplicationIds).eq(PaymentApplication::getIsDeleted, 0))
|
||||
.stream().collect(Collectors.toMap(PaymentApplication::getId, PaymentApplication::getApprovalStatus));
|
||||
appliedAmount = appliedAmount.add(relations.stream()
|
||||
.filter(item -> REVIEWING.equals(relationStatuses.get(item.getPaymentApplicationId())))
|
||||
return calculatePaymentSummary(settlement, new PaymentSummaryData(relations, applications,
|
||||
relationStatuses, null, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 收款汇总纯计算:查询数据由调用方供给(单笔版自行查询;批量版由 refreshPaymentSummaries 一次供给多单)。
|
||||
* sources / receiptClaimPaid 为 null 时按需自行查询(仅 payable/receivable 分支使用)。
|
||||
*/
|
||||
private PaymentSummary calculatePaymentSummary(FormalSettlement settlement, PaymentSummaryData data) {
|
||||
BigDecimal appliedAmount = data.directApplications().stream()
|
||||
.filter(item -> TransportBusiConstant.REVIEWING.equals(item.getApprovalStatus()))
|
||||
.map(PaymentApplication::getAppliedAmount).map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal paidAmount = data.directApplications().stream()
|
||||
.filter(item -> TransportBusiConstant.APPROVED.equals(item.getApprovalStatus()))
|
||||
.map(PaymentApplication::getPaidAmount).map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
appliedAmount = appliedAmount.add(data.relations().stream()
|
||||
.filter(item -> TransportBusiConstant.REVIEWING.equals(data.relationStatuses().get(item.getPaymentApplicationId())))
|
||||
.map(PaymentApplicationSettlement::getAppliedAmount).map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
paidAmount = paidAmount.add(relations.stream()
|
||||
.filter(item -> APPROVED.equals(relationStatuses.get(item.getPaymentApplicationId())))
|
||||
paidAmount = paidAmount.add(data.relations().stream()
|
||||
.filter(item -> TransportBusiConstant.APPROVED.equals(data.relationStatuses().get(item.getPaymentApplicationId())))
|
||||
.map(PaymentApplicationSettlement::getPaidAmount).map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
if ("payable".equals(settlement.getSettlementType())) {
|
||||
List<FormalSettlementSource> sources = sourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery()
|
||||
.eq(FormalSettlementSource::getFormalSettlementId, settlement.getId())
|
||||
.eq(FormalSettlementSource::getIsDeleted, 0));
|
||||
List<FormalSettlementSource> sources = data.sources() != null ? data.sources()
|
||||
: sourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery()
|
||||
.eq(FormalSettlementSource::getFormalSettlementId, settlement.getId())
|
||||
.eq(FormalSettlementSource::getIsDeleted, 0));
|
||||
appliedAmount = appliedAmount.add(sources.stream().map(FormalSettlementSource::getAdvanceAppliedAmount)
|
||||
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
paidAmount = paidAmount.add(sources.stream().map(FormalSettlementSource::getAdvancePaidAmount)
|
||||
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
}
|
||||
if ("receivable".equals(settlement.getSettlementType())) {
|
||||
paidAmount = receiptClaimSettlementMapper.selectList(
|
||||
Wrappers.<ReceiptClaimSettlement>lambdaQuery()
|
||||
.eq(ReceiptClaimSettlement::getFormalSettlementId, settlement.getId())
|
||||
.eq(ReceiptClaimSettlement::getStatus, 1))
|
||||
.stream().map(ReceiptClaimSettlement::getAllocatedReceiptAmount).map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
paidAmount = data.receiptClaimPaid() != null ? data.receiptClaimPaid()
|
||||
: receiptClaimSettlementMapper.selectList(
|
||||
Wrappers.<ReceiptClaimSettlement>lambdaQuery()
|
||||
.eq(ReceiptClaimSettlement::getFormalSettlementId, settlement.getId())
|
||||
.eq(ReceiptClaimSettlement::getStatus, 1))
|
||||
.stream().map(ReceiptClaimSettlement::getAllocatedReceiptAmount).map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
return new PaymentSummary(money(appliedAmount), money(paidAmount));
|
||||
}
|
||||
@@ -1239,6 +1807,20 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
private record PaymentSummary(BigDecimal appliedAmount, BigDecimal paidAmount) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 收款汇总计算的供给数据(批量场景一次查询多张结算单后按单分发)
|
||||
*
|
||||
* @param relations 结算-付款申请关系行
|
||||
* @param directApplications 直挂付款申请(已排除关系表引用的申请)
|
||||
* @param relationStatuses 付款申请id → 审批状态
|
||||
* @param sources 预收来源行(仅应付结算单;null 表示由纯计算按需查询)
|
||||
* @param receiptClaimPaid 收款认领分摊聚合(仅应收结算单;null 表示由纯计算按需查询)
|
||||
*/
|
||||
private record PaymentSummaryData(List<PaymentApplicationSettlement> relations,
|
||||
List<PaymentApplication> directApplications, Map<Long, String> relationStatuses,
|
||||
List<FormalSettlementSource> sources, BigDecimal receiptClaimPaid) {
|
||||
}
|
||||
|
||||
private SettlementAdjustmentVO toAdjustmentVO(SettlementAdjustment entity, String kingdeeBillNo) {
|
||||
SettlementAdjustmentVO vo = Objects.requireNonNull(
|
||||
BeanUtil.copyProperties(entity, SettlementAdjustmentVO.class));
|
||||
@@ -1250,12 +1832,15 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
}
|
||||
|
||||
private String approvalStatusName(String value) {
|
||||
return switch (value == null ? "" : value) {
|
||||
case DRAFT -> "草稿";
|
||||
case REVIEWING -> "审批中";
|
||||
case APPROVED -> "审批通过";
|
||||
case RETURNED -> "已驳回";
|
||||
case VOIDED -> "已作废";
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return switch (value) {
|
||||
case "draft" -> "草稿";
|
||||
case "reviewing" -> "审批中";
|
||||
case "approved" -> "审批通过";
|
||||
case "returned" -> "已驳回";
|
||||
case "voided" -> "已作废";
|
||||
default -> value;
|
||||
};
|
||||
}
|
||||
@@ -1263,7 +1848,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
private PreSettlement availableSource(Long id) {
|
||||
PreSettlement source = preSettlementMapper.selectById(id);
|
||||
if (source == null || Objects.equals(source.getIsDeleted(), 1)) throw new ServiceException("预结算单不存在");
|
||||
if (!APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("仅审批通过的预结算单可生成正式结算");
|
||||
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("仅审批通过的预结算单可生成正式结算");
|
||||
if (Func.isNotEmpty(source.getFormalSettlementNo())) throw new ServiceException("预结算单" + source.getPreSettlementNo() + "已被正式结算占用");
|
||||
return source;
|
||||
}
|
||||
@@ -1285,15 +1870,15 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
|
||||
private FormalSettlement editable(Long id) {
|
||||
FormalSettlement entity = existing(id);
|
||||
if (!DRAFT.equals(entity.getApprovalStatus()) && !RETURNED.equals(entity.getApprovalStatus())) throw new ServiceException("仅草稿或已驳回的正式结算单允许编辑");
|
||||
if (!TransportBusiConstant.DRAFT.equals(entity.getApprovalStatus()) && !TransportBusiConstant.RETURNED.equals(entity.getApprovalStatus())) throw new ServiceException("仅草稿或已驳回的正式结算单允许编辑");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void changeStatus(Long id, String expected, String target, String node, String reason) {
|
||||
FormalSettlement settlement = existing(id);
|
||||
if (!expected.equals(settlement.getApprovalStatus()) && !(DRAFT.equals(expected) && RETURNED.equals(settlement.getApprovalStatus()))) throw new ServiceException("当前状态不允许该操作");
|
||||
if (!expected.equals(settlement.getApprovalStatus()) && !(TransportBusiConstant.DRAFT.equals(expected) && TransportBusiConstant.RETURNED.equals(settlement.getApprovalStatus()))) throw new ServiceException("当前状态不允许该操作");
|
||||
settlement.setApprovalStatus(target); settlement.setCurrentNode(node); settlement.setCurrentProcessor(AuthUtil.getUserName());
|
||||
if (RETURNED.equals(target)) settlement.setVoidReason(limit(reason, 200));
|
||||
if (TransportBusiConstant.RETURNED.equals(target)) settlement.setVoidReason(limit(reason, 200));
|
||||
updateById(settlement);
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", node, reason);
|
||||
}
|
||||
|
||||
+972
-29
File diff suppressed because it is too large
Load Diff
+1480
-83
File diff suppressed because it is too large
Load Diff
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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.service.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.transport.mapper.KingdeeAttachmentSyncMapper;
|
||||
import org.springblade.transport.pojo.entity.KingdeeAttachmentSync;
|
||||
import org.springblade.transport.service.IKingdeeAttachmentSyncService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 金蝶附件同步服务实现
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class KingdeeAttachmentSyncServiceImpl extends BaseServiceImpl<KingdeeAttachmentSyncMapper, KingdeeAttachmentSync>
|
||||
implements IKingdeeAttachmentSyncService {
|
||||
}
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* 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 com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseEntity;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.constant.KingdeeLockConstant;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.mapper.FormalSettlementMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationMapper;
|
||||
import org.springblade.transport.mapper.KingdeeBillLockMapper;
|
||||
import org.springblade.transport.mapper.PaymentApplicationMapper;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplication;
|
||||
import org.springblade.transport.pojo.entity.KingdeeBillLock;
|
||||
import org.springblade.transport.pojo.entity.PaymentApplication;
|
||||
import org.springblade.transport.pojo.vo.KingdeeUnlockMark;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IInvoiceApplicationService;
|
||||
import org.springblade.transport.service.IKingdeeBillLockService;
|
||||
import org.springblade.transport.service.IPaymentApplicationService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定记录服务实现
|
||||
* <p>
|
||||
* 锁定:推送链路事务提交后逐张 LOCK(接口 1对1:单次一个源单号对一个金蝶单号,每张先落 failed 行占位再调
|
||||
* lockBusiData,成功翻 locked;单张成败互不影响),失败由补偿定时任务重试;落库统一批量更新。
|
||||
* 解锁:金蝶 unLockNotifyBusiData 回调(支持逗号拼接批量),
|
||||
* DELETE 时分发废除源单并还原关联数据。
|
||||
* 与结算单/开票申请服务互相依赖,为避免构造器循环依赖,单据服务经 {@code SpringUtil.getBean} 运行时获取
|
||||
* (与本工程 doSyncKingdee 自代理同一惯用法)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeBillLockServiceImpl extends ServiceImpl<KingdeeBillLockMapper, KingdeeBillLock> implements IKingdeeBillLockService {
|
||||
|
||||
/** 金蝶各接口统一日期文本格式(与 A102-2/A102-4/A103-1 等一致) */
|
||||
private static final DateTimeFormatter BFI_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final IBfiService bfiService;
|
||||
private final BfiProperties bfiProperties;
|
||||
private final FormalSettlementMapper formalSettlementMapper;
|
||||
private final InvoiceApplicationMapper invoiceApplicationMapper;
|
||||
private final PaymentApplicationMapper paymentApplicationMapper;
|
||||
|
||||
@Override
|
||||
public String lockAfterPush(String sourceBillType, Long sourceBillId, String sourceBillNo, String targetBillType, List<String> targetBillNos) {
|
||||
// 总开关关闭:不落锁记录、不调 lockBusiData,返回 null 不影响推送主流程
|
||||
if (!bfiProperties.getBillLock().isEnabled()) {
|
||||
log.info("金蝶单据锁定总开关关闭,跳过锁定:源单[{}:{}]", sourceBillType, sourceBillNo);
|
||||
return null;
|
||||
}
|
||||
if (Func.isEmpty(targetBillNos)) {
|
||||
return null;
|
||||
}
|
||||
String operator = operatorName();
|
||||
// 待锁目标单:已 locked 幂等跳过(人工重推「同步金蝶」幂等命中路径不会重复锁);
|
||||
// 其余(新目标单/failed 重锁/同单重推)先落 failed 行占位,保证补偿扫表无遗漏
|
||||
List<KingdeeBillLock> pendingRecords = new ArrayList<>();
|
||||
for (String targetBillNo : targetBillNos.stream().filter(Func::isNotBlank).distinct().toList()) {
|
||||
KingdeeBillLock lockRecord = latestRecord(sourceBillType, sourceBillId, targetBillNo);
|
||||
if (lockRecord != null && KingdeeLockConstant.STATUS_LOCKED.equals(lockRecord.getLockStatus())) {
|
||||
continue;
|
||||
}
|
||||
if (lockRecord == null) {
|
||||
lockRecord = new KingdeeBillLock();
|
||||
lockRecord.setSourceBillType(sourceBillType);
|
||||
lockRecord.setSourceBillId(sourceBillId);
|
||||
lockRecord.setSourceBillNo(sourceBillNo);
|
||||
lockRecord.setTargetBillType(targetBillType);
|
||||
lockRecord.setTargetBillNo(targetBillNo);
|
||||
lockRecord.setLockStatus(KingdeeLockConstant.STATUS_FAILED);
|
||||
lockRecord.setOperationType(KingdeeLockConstant.OPERATION_SAVE);
|
||||
lockRecord.setTryCount(0);
|
||||
lockRecord.setLockOperator(operator);
|
||||
this.save(lockRecord);
|
||||
} else {
|
||||
// 人工重推触发的补锁:重置补偿计数重新起算(定时补偿烧完上限后,人工动作可重新接管自动补偿)
|
||||
KingdeeBillLock resetRecord = new KingdeeBillLock();
|
||||
resetRecord.setId(lockRecord.getId());
|
||||
resetRecord.setTryCount(0);
|
||||
this.updateById(resetRecord);
|
||||
lockRecord.setTryCount(0);
|
||||
}
|
||||
pendingRecords.add(lockRecord);
|
||||
}
|
||||
if (pendingRecords.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
// 锁单接口 1对1:单次仅一个源单号对一个金蝶单号,逐张调用,单张成败互不影响;成败落库批量更新
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<KingdeeBillLock> lockedRecords = new ArrayList<>();
|
||||
List<KingdeeBillLock> failedRecords = new ArrayList<>();
|
||||
for (KingdeeBillLock lockRecord : pendingRecords) {
|
||||
try {
|
||||
bfiService.lockBusiData(this.buildLockParam(sourceBillType, sourceBillNo, targetBillType, lockRecord.getTargetBillNo(), operator));
|
||||
lockRecord.setLockStatus(KingdeeLockConstant.STATUS_LOCKED);
|
||||
lockRecord.setLockTime(now);
|
||||
lockRecord.setFailReason(null);
|
||||
lockedRecords.add(lockRecord);
|
||||
log.info("金蝶单据锁定成功:源单[{}:{}],金蝶单[{}]", sourceBillType, sourceBillNo, lockRecord.getTargetBillNo());
|
||||
} catch (Exception exception) {
|
||||
String failReason = limitText(exception.getMessage(), 500);
|
||||
lockRecord.setTryCount((lockRecord.getTryCount() == null ? 0 : lockRecord.getTryCount()) + 1);
|
||||
lockRecord.setFailReason(failReason);
|
||||
failedRecords.add(lockRecord);
|
||||
log.error("金蝶单据锁定失败(待补偿重锁):源单[{}:{}],金蝶单[{}],原因:{}", sourceBillType, sourceBillNo, lockRecord.getTargetBillNo(), failReason, exception);
|
||||
}
|
||||
}
|
||||
this.updateBatchById(lockedRecords);
|
||||
this.updateBatchById(failedRecords);
|
||||
if (failedRecords.isEmpty()) {
|
||||
log.info("金蝶单据锁定全部成功:源单[{}:{}],金蝶单{}张[{}]", sourceBillType, sourceBillNo, lockedRecords.size(),
|
||||
lockedRecords.stream().map(KingdeeBillLock::getTargetBillNo).collect(Collectors.joining(",")));
|
||||
return null;
|
||||
}
|
||||
String failedNos = failedRecords.stream().map(KingdeeBillLock::getTargetBillNo).collect(Collectors.joining(","));
|
||||
log.warn("金蝶单据锁定汇总:源单[{}:{}],成功{}张,失败{}张[{}]", sourceBillType, sourceBillNo, lockedRecords.size(), failedRecords.size(), failedNos);
|
||||
return failedNos;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int onUnlockNotify(BfiBillLockDTO param) {
|
||||
// 总开关关闭:只应答不处理(返回 0 与「未匹配」同构,金蝶侧不会触发重试)
|
||||
if (!bfiProperties.getBillLock().isEnabled()) {
|
||||
log.warn("金蝶单据锁定总开关关闭,跳过解锁回调处理:operationType={},金蝶单号={}",
|
||||
param == null ? null : param.getOperationType(), param == null ? null : param.getSourceBillNo());
|
||||
return 0;
|
||||
}
|
||||
if (param == null || !KingdeeLockConstant.SYSTEM_XH.equals(param.getSourceSystem())
|
||||
|| !KingdeeLockConstant.SYSTEM_WLTMS.equals(param.getTargetSystem())
|
||||
|| !KingdeeLockConstant.ACTION_UNLOCK.equals(param.getLockAction())) {
|
||||
throw new ServiceException("金蝶解锁回调参数非法:sourceSystem须为XH、targetSystem须为WLTMS、lockAction须为UNLOCK");
|
||||
}
|
||||
if (Func.isBlank(param.getSourceBillNo()) || Func.isBlank(param.getTargetBillNo())) {
|
||||
throw new ServiceException("金蝶解锁回调参数非法:sourceBillNo、targetBillNo不能为空");
|
||||
}
|
||||
// 回调方向与锁定相反:回调 sourceBillNo=金蝶单号(即锁记录 targetBillNo)、targetBillNo=WLTMS 单号(即锁记录 sourceBillNo)、
|
||||
// targetBillType 可能为空,故按金蝶单号+我方单号双条件反查;未匹配锁定中记录的幂等跳过(重复通知防金蝶重试风暴)
|
||||
// 一次 IN 查询取回候选记录、内存取各单号最新一条,批量更新解锁状态,避免逐张查询/更新的 N+1
|
||||
List<String> kingdeeBillNos = Func.toStrList(param.getSourceBillNo());
|
||||
Map<String, KingdeeBillLock> latestLockMap = this.list(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.in(KingdeeBillLock::getTargetBillNo, kingdeeBillNos)
|
||||
.eq(KingdeeBillLock::getSourceBillNo, param.getTargetBillNo())
|
||||
.orderByDesc(KingdeeBillLock::getId))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(KingdeeBillLock::getTargetBillNo, Function.identity(), (first, second) -> first));
|
||||
List<KingdeeBillLock> unlockRecords = new ArrayList<>();
|
||||
for (String kingdeeBillNo : kingdeeBillNos) {
|
||||
KingdeeBillLock lockRecord = latestLockMap.get(kingdeeBillNo);
|
||||
if (lockRecord == null || !KingdeeLockConstant.STATUS_LOCKED.equals(lockRecord.getLockStatus())) {
|
||||
log.warn("金蝶解锁回调未匹配锁定中的锁记录(幂等跳过):金蝶单号={},回调targetBillNo={}",
|
||||
kingdeeBillNo, param.getTargetBillNo());
|
||||
continue;
|
||||
}
|
||||
lockRecord.setLockStatus(KingdeeLockConstant.STATUS_UNLOCKED);
|
||||
lockRecord.setOperationType(param.getOperationType());
|
||||
lockRecord.setUnlockTime(LocalDateTime.now());
|
||||
lockRecord.setNotifyOperator(param.getOperator());
|
||||
unlockRecords.add(lockRecord);
|
||||
log.info("金蝶单据[{}:{}]解锁回调落库:operationType={},操作人={}",
|
||||
lockRecord.getSourceBillType(), lockRecord.getSourceBillNo(), param.getOperationType(), param.getOperator());
|
||||
}
|
||||
this.updateBatchById(unlockRecords);
|
||||
// DELETE 分发废除还原:按源单逐张执行(各单据类型还原动作为独立业务事务,无法合并)
|
||||
if (KingdeeLockConstant.OPERATION_DELETE.equals(param.getOperationType())) {
|
||||
unlockRecords.forEach(lockRecord -> this.restoreOnKingdeeDelete(lockRecord, param.getOperator()));
|
||||
}
|
||||
return unlockRecords.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶删单(DELETE 回调)分发:废除源单并还原关联数据(还原口径详见各 voidByKingdee 实现)
|
||||
*
|
||||
* @param lockRecord 锁记录(携带源单类型与源单主键)
|
||||
* @param operator 金蝶侧操作人
|
||||
*/
|
||||
private void restoreOnKingdeeDelete(KingdeeBillLock lockRecord, String operator) {
|
||||
String sourceType = KingdeeLockConstant.TARGET_TO_SOURCE_MAP.get(lockRecord.getTargetBillType());
|
||||
switch (sourceType == null ? "" : sourceType) {
|
||||
case KingdeeLockConstant.SOURCE_SETTLEMENT -> {
|
||||
FormalSettlement settlement = formalSettlementMapper.selectById(lockRecord.getSourceBillId());
|
||||
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) {
|
||||
log.warn("金蝶删单回调源结算单不存在:id={}", lockRecord.getSourceBillId());
|
||||
return;
|
||||
}
|
||||
if (TransportBusiConstant.VOIDED.equals(settlement.getApprovalStatus())) {
|
||||
log.info("金蝶删单回调源结算单[{}]已作废(幂等跳过)", settlement.getFormalSettlementNo());
|
||||
return;
|
||||
}
|
||||
SpringUtil.getBean(IFormalSettlementService.class).voidByKingdee(settlement.getId(), operator);
|
||||
}
|
||||
case KingdeeLockConstant.SOURCE_INVOICE_APPLY -> {
|
||||
InvoiceApplication application = invoiceApplicationMapper.selectById(lockRecord.getSourceBillId());
|
||||
if (application == null || Objects.equals(application.getIsDeleted(), 1)) {
|
||||
log.warn("金蝶删单回调源开票申请不存在:id={}", lockRecord.getSourceBillId());
|
||||
return;
|
||||
}
|
||||
if (TransportBusiConstant.VOIDED.equals(application.getApprovalStatus())) {
|
||||
log.info("金蝶删单回调源开票申请[{}]已作废(幂等跳过)", application.getApplicationNo());
|
||||
return;
|
||||
}
|
||||
SpringUtil.getBean(IInvoiceApplicationService.class).voidByKingdee(application.getId(), operator);
|
||||
}
|
||||
case KingdeeLockConstant.SOURCE_PAY_APPLY -> {
|
||||
PaymentApplication application = paymentApplicationMapper.selectById(lockRecord.getSourceBillId());
|
||||
if (application == null || Objects.equals(application.getIsDeleted(), 1)) {
|
||||
log.warn("金蝶删单回调源付款申请不存在:id={}", lockRecord.getSourceBillId());
|
||||
return;
|
||||
}
|
||||
if (TransportBusiConstant.VOIDED.equals(application.getApprovalStatus())) {
|
||||
log.info("金蝶删单回调源付款申请[{}]已作废(幂等跳过)", application.getPaymentNo());
|
||||
return;
|
||||
}
|
||||
// 复用 ADR-0005 解锁口径:不作废源单,kingdeeStatus→生成失败、清编号内码(原值留档),
|
||||
// 不自动重推,人工核实后重新推送(A103-2 勾稽预检防重复推单)
|
||||
SpringUtil.getBean(IPaymentApplicationService.class).unlockBill(application.getId(), "金蝶删单回调");
|
||||
}
|
||||
default -> log.warn("金蝶删单回调暂不支持的单据类型:targetBillType={}(映射预留)", lockRecord.getTargetBillType());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkEditable(String sourceBillType, Long sourceBillId) {
|
||||
long lockedCount = this.count(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.eq(KingdeeBillLock::getSourceBillType, sourceBillType)
|
||||
.eq(KingdeeBillLock::getSourceBillId, sourceBillId)
|
||||
.eq(KingdeeBillLock::getLockStatus, KingdeeLockConstant.STATUS_LOCKED));
|
||||
if (lockedCount > 0) {
|
||||
throw new ServiceException("该单据已被金蝶锁定,不可修改");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, KingdeeBillLock> mapActiveLocks(String sourceBillType, List<Long> sourceBillIds) {
|
||||
if (Func.isEmpty(sourceBillIds)) {
|
||||
return Map.of();
|
||||
}
|
||||
List<KingdeeBillLock> lockRecords = this.list(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.eq(KingdeeBillLock::getSourceBillType, sourceBillType)
|
||||
.in(KingdeeBillLock::getSourceBillId, sourceBillIds)
|
||||
.in(KingdeeBillLock::getLockStatus, List.of(KingdeeLockConstant.STATUS_LOCKED, KingdeeLockConstant.STATUS_FAILED))
|
||||
.orderByAsc(KingdeeBillLock::getId));
|
||||
// 升序取回后写覆盖:每源单保留最新一条;unlocked 终态不参与查询
|
||||
return lockRecords.stream().collect(Collectors.toMap(KingdeeBillLock::getSourceBillId, Function.identity(), (first, second) -> second));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, KingdeeBillLock> mapUnlockedLocks(String sourceBillType, List<Long> sourceBillIds) {
|
||||
if (Func.isEmpty(sourceBillIds)) {
|
||||
return Map.of();
|
||||
}
|
||||
List<KingdeeBillLock> lockRecords = this.list(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.eq(KingdeeBillLock::getSourceBillType, sourceBillType)
|
||||
.in(KingdeeBillLock::getSourceBillId, sourceBillIds)
|
||||
.eq(KingdeeBillLock::getLockStatus, KingdeeLockConstant.STATUS_UNLOCKED)
|
||||
.orderByDesc(KingdeeBillLock::getId));
|
||||
// 降序取回后首个生效:每源单保留最新一条解锁留痕
|
||||
return lockRecords.stream().collect(Collectors.toMap(KingdeeBillLock::getSourceBillId, Function.identity(), (first, second) -> first));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends BaseEntity & KingdeeUnlockMark> void fillUnlockMarks(String sourceBillType, List<T> rows, Map<Long, KingdeeBillLock> activeLockMap) {
|
||||
if (Func.isEmpty(rows)) {
|
||||
return;
|
||||
}
|
||||
List<Long> unlockedIds = rows.stream()
|
||||
.filter(row -> !activeLockMap.containsKey(row.getId()))
|
||||
.map(BaseEntity::getId).toList();
|
||||
if (Func.isEmpty(unlockedIds)) {
|
||||
return;
|
||||
}
|
||||
Map<Long, KingdeeBillLock> unlockedMap = this.mapUnlockedLocks(sourceBillType, unlockedIds);
|
||||
for (T row : rows) {
|
||||
KingdeeBillLock unlockRecord = unlockedMap.get(row.getId());
|
||||
if (unlockRecord == null) {
|
||||
continue;
|
||||
}
|
||||
row.setKingdeeUnlockTime(unlockRecord.getUnlockTime());
|
||||
row.setKingdeeUnlockOperator(unlockRecord.getNotifyOperator());
|
||||
row.setKingdeeUnlockOperationName(KingdeeLockConstant.OPERATION_DELETE.equals(unlockRecord.getOperationType())
|
||||
? "金蝶删单" : "金蝶锁单");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定补偿:扫 failed 且 try_count 未达重试上限(RETRY_MAX_COUNT)的锁记录,逐张重调 lockBusiData
|
||||
* (接口 1对1:单次一个源单号对一个金蝶单号,单张成败互不影响,由定时任务定时触发,亦可人工触发)。
|
||||
* <p>成功翻 locked(补锁时间、清失败原因);失败 try_count+1 并记失败原因,计数烧满上限后不再自动重试
|
||||
* (人工重推「同步金蝶」重置计数重新接管);成败落库均为批量更新(updateBatchById),避免逐张 update
|
||||
*/
|
||||
@Override
|
||||
public String retryFailedLocks() {
|
||||
// 总开关关闭:定时任务与手动补偿一并跳过
|
||||
if (!bfiProperties.getBillLock().isEnabled()) {
|
||||
return "金蝶单据锁定总开关关闭,跳过补偿";
|
||||
}
|
||||
List<KingdeeBillLock> failedList = this.list(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.eq(KingdeeBillLock::getLockStatus, KingdeeLockConstant.STATUS_FAILED)
|
||||
.lt(KingdeeBillLock::getTryCount, KingdeeLockConstant.RETRY_MAX_COUNT));
|
||||
if (Func.isEmpty(failedList)) {
|
||||
return "无待补偿锁记录";
|
||||
}
|
||||
log.info("金蝶单据锁定补偿开始:待补偿{}张", failedList.size());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<KingdeeBillLock> lockedRecords = new ArrayList<>();
|
||||
List<KingdeeBillLock> failedRecords = new ArrayList<>();
|
||||
for (KingdeeBillLock lockRecord : failedList) {
|
||||
String operator = Func.isBlank(lockRecord.getLockOperator()) ? "system" : lockRecord.getLockOperator();
|
||||
try {
|
||||
bfiService.lockBusiData(this.buildLockParam(lockRecord.getSourceBillType(), lockRecord.getSourceBillNo(),
|
||||
lockRecord.getTargetBillType(), lockRecord.getTargetBillNo(), operator));
|
||||
lockRecord.setLockStatus(KingdeeLockConstant.STATUS_LOCKED);
|
||||
lockRecord.setLockTime(now);
|
||||
lockRecord.setFailReason(null);
|
||||
lockedRecords.add(lockRecord);
|
||||
log.info("金蝶单据锁定补偿成功:源单[{}:{}],金蝶单[{}]",
|
||||
lockRecord.getSourceBillType(), lockRecord.getSourceBillNo(), lockRecord.getTargetBillNo());
|
||||
} catch (Exception exception) {
|
||||
String failReason = limitText(exception.getMessage(), 500);
|
||||
lockRecord.setTryCount((lockRecord.getTryCount() == null ? 0 : lockRecord.getTryCount()) + 1);
|
||||
lockRecord.setFailReason(failReason);
|
||||
failedRecords.add(lockRecord);
|
||||
log.error("金蝶单据锁定补偿失败(try_count已+1,待下次补偿):源单[{}:{}],金蝶单[{}],原因:{}",
|
||||
lockRecord.getSourceBillType(), lockRecord.getSourceBillNo(), lockRecord.getTargetBillNo(), failReason, exception);
|
||||
}
|
||||
}
|
||||
this.updateBatchById(lockedRecords);
|
||||
this.updateBatchById(failedRecords);
|
||||
log.info("金蝶单据锁定补偿结束:待补偿{}张,成功{}张,仍失败{}张[{}]", failedList.size(), lockedRecords.size(), failedRecords.size(),
|
||||
failedRecords.stream().map(KingdeeBillLock::getTargetBillNo).collect(Collectors.joining(",")));
|
||||
return "待补偿" + failedList.size() + "张,成功" + lockedRecords.size() + "张";
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 lockBusiData 参数:WLTMS 调金蝶,源系统取配置(默认 WLTMS),目标系统 XH,
|
||||
* 操作类型固定 LOCK/SAVE;接口 1对1:单次仅一个源单号对一个金蝶单号
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型
|
||||
* @param sourceBillNo WLTMS 单号
|
||||
* @param targetBillType 金蝶目标单据类型
|
||||
* @param targetBillNo 金蝶单据编号(单张)
|
||||
* @param operator 操作人
|
||||
* @return 锁单参数
|
||||
*/
|
||||
private BfiBillLockDTO buildLockParam(String sourceBillType, String sourceBillNo, String targetBillType, String targetBillNo, String operator) {
|
||||
BfiBillLockDTO param = new BfiBillLockDTO();
|
||||
param.setLockAction(KingdeeLockConstant.ACTION_LOCK);
|
||||
param.setSourceSystem(Func.isBlank(bfiProperties.getSrcSys()) ? KingdeeLockConstant.SYSTEM_WLTMS : bfiProperties.getSrcSys());
|
||||
param.setTargetSystem(KingdeeLockConstant.SYSTEM_XH);
|
||||
param.setOperationType(KingdeeLockConstant.OPERATION_SAVE);
|
||||
param.setSourceBillType(sourceBillType);
|
||||
param.setSourceBillNo(sourceBillNo);
|
||||
param.setTargetBillType(targetBillType);
|
||||
param.setTargetBillNo(targetBillNo);
|
||||
param.setTime(LocalDateTime.now().format(BFI_DATE_TIME));
|
||||
param.setOperator(operator);
|
||||
return param;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查同源单同目标单最新一条锁记录
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型
|
||||
* @param sourceBillId WLTMS 业务主键
|
||||
* @param targetBillNo 金蝶单据编号
|
||||
* @return 最新锁记录,无则 null
|
||||
*/
|
||||
private KingdeeBillLock latestRecord(String sourceBillType, Long sourceBillId, String targetBillNo) {
|
||||
return this.getOne(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.eq(KingdeeBillLock::getSourceBillType, sourceBillType)
|
||||
.eq(KingdeeBillLock::getSourceBillId, sourceBillId)
|
||||
.eq(KingdeeBillLock::getTargetBillNo, targetBillNo)
|
||||
.orderByDesc(KingdeeBillLock::getId)
|
||||
.last("limit 1"), false);
|
||||
}
|
||||
|
||||
/** 当前操作人:无登录态(定时任务/回调)时兜底 system(与 saveChange 同款兜底) */
|
||||
private String operatorName() {
|
||||
return Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName();
|
||||
}
|
||||
|
||||
/** 失败原因文本截断(fail_reason 列宽 500) */
|
||||
private String limitText(String text, int maxLength) {
|
||||
if (Func.isBlank(text)) {
|
||||
return null;
|
||||
}
|
||||
return text.length() <= maxLength ? text : text.substring(0, maxLength);
|
||||
}
|
||||
}
|
||||
+1287
-80
File diff suppressed because it is too large
Load Diff
+13
@@ -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)
|
||||
@@ -241,6 +246,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
result.put("partyA", contract.getPartyA());
|
||||
result.put("partyB", contract.getPartyB());
|
||||
result.put("contractCategory", contract.getContractCategory());
|
||||
result.put("settlementCurrency", contract.getSettlementCurrency());
|
||||
result.put("fundDemand", project == null ? null : project.getFundDemand());
|
||||
result.put("settlementMode", project == null ? null : project.getSettlementMode());
|
||||
result.put("settlementType", contractSettlementType(contract));
|
||||
@@ -350,6 +356,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(), "");
|
||||
@@ -366,6 +378,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
}
|
||||
releaseSourceDetails(settlement, listDetails(id));
|
||||
deleteChildren(id);
|
||||
projectAdvanceWriteOffService.deleteBySettlement(TransportBusiConstant.SETTLEMENT_BILL_PRE, id);
|
||||
deleteLogic(List.of(id));
|
||||
}
|
||||
|
||||
|
||||
+132
-15
@@ -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
|
||||
@@ -338,19 +338,23 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
TransportBusinessSupport.validateAllDept(projectApply.getAllDept(), "项目立项");
|
||||
LambdaQueryWrapper<ProjectApply> queryWrapper = Wrappers.<ProjectApply>lambdaQuery().eq(ProjectApply::getIsDeleted, 0);
|
||||
if (TransportBusinessSupport.isAdministrator()) {
|
||||
// 超管默认可查看全部未删除数据;仅在明确指定承办组织时按组织精确筛选。
|
||||
// 超管默认可查看全部未删除数据;仅在明确指定平台公司时按包含匹配筛选。
|
||||
if (Objects.equals(projectApply.getAllDept(), 1) && Func.isNotEmpty(projectApply.getUndertakeDeptId())) {
|
||||
queryWrapper.eq(ProjectApply::getUndertakeDeptId, projectApply.getUndertakeDeptId());
|
||||
applyUndertakeDeptContains(queryWrapper, projectApply.getUndertakeDeptId());
|
||||
}
|
||||
} else if (!Objects.equals(projectApply.getAllDept(), 1)) {
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目立项");
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
queryWrapper.and(wrapper -> wrapper.eq(ProjectApply::getUndertakeDeptId, currentDeptId)
|
||||
.or().eq(ProjectApply::getCreateUser, currentUserId)
|
||||
.or().eq(ProjectApply::getHandlerUserId, currentUserId)
|
||||
.or().eq(ProjectApply::getPrincipalUserId, currentUserId));
|
||||
queryWrapper.and(wrapper -> {
|
||||
if (currentDeptId != null) {
|
||||
wrapper.apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
|
||||
}
|
||||
wrapper.or().eq(ProjectApply::getCreateUser, currentUserId)
|
||||
.or().eq(ProjectApply::getHandlerUserId, currentUserId)
|
||||
.or().eq(ProjectApply::getPrincipalUserId, currentUserId);
|
||||
});
|
||||
} else if (Func.isNotEmpty(projectApply.getUndertakeDeptId())) {
|
||||
queryWrapper.eq(ProjectApply::getUndertakeDeptId, projectApply.getUndertakeDeptId());
|
||||
applyUndertakeDeptContains(queryWrapper, projectApply.getUndertakeDeptId());
|
||||
}
|
||||
queryWrapper.like(Func.isNotEmpty(projectApply.getApplyNo()), ProjectApply::getApplyNo, projectApply.getApplyNo())
|
||||
.like(Func.isNotEmpty(projectApply.getProjectCode()), ProjectApply::getProjectCode, projectApply.getProjectCode())
|
||||
@@ -464,6 +468,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
}
|
||||
|
||||
private void prepareCreateOrUpdate(ProjectApply projectApply) {
|
||||
normalizeUndertakeDept(projectApply);
|
||||
if (Func.isEmpty(projectApply.getId())) {
|
||||
// 新增项目不产生变更记录,避免客户端误传变更字段导致记录回显。
|
||||
projectApply.setChangeContent(null);
|
||||
@@ -476,12 +481,12 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
}
|
||||
if (Func.isEmpty(projectApply.getUndertakeDeptId())) {
|
||||
Dept dept = TransportBusinessSupport.currentDept("项目立项");
|
||||
projectApply.setUndertakeDeptId(dept.getId());
|
||||
projectApply.setUndertakeDeptId(String.valueOf(dept.getId()));
|
||||
projectApply.setUndertakeDeptName(dept.getDeptName());
|
||||
}
|
||||
if (Func.isEmpty(projectApply.getBusinessDeptId())) {
|
||||
projectApply.setBusinessDeptId(projectApply.getUndertakeDeptId());
|
||||
projectApply.setBusinessDeptName(projectApply.getUndertakeDeptName());
|
||||
projectApply.setBusinessDeptId(firstUndertakeDeptId(projectApply.getUndertakeDeptId()));
|
||||
projectApply.setBusinessDeptName(firstUndertakeDeptName(projectApply.getUndertakeDeptName()));
|
||||
}
|
||||
if (Func.isEmpty(projectApply.getHandlerUserId())) {
|
||||
projectApply.setHandlerUserId(AuthUtil.getUserId());
|
||||
@@ -495,8 +500,11 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
ProjectApply oldRecord = loadEditable(projectApply.getId(), true);
|
||||
projectApply.setApplyNo(oldRecord.getApplyNo());
|
||||
projectApply.setProjectCode(oldRecord.getProjectCode());
|
||||
projectApply.setUndertakeDeptId(oldRecord.getUndertakeDeptId());
|
||||
projectApply.setUndertakeDeptName(oldRecord.getUndertakeDeptName());
|
||||
// 平台公司支持编辑多选保存,不再强制写回旧值
|
||||
if (Func.isEmpty(projectApply.getUndertakeDeptId())) {
|
||||
projectApply.setUndertakeDeptId(oldRecord.getUndertakeDeptId());
|
||||
projectApply.setUndertakeDeptName(oldRecord.getUndertakeDeptName());
|
||||
}
|
||||
}
|
||||
if (Func.isEmpty(projectApply.getEffectiveType())) {
|
||||
projectApply.setEffectiveType(EFFECTIVE_TEMPORARY);
|
||||
@@ -764,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个字");
|
||||
@@ -981,10 +991,117 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
}
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目立项");
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
return Objects.equals(projectApply.getUndertakeDeptId(), currentDeptId)
|
||||
return containsUndertakeDept(projectApply.getUndertakeDeptId(), currentDeptId)
|
||||
|| Objects.equals(projectApply.getCreateUser(), currentUserId)
|
||||
|| Objects.equals(projectApply.getHandlerUserId(), currentUserId)
|
||||
|| Objects.equals(projectApply.getPrincipalUserId(), currentUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台公司多值筛选:历史单 ID 与逗号分隔多 ID 均按包含匹配。
|
||||
*/
|
||||
private void applyUndertakeDeptContains(LambdaQueryWrapper<ProjectApply> queryWrapper, String undertakeDeptId) {
|
||||
if (Func.isBlank(undertakeDeptId) || queryWrapper == null) {
|
||||
return;
|
||||
}
|
||||
List<String> deptIds = Arrays.stream(undertakeDeptId.split("[,,]"))
|
||||
.map(String::trim)
|
||||
.filter(Func::isNotEmpty)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (deptIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
queryWrapper.and(wrapper -> {
|
||||
boolean first = true;
|
||||
for (String deptId : deptIds) {
|
||||
if (first) {
|
||||
wrapper.apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", deptId);
|
||||
first = false;
|
||||
} else {
|
||||
wrapper.or().apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", deptId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
List<String> deptIds = Arrays.stream(Func.toStr(projectApply.getUndertakeDeptId()).split("[,,]"))
|
||||
.map(String::trim)
|
||||
.filter(Func::isNotEmpty)
|
||||
.filter(id -> !"-1".equals(id))
|
||||
.distinct()
|
||||
.toList();
|
||||
projectApply.setUndertakeDeptId(deptIds.isEmpty() ? null : String.join(",", deptIds));
|
||||
List<String> deptNames = Arrays.stream(Func.toStr(projectApply.getUndertakeDeptName()).split("[,,、]"))
|
||||
.map(String::trim)
|
||||
.filter(Func::isNotEmpty)
|
||||
.distinct()
|
||||
.toList();
|
||||
projectApply.setUndertakeDeptName(deptNames.isEmpty() ? null : String.join("、", deptNames));
|
||||
}
|
||||
|
||||
private Long firstUndertakeDeptId(String undertakeDeptId) {
|
||||
return Arrays.stream(Func.toStr(undertakeDeptId).split("[,,]"))
|
||||
.map(String::trim)
|
||||
.filter(Func::isNotEmpty)
|
||||
.filter(id -> !"-1".equals(id))
|
||||
.map(Func::toLong)
|
||||
.filter(id -> id != null && id > 0L)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private String firstUndertakeDeptName(String undertakeDeptName) {
|
||||
return Arrays.stream(Func.toStr(undertakeDeptName).split("[,,、]"))
|
||||
.map(String::trim)
|
||||
.filter(Func::isNotEmpty)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private boolean containsUndertakeDept(String undertakeDeptId, Long deptId) {
|
||||
if (deptId == null || Func.isBlank(undertakeDeptId)) {
|
||||
return false;
|
||||
}
|
||||
String target = String.valueOf(deptId);
|
||||
return Arrays.stream(undertakeDeptId.split("[,,]"))
|
||||
.map(String::trim)
|
||||
.anyMatch(target::equals);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+170
-16
@@ -28,10 +28,17 @@ package org.springblade.transport.service.impl;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.thirdparty.bfi.constant.BfiConstant;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiRecBillQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiRecBillVO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springblade.thirdparty.bfi.util.BfiUtil;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.mapper.FormalSettlementMapper;
|
||||
import org.springblade.transport.mapper.KingdeeReceiptFlowMapper;
|
||||
import org.springblade.transport.mapper.ReceiptClaimMapper;
|
||||
@@ -51,6 +58,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Comparator;
|
||||
@@ -58,12 +66,16 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 认领记录服务实现类
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimMapper, ReceiptClaim>
|
||||
@@ -72,12 +84,21 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimM
|
||||
private static final String CLAIMED = "claimed";
|
||||
private static final String VOIDED = "voided";
|
||||
private static final String APPROVED = "approved";
|
||||
/**
|
||||
* A301-1 收款单查询回看窗口天数(金蝶约束业务日期范围不可超过1个月)
|
||||
*/
|
||||
private static final int RESULT_WINDOW_DAYS = 30;
|
||||
/**
|
||||
* A301-1 收款单查询分页大小
|
||||
*/
|
||||
private static final int RESULT_PAGE_SIZE = 200;
|
||||
|
||||
private final ReceiptClaimSettlementMapper claimSettlementMapper;
|
||||
private final IFormalSettlementService formalSettlementService;
|
||||
private final KingdeeReceiptFlowMapper receiptFlowMapper;
|
||||
private final FormalSettlementMapper formalSettlementMapper;
|
||||
private final ReceiptFlowRecordMapper recordMapper;
|
||||
private final IBfiService bfiService;
|
||||
|
||||
@Override
|
||||
public IPage<ReceiptClaimRecordVO> selectPage(IPage<ReceiptClaimRecordVO> page,
|
||||
@@ -163,7 +184,7 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimM
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String voidClaim(Long id) {
|
||||
public void voidClaim(Long id) {
|
||||
ReceiptClaim claim = baseMapper.selectOne(Wrappers.<ReceiptClaim>lambdaQuery()
|
||||
.eq(ReceiptClaim::getId, id)
|
||||
.eq(ReceiptClaim::getClaimerId, AuthUtil.getUserId())
|
||||
@@ -182,6 +203,10 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimM
|
||||
if (flow == null || Objects.equals(flow.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("关联收款流水不存在");
|
||||
}
|
||||
// 已推送金蝶的认领不允许作废:金蝶收款单已生成,冲销/调整由人工在金蝶处置
|
||||
if (KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_PUSHED.equals(flow.getPushStatus())) {
|
||||
throw new ServiceException("该认领已推送金蝶,不允许作废,请在金蝶侧人工处置");
|
||||
}
|
||||
|
||||
List<ReceiptClaimSettlement> relations = claimSettlementMapper.selectList(
|
||||
Wrappers.<ReceiptClaimSettlement>lambdaQuery()
|
||||
@@ -194,29 +219,47 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimM
|
||||
List<ReceiptClaimSettlement> orderedRelations = relations.stream()
|
||||
.sorted(Comparator.comparing(ReceiptClaimSettlement::getFormalSettlementId))
|
||||
.toList();
|
||||
for (ReceiptClaimSettlement relation : orderedRelations) {
|
||||
FormalSettlement settlement = formalSettlementMapper.selectOne(
|
||||
// 批量锁行关联结算单(一次 IN 查询替代逐单 selectOne,id 升序加锁防死锁)
|
||||
List<Long> settlementIds = orderedRelations.stream()
|
||||
.map(ReceiptClaimSettlement::getFormalSettlementId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (settlementIds.isEmpty()) {
|
||||
throw new ServiceException("关联结算单不存在");
|
||||
}
|
||||
Map<Long, FormalSettlement> settlements = formalSettlementMapper.selectList(
|
||||
Wrappers.<FormalSettlement>lambdaQuery()
|
||||
.eq(FormalSettlement::getId, relation.getFormalSettlementId())
|
||||
.last("FOR UPDATE"));
|
||||
.in(FormalSettlement::getId, settlementIds)
|
||||
.orderByAsc(FormalSettlement::getId)
|
||||
.last("FOR UPDATE")).stream()
|
||||
.collect(Collectors.toMap(FormalSettlement::getId, Function.identity()));
|
||||
for (ReceiptClaimSettlement relation : orderedRelations) {
|
||||
FormalSettlement settlement = settlements.get(relation.getFormalSettlementId());
|
||||
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("关联结算单不存在");
|
||||
}
|
||||
relation.setStatus(0);
|
||||
claimSettlementMapper.updateById(relation);
|
||||
formalSettlementService.refreshPaymentSummary(settlement.getId());
|
||||
}
|
||||
// 分摊行批量置无效(单条 UPDATE 替代逐行 updateById,实体携带 status 保留审计字段填充)
|
||||
ReceiptClaimSettlement voidTemplate = new ReceiptClaimSettlement();
|
||||
voidTemplate.setStatus(0);
|
||||
claimSettlementMapper.update(voidTemplate, Wrappers.<ReceiptClaimSettlement>lambdaUpdate()
|
||||
.in(ReceiptClaimSettlement::getId, relations.stream()
|
||||
.map(ReceiptClaimSettlement::getId).toList()));
|
||||
// 批量刷新结算单收款汇总(固定数次查询,避免逐单 N+1)
|
||||
formalSettlementService.refreshPaymentSummaries(settlementIds);
|
||||
|
||||
BigDecimal flowClaimedAfter = money(flow.getClaimedAmount())
|
||||
.subtract(money(claim.getClaimAmount())).max(BigDecimal.ZERO);
|
||||
flow.setClaimedAmount(flowClaimedAfter);
|
||||
flow.setClaimStatus(amountClaimStatus(flowClaimedAfter, flow.getReceiptAmount()));
|
||||
// 认领状态对照公告未认领金额重算(作废分摊不计入累计)
|
||||
flow.setClaimStatus(amountClaimStatus(flowClaimedAfter, flow.getUnclaimedAmount()));
|
||||
receiptFlowMapper.updateById(flow);
|
||||
|
||||
String kingdeeBillNo = buildKingdeeBillNo(claim.getId());
|
||||
// 作废不生成金蝶认领冲单:金蝶单号/状态保留原值(未推送为空,已推送为真实收款单信息),
|
||||
// 金蝶侧冲销/红冲由人工处置;冲单逻辑后续迭代如需恢复,放开下行调用即可
|
||||
// String kingdeeBillNo = buildKingdeeBillNo(claim.getId());
|
||||
claim.setClaimStatus(VOIDED);
|
||||
claim.setKingdeeBillNo(kingdeeBillNo);
|
||||
claim.setKingdeeBillStatus(APPROVED);
|
||||
claim.setVoidedBy(AuthUtil.getUserId());
|
||||
claim.setVoidedByName(Func.isEmpty(AuthUtil.getUserName()) ? claim.getClaimerName()
|
||||
: AuthUtil.getUserName());
|
||||
@@ -233,9 +276,110 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimM
|
||||
operationRecord.setOperationAmount(money(claim.getClaimAmount()));
|
||||
operationRecord.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system"
|
||||
: AuthUtil.getUserName());
|
||||
operationRecord.setContent("生成金蝶认领冲单:" + kingdeeBillNo + ",状态:审核通过");
|
||||
operationRecord.setContent("作废认领,回退认领金额 " + money(claim.getClaimAmount()));
|
||||
recordMapper.insert(operationRecord);
|
||||
return kingdeeBillNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* A301-1 收款单查询回写认领结果:按来源单据编码匹配金蝶收款单,推进流水金蝶回写状态
|
||||
* <p>
|
||||
* 回写口径:A301-1 契约本身不支持按来源单号过滤,故按近 30 天业务日期窗口全量翻页后,
|
||||
* 以金蝶收款单 sourcebillnumber == 流水 awrp_src_code(A301-3 推送时按流水生成、重试复用)在本地匹配;
|
||||
* 仅推进金蝶回写状态与补登收款单号,不改本地认领金额/分摊;名下未作废认领主单冗余展示字段级联刷新。
|
||||
*
|
||||
* @return 回写条数
|
||||
*/
|
||||
@Override
|
||||
public int refreshKingdeeResult() {
|
||||
List<KingdeeReceiptFlow> pending = receiptFlowMapper.selectList(
|
||||
Wrappers.<KingdeeReceiptFlow>lambdaQuery()
|
||||
.eq(KingdeeReceiptFlow::getPushStatus, KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_PUSHED)
|
||||
.in(KingdeeReceiptFlow::getKingdeeBillStatus,
|
||||
List.of(KingdeeBfiConstant.KINGDEE_STATUS_PUSHED, APPROVED))
|
||||
.eq(KingdeeReceiptFlow::getStatus, 1)
|
||||
.orderByAsc(KingdeeReceiptFlow::getId));
|
||||
if (pending.isEmpty()) {
|
||||
log.info("金蝶认领结果回写:无待回写流水(已推送/已审核且未终态)");
|
||||
return 0;
|
||||
}
|
||||
Set<String> srcCodes = pending.stream()
|
||||
.map(KingdeeReceiptFlow::getAwrpSrcCode)
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toSet());
|
||||
if (srcCodes.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
log.info("金蝶认领结果回写:待回写流水 {} 条", pending.size());
|
||||
|
||||
LocalDate end = LocalDate.now();
|
||||
LocalDate begin = end.minusDays(RESULT_WINDOW_DAYS);
|
||||
long start = System.currentTimeMillis();
|
||||
log.info("调用BFI接口 收款单查询开始,业务日期窗口 {} ~ {}", begin, end);
|
||||
List<BfiRecBillVO> rows = BfiUtil.queryAllPages(pageNo -> {
|
||||
BfiRecBillQueryDTO dto = new BfiRecBillQueryDTO();
|
||||
dto.setPageNo(pageNo);
|
||||
dto.setPageSize(RESULT_PAGE_SIZE);
|
||||
dto.setBeginBizdate(begin.format(DateTimeFormatter.ISO_LOCAL_DATE));
|
||||
dto.setEndBizdate(end.format(DateTimeFormatter.ISO_LOCAL_DATE));
|
||||
return bfiService.queryRecBill(dto);
|
||||
});
|
||||
log.info("调用BFI接口 收款单查询结束 耗时:{}ms,拉取 {} 条", System.currentTimeMillis() - start, rows.size());
|
||||
|
||||
// 窗口内收款单按 sourcebillnumber 匹配待回写来源编码,同键多单取首条
|
||||
Map<String, BfiRecBillVO> matched = rows.stream()
|
||||
.filter(row -> Func.isNotEmpty(row.getSourcebillnumber()))
|
||||
.filter(row -> srcCodes.contains(row.getSourcebillnumber()))
|
||||
.collect(Collectors.toMap(BfiRecBillVO::getSourcebillnumber, Function.identity(),
|
||||
(first, second) -> first, HashMap::new));
|
||||
int updated = 0;
|
||||
for (KingdeeReceiptFlow flow : pending) {
|
||||
BfiRecBillVO bill = matched.get(flow.getAwrpSrcCode());
|
||||
if (bill == null) {
|
||||
// 窗口内未查到:金蝶回单延迟/状态未同步,不改状态,由下次回写或人工处置兜底
|
||||
continue;
|
||||
}
|
||||
// 推送成功时金蝶收款单号先以来源编码占位(A301-3 响应 number 为空,取回执 keys.awrp_src_code),
|
||||
// 回写命中后以金蝶真实收款单号覆盖
|
||||
if (Func.isNotEmpty(bill.getBillno())) {
|
||||
flow.setKingdeeBillNo(bill.getBillno());
|
||||
}
|
||||
flow.setKingdeeBillStatus(billStatusOf(bill.getBillstatus()));
|
||||
receiptFlowMapper.updateById(flow);
|
||||
// 级联刷新名下未作废认领主单的冗余展示字段
|
||||
baseMapper.update(null, Wrappers.<ReceiptClaim>lambdaUpdate()
|
||||
.eq(ReceiptClaim::getReceiptFlowId, flow.getId())
|
||||
.eq(ReceiptClaim::getClaimStatus, CLAIMED)
|
||||
.set(ReceiptClaim::getKingdeeBillNo, flow.getKingdeeBillNo())
|
||||
.set(ReceiptClaim::getKingdeeBillStatus, flow.getKingdeeBillStatus()));
|
||||
updated++;
|
||||
log.info("金蝶认领结果回写 flowId:{},awrpSrcCode:{},金蝶收款单号:{},状态:{} → {}",
|
||||
flow.getId(), flow.getAwrpSrcCode(), bill.getBillno(), bill.getBillstatus(),
|
||||
flow.getKingdeeBillStatus());
|
||||
}
|
||||
log.info("金蝶认领结果回写完成:匹配 {} 条,更新 {} 条", matched.size(), updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶收款单单据状态 → 本地认领金蝶状态
|
||||
*
|
||||
* @param billStatus 金蝶 billstatus(A暂存/B已提交/C已审核/D已收款/G已退单)
|
||||
* @return 本地状态
|
||||
*/
|
||||
private String billStatusOf(String billStatus) {
|
||||
if (BfiConstant.BILL_STATUS_RECEIVED.equals(billStatus)) {
|
||||
return KingdeeBfiConstant.KINGDEE_STATUS_RECEIVED;
|
||||
}
|
||||
if (BfiConstant.BILL_STATUS_AUDITED.equals(billStatus)) {
|
||||
return APPROVED;
|
||||
}
|
||||
if (BfiConstant.BILL_STATUS_DRAFT.equals(billStatus)
|
||||
|| BfiConstant.BILL_STATUS_SUBMITTED.equals(billStatus)) {
|
||||
return KingdeeBfiConstant.KINGDEE_STATUS_PUSHED;
|
||||
}
|
||||
// 其余状态(含 G 已退单)按处理失败回收,提示人工在金蝶核查
|
||||
log.warn("金蝶收款单状态[{}]未匹配映射,认领回写按 failed 处理", billStatus);
|
||||
return KingdeeBfiConstant.KINGDEE_STATUS_FAILED;
|
||||
}
|
||||
|
||||
private void fillStatusNames(ReceiptClaimRecordVO record) {
|
||||
@@ -244,6 +388,8 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimM
|
||||
record.setKingdeeBillStatusName(switch (record.getKingdeeBillStatus() == null
|
||||
? "" : record.getKingdeeBillStatus()) {
|
||||
case APPROVED -> "审核通过";
|
||||
case "received" -> "已收款";
|
||||
case "pushed" -> "已推送";
|
||||
case "failed" -> "处理失败";
|
||||
default -> "未生成";
|
||||
});
|
||||
@@ -253,17 +399,25 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimM
|
||||
return VOIDED.equals(claimStatus) ? VOIDED : CLAIMED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成本地金蝶认领冲单号(KDCX + 时间戳 + 认领id尾号6位)。
|
||||
* 当前作废流程不生成冲单(调用点已注释保留),后续迭代接金蝶冲销时放开即可。
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
private String buildKingdeeBillNo(Long claimId) {
|
||||
String time = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
|
||||
String suffix = String.valueOf(claimId);
|
||||
return "KDCX" + time + suffix.substring(Math.max(0, suffix.length() - 6));
|
||||
}
|
||||
|
||||
private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal receiptAmount) {
|
||||
/**
|
||||
* 累计认领金额 → 流水认领状态:对照公告未认领金额判定(与收款流水侧口径一致)
|
||||
*/
|
||||
private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal unclaimedAmount) {
|
||||
if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return "unclaimed";
|
||||
}
|
||||
return claimedAmount.compareTo(money(receiptAmount)) >= 0 ? "claimed" : "partial";
|
||||
return claimedAmount.compareTo(money(unclaimedAmount)) >= 0 ? "claimed" : "partial";
|
||||
}
|
||||
|
||||
private String amountStatus(BigDecimal paidAmount, BigDecimal settlementAmount) {
|
||||
|
||||
+712
-104
File diff suppressed because it is too large
Load Diff
+270
-23
@@ -432,13 +432,23 @@ public class ReceivablePayableDetailServiceImpl
|
||||
|
||||
@Override
|
||||
public IPage<ReceivablePayableChangeRecordVO> changeRecords(IPage<?> page, Long detailId) {
|
||||
LambdaQueryWrapper<ReceivablePayableChangeRecord> wrapper = Wrappers.<ReceivablePayableChangeRecord>lambdaQuery()
|
||||
.eq(ReceivablePayableChangeRecord::getDetailId, detailId)
|
||||
.eq(ReceivablePayableChangeRecord::getIsDeleted, 0)
|
||||
.orderByDesc(ReceivablePayableChangeRecord::getAdjustTime);
|
||||
IPage<ReceivablePayableChangeRecord> entityPage = changeRecordMapper.selectPage(new Page<>(page.getCurrent(), page.getSize()), wrapper);
|
||||
Page<ReceivablePayableChangeRecordVO> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
|
||||
voPage.setRecords(entityPage.getRecords().stream().map(record -> Objects.requireNonNull(
|
||||
List<ReceivablePayableChangeRecord> meaningfulRecords = changeRecordMapper.selectList(
|
||||
Wrappers.<ReceivablePayableChangeRecord>lambdaQuery()
|
||||
.eq(ReceivablePayableChangeRecord::getDetailId, detailId)
|
||||
.eq(ReceivablePayableChangeRecord::getIsDeleted, 0)
|
||||
.orderByDesc(ReceivablePayableChangeRecord::getAdjustTime))
|
||||
.stream()
|
||||
.filter(record -> hasMeaningfulChange(record.getChangeContent()))
|
||||
.toList();
|
||||
long current = page.getCurrent();
|
||||
long size = page.getSize();
|
||||
int fromIndex = (int) Math.max(0L, (current - 1L) * size);
|
||||
int toIndex = (int) Math.min(meaningfulRecords.size(), fromIndex + size);
|
||||
List<ReceivablePayableChangeRecord> pageRecords = fromIndex >= meaningfulRecords.size()
|
||||
? List.of()
|
||||
: meaningfulRecords.subList(fromIndex, toIndex);
|
||||
Page<ReceivablePayableChangeRecordVO> voPage = new Page<>(current, size, meaningfulRecords.size());
|
||||
voPage.setRecords(pageRecords.stream().map(record -> Objects.requireNonNull(
|
||||
BeanUtil.copyProperties(record, ReceivablePayableChangeRecordVO.class))).toList());
|
||||
return voPage;
|
||||
}
|
||||
@@ -471,8 +481,10 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
List<ReceivablePayableCargoFee> afterRows = activeCargoFees(detail.getId());
|
||||
List<String> changes = reconcileUpdatedFeeAmounts(beforeRows, afterRows);
|
||||
LocalDateTime adjustTime = LocalDateTime.now();
|
||||
for (int index = 0; index < changes.size(); index++) {
|
||||
saveChangeRecord(detail, changes.get(index), request.getAdjustReason(), String.format("%04d", index + 1));
|
||||
saveChangeRecord(detail, changes.get(index), request.getAdjustReason(),
|
||||
String.format("%04d", index + 1), adjustTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -613,14 +625,8 @@ public class ReceivablePayableDetailServiceImpl
|
||||
appendChange(changes, "运输单价", existing.getUnitPrice(), adjusted.getUnitPrice());
|
||||
appendChange(changes, "里程", existing.getMileage(), mileage);
|
||||
appendChange(changes, "运输费", existing.getFreightAmount(), freightAmount);
|
||||
if (!Objects.equals(existing.getRemark(), adjusted.getRemark())) {
|
||||
changes.add("【备注】从[" + Objects.toString(existing.getRemark(), "") + "]调整为["
|
||||
+ Objects.toString(adjusted.getRemark(), "") + "]");
|
||||
}
|
||||
if (!Objects.equals(existing.getChangeReason(), adjusted.getChangeReason())) {
|
||||
changes.add("【变更原因】从[" + Objects.toString(existing.getChangeReason(), "") + "]调整为["
|
||||
+ Objects.toString(adjusted.getChangeReason(), "") + "]");
|
||||
}
|
||||
appendChange(changes, "备注", existing.getRemark(), adjusted.getRemark());
|
||||
appendChange(changes, "变更原因", existing.getChangeReason(), adjusted.getChangeReason());
|
||||
Map<String, Object> oldFeeItems = parseMap(existing.getFeeItemsJson());
|
||||
for (String name : allowedFeeItems) {
|
||||
appendChange(changes, name, decimal(oldFeeItems.get(name)), money(effectiveFeeItems.get(name)));
|
||||
@@ -646,8 +652,9 @@ public class ReceivablePayableDetailServiceImpl
|
||||
if (changes.isEmpty()) {
|
||||
throw new ServiceException("未修改任何费用数据");
|
||||
}
|
||||
LocalDateTime adjustTime = LocalDateTime.now();
|
||||
for (int i = 0; i < changes.size(); i++) {
|
||||
saveChangeRecord(detail, changes.get(i), changeReasons.get(i), String.format("%04d", i + 1));
|
||||
saveChangeRecord(detail, changes.get(i), changeReasons.get(i), String.format("%04d", i + 1), adjustTime);
|
||||
}
|
||||
refreshAdjustedDetail(detail, allRows);
|
||||
}
|
||||
@@ -738,10 +745,13 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<Map<String, Object>> transferCandidates(IPage<?> page, String contractName, String batchNo,
|
||||
public IPage<Map<String, Object>> transferCandidates(IPage<?> page, String documentNo, String contractNo,
|
||||
String contractName, String batchNo,
|
||||
String generateStartDate, String generateEndDate, String settlementBillType,
|
||||
String settlementType) {
|
||||
ReceivablePayableDetailVO query = new ReceivablePayableDetailVO();
|
||||
query.setDocumentNo(documentNo);
|
||||
query.setContractNo(contractNo);
|
||||
query.setContractName(contractName);
|
||||
query.setBatchNo(batchNo);
|
||||
query.setSettlementStatus("pending");
|
||||
@@ -837,6 +847,186 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
}
|
||||
|
||||
private static final String SOURCE_SETTLEMENT_IMPORT = "结算导入";
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<Long> createDetailsFromSettlementImport(String settlementType, Long contractId,
|
||||
List<Map<String, Object>> feeRows) {
|
||||
if (Func.isEmpty(settlementType) || Func.isEmpty(contractId)) {
|
||||
throw new ServiceException("结算类型与合同不能为空");
|
||||
}
|
||||
if (Func.isEmpty(feeRows)) {
|
||||
return List.of();
|
||||
}
|
||||
ContractManage contract = contractManageService.getById(contractId);
|
||||
if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("合同不存在");
|
||||
}
|
||||
String targetSettlementType = settlementType(settlementType);
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
Date generateTime = new Date();
|
||||
List<Long> createdIds = new ArrayList<>();
|
||||
for (Map<String, Object> feeRow : feeRows) {
|
||||
if (feeRow == null) {
|
||||
continue;
|
||||
}
|
||||
Long waybillId = toLong(feeRow.get("waybillId"));
|
||||
if (waybillId == null) {
|
||||
continue;
|
||||
}
|
||||
Waybill waybill = waybillService.getById(waybillId);
|
||||
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("运单不存在:" + waybillId);
|
||||
}
|
||||
assertWaybillNotSettledForImport(waybill, targetSettlementType);
|
||||
|
||||
BigDecimal transportFee = money(decimal(feeRow.get("transportFee")));
|
||||
BigDecimal unitPrice = money(decimal(feeRow.get("unitPrice")));
|
||||
Map<String, BigDecimal> feeItems = normalizeFeeItems(toFeeItemMap(feeRow.get("feeItems")));
|
||||
BigDecimal feeItemTotal = feeItems.values().stream().map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal afterAmount = transportFee.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
|
||||
ReceivablePayableCargoFee cargoFee = buildSettlementImportCargoFee(
|
||||
waybill, transportFee, unitPrice, feeItems, afterAmount, stringValue(feeRow.get("remark")));
|
||||
|
||||
ReceivablePayableDetail importDetail = getOne(Wrappers.<ReceivablePayableDetail>lambdaQuery()
|
||||
.eq(ReceivablePayableDetail::getWaybillId, waybillId)
|
||||
.eq(ReceivablePayableDetail::getSettlementType, targetSettlementType)
|
||||
.eq(ReceivablePayableDetail::getSourceType, SOURCE_SETTLEMENT_IMPORT)
|
||||
.eq(ReceivablePayableDetail::getIsDeleted, 0)
|
||||
.orderByDesc(ReceivablePayableDetail::getCreateTime)
|
||||
.last("limit 1"), false);
|
||||
if (importDetail != null) {
|
||||
refreshSettlementImportDetail(importDetail, waybill, contract, targetSettlementType,
|
||||
cargoFee, unitPrice, stringValue(feeRow.get("remark")), currentUserId, generateTime);
|
||||
createdIds.add(importDetail.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
ReceivablePayableDetail detail = buildDetail(waybill, contract, targetSettlementType,
|
||||
List.of(cargoFee), unitPrice);
|
||||
detail.setSourceType(SOURCE_SETTLEMENT_IMPORT);
|
||||
detail.setSettlementStatus("pending");
|
||||
if (Func.isNotEmpty(stringValue(feeRow.get("remark")))) {
|
||||
detail.setRemark(stringValue(feeRow.get("remark")));
|
||||
}
|
||||
saveAutomaticDetail(detail, List.of(cargoFee), false, currentUserId, generateTime);
|
||||
createdIds.add(detail.getId());
|
||||
}
|
||||
return createdIds;
|
||||
}
|
||||
|
||||
/** 运单在该结算类型下已进入预/正式结算时,禁止再导入。 */
|
||||
private void assertWaybillNotSettledForImport(Waybill waybill, String settlementType) {
|
||||
List<ReceivablePayableDetail> details = list(Wrappers.<ReceivablePayableDetail>lambdaQuery()
|
||||
.eq(ReceivablePayableDetail::getWaybillId, waybill.getId())
|
||||
.eq(ReceivablePayableDetail::getSettlementType, settlementType)
|
||||
.eq(ReceivablePayableDetail::getIsDeleted, 0));
|
||||
for (ReceivablePayableDetail detail : details) {
|
||||
String settlementStatus = Func.toStr(detail.getSettlementStatus(), "pending");
|
||||
if (Func.isNotEmpty(detail.getFormalSettlementNo())
|
||||
|| Func.isNotEmpty(detail.getPreSettlementNo())
|
||||
|| List.of("pre_settled", "formal_settled", "closed").contains(settlementStatus)) {
|
||||
throw new ServiceException("运单" + Func.toStr(waybill.getWaybillNo(), String.valueOf(waybill.getId()))
|
||||
+ "已进入结算,无法重复导入");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ReceivablePayableCargoFee buildSettlementImportCargoFee(Waybill waybill, BigDecimal transportFee,
|
||||
BigDecimal unitPrice, Map<String, BigDecimal> feeItems,
|
||||
BigDecimal afterAmount, String remark) {
|
||||
ReceivablePayableCargoFee cargoFee = new ReceivablePayableCargoFee();
|
||||
cargoFee.setWaybillId(waybill.getId());
|
||||
cargoFee.setLineNo("0001");
|
||||
cargoFee.setDataSource(FEE_SOURCE_MANUAL);
|
||||
cargoFee.setCargoName(waybill.getCargoName());
|
||||
cargoFee.setCargoType(waybill.getCargoType());
|
||||
cargoFee.setSpecification(waybill.getSpecification());
|
||||
cargoFee.setModel(waybill.getModel());
|
||||
cargoFee.setBillingFactor("按重量");
|
||||
cargoFee.setBillingType("固定单价");
|
||||
cargoFee.setTransportQuantity(money(waybill.getQuantity()));
|
||||
cargoFee.setQuantityUnit(waybill.getQuantityUnit());
|
||||
cargoFee.setPriceUnit(waybill.getPriceUnit());
|
||||
cargoFee.setUnitPrice(unitPrice);
|
||||
cargoFee.setMileage(normalizeGeneratedMileage(waybill.getMileage()));
|
||||
cargoFee.setFreightAmount(transportFee);
|
||||
cargoFee.setFeeItemsJson(JsonUtil.toJson(feeItems));
|
||||
cargoFee.setOriginalAmount(afterAmount);
|
||||
cargoFee.setAdjustAmount(BigDecimal.ZERO.setScale(2));
|
||||
cargoFee.setAfterAmount(afterAmount);
|
||||
cargoFee.setRemark(remark);
|
||||
return cargoFee;
|
||||
}
|
||||
|
||||
/** 刷新已有结算导入明细:费用项目与金额始终覆盖为本次导入数据。 */
|
||||
private void refreshSettlementImportDetail(ReceivablePayableDetail detail, Waybill waybill, ContractManage contract,
|
||||
String settlementType, ReceivablePayableCargoFee cargoFee,
|
||||
BigDecimal unitPrice, String remark, Long currentUserId, Date generateTime) {
|
||||
ReceivablePayableDetail rebuilt = buildDetail(waybill, contract, settlementType, List.of(cargoFee), unitPrice);
|
||||
detail.setFreightAmount(rebuilt.getFreightAmount());
|
||||
detail.setOtherFeeAmount(rebuilt.getOtherFeeAmount());
|
||||
detail.setTotalAmount(rebuilt.getTotalAmount());
|
||||
detail.setUnitPrice(rebuilt.getUnitPrice());
|
||||
detail.setCurrency(rebuilt.getCurrency());
|
||||
detail.setFeeItemsJson(rebuilt.getFeeItemsJson());
|
||||
detail.setContractId(rebuilt.getContractId());
|
||||
detail.setContractNo(rebuilt.getContractNo());
|
||||
detail.setContractName(rebuilt.getContractName());
|
||||
detail.setCustomerName(rebuilt.getCustomerName());
|
||||
detail.setSourceType(SOURCE_SETTLEMENT_IMPORT);
|
||||
detail.setSettlementStatus("pending");
|
||||
if (Func.isNotEmpty(remark)) {
|
||||
detail.setRemark(remark);
|
||||
}
|
||||
updateById(detail);
|
||||
cargoFeeMapper.delete(Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||
.eq(ReceivablePayableCargoFee::getDetailId, detail.getId()));
|
||||
ReceivablePayableCargoFee copy = BeanUtil.copyProperties(cargoFee, ReceivablePayableCargoFee.class);
|
||||
copy.setId(null);
|
||||
copy.setDetailId(detail.getId());
|
||||
fillGeneratedAuditFields(copy, currentUserId, generateTime);
|
||||
cargoFeeMapper.insert(copy);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, BigDecimal> toFeeItemMap(Object value) {
|
||||
Map<String, BigDecimal> result = new LinkedHashMap<>();
|
||||
if (!(value instanceof Map<?, ?> raw)) {
|
||||
return result;
|
||||
}
|
||||
raw.forEach((key, amount) -> {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
result.put(String.valueOf(key), decimal(amount));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value == null || "".equals(value)) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(String.valueOf(value).trim());
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String stringValue(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
|
||||
rollbackFor = Exception.class)
|
||||
@@ -2367,12 +2557,56 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
|
||||
private void appendChange(List<String> changes, String field, String before, String after) {
|
||||
if (!Objects.equals(before, after)) {
|
||||
changes.add("【" + field + "】从[" + Objects.toString(before, "") + "]调整为["
|
||||
+ Objects.toString(after, "") + "]");
|
||||
String oldValue = normalizeChangeText(before);
|
||||
String newValue = normalizeChangeText(after);
|
||||
if (!Objects.equals(oldValue, newValue)) {
|
||||
changes.add("【" + field + "】从[" + oldValue + "]调整为[" + newValue + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 变更文本归一化:null / 空白视为相同,避免出现「从[]调整为[]」无效记录
|
||||
*/
|
||||
private String normalizeChangeText(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在实际前后差异;整段内容均无变化时不展示
|
||||
*/
|
||||
private boolean hasMeaningfulChange(String content) {
|
||||
if (Func.isBlank(content)) {
|
||||
return false;
|
||||
}
|
||||
String text = content.trim();
|
||||
int suffixStart = text.lastIndexOf('(');
|
||||
if (suffixStart > 0 && text.endsWith(")")) {
|
||||
text = text.substring(0, suffixStart);
|
||||
}
|
||||
boolean hasPart = false;
|
||||
for (String part : text.split(";")) {
|
||||
if (Func.isBlank(part)) {
|
||||
continue;
|
||||
}
|
||||
hasPart = true;
|
||||
if (isMeaningfulChangePart(part.trim())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return !hasPart;
|
||||
}
|
||||
|
||||
private boolean isMeaningfulChangePart(String part) {
|
||||
int fromIdx = part.indexOf("从[");
|
||||
int toIdx = part.indexOf("]调整为[");
|
||||
if (fromIdx < 0 || toIdx < fromIdx || !part.endsWith("]")) {
|
||||
return true;
|
||||
}
|
||||
String before = part.substring(fromIdx + 2, toIdx);
|
||||
String after = part.substring(toIdx + 5, part.length() - 1);
|
||||
return !Objects.equals(before, after);
|
||||
}
|
||||
|
||||
private String formatValue(BigDecimal value) {
|
||||
return money(value).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
@@ -2613,10 +2847,18 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
|
||||
private void saveChangeRecord(ReceivablePayableDetail detail, String content, String reason) {
|
||||
saveChangeRecord(detail, content, reason, "0001");
|
||||
saveChangeRecord(detail, content, reason, "0001", LocalDateTime.now());
|
||||
}
|
||||
|
||||
private void saveChangeRecord(ReceivablePayableDetail detail, String content, String reason, String lineNo) {
|
||||
saveChangeRecord(detail, content, reason, lineNo, LocalDateTime.now());
|
||||
}
|
||||
|
||||
private void saveChangeRecord(ReceivablePayableDetail detail, String content, String reason, String lineNo,
|
||||
LocalDateTime adjustTime) {
|
||||
if (!hasMeaningfulChange(content)) {
|
||||
return;
|
||||
}
|
||||
ReceivablePayableChangeRecord record = new ReceivablePayableChangeRecord();
|
||||
record.setDetailId(detail.getId());
|
||||
record.setLineNo(lineNo);
|
||||
@@ -2625,7 +2867,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
record.setAdjustUser(AuthUtil.getUserId());
|
||||
record.setAdjustUserName(AuthUtil.getUserName());
|
||||
record.setAdjustReason(reason);
|
||||
record.setAdjustTime(LocalDateTime.now());
|
||||
record.setAdjustTime(adjustTime == null ? LocalDateTime.now() : adjustTime);
|
||||
changeRecordMapper.insert(record);
|
||||
}
|
||||
|
||||
@@ -2979,6 +3221,11 @@ public class ReceivablePayableDetailServiceImpl
|
||||
}
|
||||
|
||||
private String formatQuantity(BigDecimal quantity, String unit) {
|
||||
// 无有效运输量(空/0/-1)时不展示数值与单位
|
||||
if (quantity == null || quantity.compareTo(BigDecimal.ZERO) == 0
|
||||
|| quantity.compareTo(BigDecimal.valueOf(-1)) == 0) {
|
||||
return "-";
|
||||
}
|
||||
return money(quantity).stripTrailingZeros().toPlainString() + (unit == null ? "" : " " + unit);
|
||||
}
|
||||
|
||||
|
||||
+17
-21
@@ -9,6 +9,7 @@ import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementDetailMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementChangeRecordMapper;
|
||||
@@ -52,11 +53,6 @@ import java.util.Set;
|
||||
@RequiredArgsConstructor
|
||||
public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementAdjustmentMapper, SettlementAdjustment>
|
||||
implements ISettlementAdjustmentService {
|
||||
private static final String DRAFT = "draft";
|
||||
private static final String REVIEWING = "reviewing";
|
||||
private static final String APPROVED = "approved";
|
||||
private static final String RETURNED = "returned";
|
||||
private static final String VOIDED = "voided";
|
||||
private static final String PAID = "paid";
|
||||
private final SettlementAdjustmentDetailMapper detailMapper;
|
||||
private final FormalSettlementMapper formalMapper;
|
||||
@@ -101,7 +97,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
@Override
|
||||
public List<Map<String, Object>> candidateFormalSettlements(String keyword) {
|
||||
List<FormalSettlement> rows = formalMapper.selectList(Wrappers.<FormalSettlement>lambdaQuery()
|
||||
.eq(FormalSettlement::getApprovalStatus, APPROVED)
|
||||
.eq(FormalSettlement::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.and(wrapper -> wrapper.isNull(FormalSettlement::getPaymentStatus)
|
||||
.or().ne(FormalSettlement::getPaymentStatus, PAID))
|
||||
.like(Func.isNotEmpty(keyword), FormalSettlement::getFormalSettlementNo, keyword)
|
||||
@@ -152,7 +148,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
FormalSettlement formal = request.getFormalSettlementId() == null
|
||||
? null : adjustableFormalSettlement(request.getFormalSettlementId());
|
||||
if (adjustment.getId() == null) adjustment.setAdjustmentNo(nextNo());
|
||||
adjustment.setApprovalStatus(DRAFT);
|
||||
adjustment.setApprovalStatus(TransportBusiConstant.DRAFT);
|
||||
adjustment.setCurrentNode("草稿");
|
||||
adjustment.setCurrentProcessor(AuthUtil.getUserName());
|
||||
adjustment.setApprovedTime(null);
|
||||
@@ -185,7 +181,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void submit(SettlementAdjustmentStatusRequest request) {
|
||||
SettlementAdjustment item = existing(request.getId());
|
||||
if (!DRAFT.equals(item.getApprovalStatus())) throw new ServiceException("仅草稿状态的调整单允许提交");
|
||||
if (!TransportBusiConstant.DRAFT.equals(item.getApprovalStatus())) throw new ServiceException("仅草稿状态的调整单允许提交");
|
||||
FormalSettlement formal = adjustableFormalSettlement(item.getFormalSettlementId());
|
||||
List<SettlementAdjustmentDetail> details = detailMapper.selectList(
|
||||
Wrappers.<SettlementAdjustmentDetail>lambdaQuery()
|
||||
@@ -204,15 +200,15 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
limit(detail.getRemark(), 200);
|
||||
}
|
||||
limit(item.getRemark(), 200);
|
||||
changeStatus(item.getId(), DRAFT, REVIEWING, "审批中", null);
|
||||
changeStatus(item.getId(), TransportBusiConstant.DRAFT, TransportBusiConstant.REVIEWING, "审批中", null);
|
||||
}
|
||||
@Override public void returnBill(SettlementAdjustmentStatusRequest request) { changeStatus(request.getId(), REVIEWING, RETURNED, "已驳回", limit(request.getReason(), 200)); }
|
||||
@Override public void returnBill(SettlementAdjustmentStatusRequest request) { changeStatus(request.getId(), TransportBusiConstant.REVIEWING, TransportBusiConstant.RETURNED, "已驳回", limit(request.getReason(), 200)); }
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String repush(Long adjustmentId) {
|
||||
SettlementAdjustment adjustment = existing(adjustmentId);
|
||||
if (!APPROVED.equals(adjustment.getApprovalStatus())) throw new ServiceException("仅审批通过的结算调整单允许重新推送");
|
||||
if (!TransportBusiConstant.APPROVED.equals(adjustment.getApprovalStatus())) throw new ServiceException("仅审批通过的结算调整单允许重新推送");
|
||||
FormalSettlement formal = formalMapper.selectById(adjustment.getFormalSettlementId());
|
||||
if (formal == null || !"synced".equals(formal.getKingdeeSyncStatus())) throw new ServiceException("关联正式结算单尚未推送金蝶,无需重新推送");
|
||||
String kingdeeNo = "K3AP" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
|
||||
@@ -224,7 +220,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void approve(SettlementAdjustmentStatusRequest request) {
|
||||
SettlementAdjustment adjustment = existing(request.getId());
|
||||
if (!REVIEWING.equals(adjustment.getApprovalStatus())) throw new ServiceException("仅审批中的结算调整单允许审核");
|
||||
if (!TransportBusiConstant.REVIEWING.equals(adjustment.getApprovalStatus())) throw new ServiceException("仅审批中的结算调整单允许审核");
|
||||
FormalSettlement formal = adjustableFormalSettlement(adjustment.getFormalSettlementId());
|
||||
for (SettlementAdjustmentDetail item : detailMapper.selectList(Wrappers.<SettlementAdjustmentDetail>lambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, adjustment.getId()))) {
|
||||
if (money(item.getAdjustmentAmountTax()).signum() == 0) continue;
|
||||
@@ -261,7 +257,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
saveFormalChange(formal.getId(), "合计费用项", null, "调整",
|
||||
"调整单" + adjustment.getAdjustmentNo() + "调整金额【" + money(adjustment.getAdjustmentAmount()) + "】",
|
||||
adjustment.getRemark());
|
||||
adjustment.setKingdeeSyncStatus(formal.getKingdeeSyncStatus()); adjustment.setApprovalStatus(APPROVED); adjustment.setCurrentNode("审批通过"); adjustment.setCurrentProcessor(AuthUtil.getUserName()); adjustment.setApprovedTime(LocalDateTime.now()); updateById(adjustment);
|
||||
adjustment.setKingdeeSyncStatus(formal.getKingdeeSyncStatus()); adjustment.setApprovalStatus(TransportBusiConstant.APPROVED); adjustment.setCurrentNode("审批通过"); adjustment.setCurrentProcessor(AuthUtil.getUserName()); adjustment.setApprovedTime(LocalDateTime.now()); updateById(adjustment);
|
||||
}
|
||||
|
||||
private void saveFormalChange(Long settlementId, String changeType, Integer lineNo,
|
||||
@@ -280,7 +276,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
|
||||
private void changeStatus(Long id, String from, String to, String node, String reason) { SettlementAdjustment item = existing(id); if (!from.equals(item.getApprovalStatus())) throw new ServiceException("当前状态不允许此操作"); item.setApprovalStatus(to); item.setCurrentNode(node); if (reason != null) item.setRemark(reason); updateById(item); }
|
||||
private SettlementAdjustment existing(Long id) { SettlementAdjustment item = getById(id); if (item == null || Objects.equals(item.getIsDeleted(), 1)) throw new ServiceException("结算调整单不存在"); return item; }
|
||||
private SettlementAdjustment editable(Long id) { SettlementAdjustment item = existing(id); if (!(DRAFT.equals(item.getApprovalStatus()) || RETURNED.equals(item.getApprovalStatus()))) throw new ServiceException("仅草稿或驳回的调整单可编辑"); return item; }
|
||||
private SettlementAdjustment editable(Long id) { SettlementAdjustment item = existing(id); if (!(TransportBusiConstant.DRAFT.equals(item.getApprovalStatus()) || TransportBusiConstant.RETURNED.equals(item.getApprovalStatus()))) throw new ServiceException("仅草稿或驳回的调整单可编辑"); return item; }
|
||||
private void applyFormalSnapshot(SettlementAdjustment adjustment, FormalSettlement formal, Long formalSettlementId) {
|
||||
adjustment.setFormalSettlementId(formalSettlementId);
|
||||
if (formal == null) {
|
||||
@@ -299,7 +295,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
private FormalSettlement adjustableFormalSettlement(Long formalSettlementId) {
|
||||
if (formalSettlementId == null) throw new ServiceException("关联正式结算单不存在");
|
||||
FormalSettlement formal = formalMapper.selectById(formalSettlementId);
|
||||
if (formal == null || !APPROVED.equals(formal.getApprovalStatus())) {
|
||||
if (formal == null || !TransportBusiConstant.APPROVED.equals(formal.getApprovalStatus())) {
|
||||
throw new ServiceException("仅审批通过的正式结算单可调整");
|
||||
}
|
||||
if (isSettlementCompleted(formal)) {
|
||||
@@ -331,15 +327,15 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
Wrappers.<FormalSettlementPayment>lambdaQuery()
|
||||
.eq(FormalSettlementPayment::getIsDeleted, 0)
|
||||
.in(FormalSettlementPayment::getFormalSettlementId, formalSettlementIds)
|
||||
.notIn(FormalSettlementPayment::getBillStatus, RETURNED, VOIDED)).stream()
|
||||
.notIn(FormalSettlementPayment::getBillStatus, TransportBusiConstant.RETURNED, TransportBusiConstant.VOIDED)).stream()
|
||||
.map(FormalSettlementPayment::getFormalSettlementId)
|
||||
.filter(Objects::nonNull).toList());
|
||||
result.addAll(paymentApplicationMapper.selectList(Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.select(PaymentApplication::getSettlementId)
|
||||
.in(PaymentApplication::getSettlementId, formalSettlementIds)
|
||||
.eq(PaymentApplication::getPaymentType, "settlement_payment")
|
||||
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT)
|
||||
.eq(PaymentApplication::getIsDeleted, 0)
|
||||
.notIn(PaymentApplication::getApprovalStatus, RETURNED, VOIDED)).stream()
|
||||
.notIn(PaymentApplication::getApprovalStatus, TransportBusiConstant.RETURNED, TransportBusiConstant.VOIDED)).stream()
|
||||
.map(PaymentApplication::getSettlementId).filter(Objects::nonNull).toList());
|
||||
List<PaymentApplicationSettlement> relations = paymentApplicationSettlementMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
@@ -355,9 +351,9 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.select(PaymentApplication::getId)
|
||||
.in(PaymentApplication::getId, applicationIds)
|
||||
.eq(PaymentApplication::getPaymentType, "settlement_payment")
|
||||
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT)
|
||||
.eq(PaymentApplication::getIsDeleted, 0)
|
||||
.notIn(PaymentApplication::getApprovalStatus, RETURNED, VOIDED)).stream()
|
||||
.notIn(PaymentApplication::getApprovalStatus, TransportBusiConstant.RETURNED, TransportBusiConstant.VOIDED)).stream()
|
||||
.map(PaymentApplication::getId).filter(Objects::nonNull).toList());
|
||||
relations.stream().filter(relation -> activeApplicationIds.contains(relation.getPaymentApplicationId()))
|
||||
.map(PaymentApplicationSettlement::getFormalSettlementId).filter(Objects::nonNull)
|
||||
@@ -394,7 +390,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
formalSummaryFeeMapper.updateById(summaryFee);
|
||||
}
|
||||
private SettlementAdjustmentVO toVO(SettlementAdjustment item) { SettlementAdjustmentVO vo = new SettlementAdjustmentVO(); org.springframework.beans.BeanUtils.copyProperties(item, vo); vo.setCreateUserName(UserCache.getUserRealName(item.getCreateUser())); vo.setApprovalStatusName(statusName(item.getApprovalStatus())); vo.setSettlementTypeName(typeName(item.getSettlementType())); return vo; }
|
||||
private String statusName(String value) { return Map.of(DRAFT, "草稿", REVIEWING, "审批中", APPROVED, "审批通过", RETURNED, "已驳回").getOrDefault(value, value); }
|
||||
private String statusName(String value) { return Map.of(TransportBusiConstant.DRAFT, "草稿", TransportBusiConstant.REVIEWING, "审批中", TransportBusiConstant.APPROVED, "审批通过", TransportBusiConstant.RETURNED, "已驳回").getOrDefault(value, value); }
|
||||
private String typeName(String value) { return "receivable".equals(value) ? "应收" : "payable".equals(value) ? "应付" : ""; }
|
||||
private synchronized String nextNo() { String prefix = "TZ" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); long count = count(Wrappers.<SettlementAdjustment>lambdaQuery().likeRight(SettlementAdjustment::getAdjustmentNo, prefix)); return prefix + String.format("%04d", count + 1); }
|
||||
private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
|
||||
|
||||
+1144
File diff suppressed because it is too large
Load Diff
+211
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -225,6 +225,11 @@ public class TransportVehicleServiceImpl extends BaseServiceImpl<TransportVehicl
|
||||
vehicle.setRoadTransportCertImage(trimToNull(vehicle.getRoadTransportCertImage()));
|
||||
vehicle.setRegistrationImage(trimToNull(vehicle.getRegistrationImage()));
|
||||
vehicle.setRemark(trimToNull(vehicle.getRemark()));
|
||||
// BladeX 数字 null 常序列化为 -1;未填外廓尺寸/准牵引总质量不得落库占位值
|
||||
vehicle.setOuterLength(normalizeOptionalInteger(vehicle.getOuterLength()));
|
||||
vehicle.setOuterWidth(normalizeOptionalInteger(vehicle.getOuterWidth()));
|
||||
vehicle.setOuterHeight(normalizeOptionalInteger(vehicle.getOuterHeight()));
|
||||
vehicle.setTractionMassKg(normalizeOptionalInteger(vehicle.getTractionMassKg()));
|
||||
if (vehicle.getCompulsoryScrapLongTerm() == null) {
|
||||
vehicle.setCompulsoryScrapLongTerm(DEFAULT_FALSE);
|
||||
}
|
||||
@@ -352,6 +357,16 @@ public class TransportVehicleServiceImpl extends BaseServiceImpl<TransportVehicl
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可选整型字段:null / -1 均视为未填写
|
||||
*/
|
||||
private Integer normalizeOptionalInteger(Integer value) {
|
||||
if (value == null || value == -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String trimToEmpty(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
+71
-3
@@ -88,6 +88,10 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
private final ObjectProvider<MinioClient> minioClientProvider;
|
||||
private final String minioBucketName;
|
||||
private final String minioRootDirectory;
|
||||
private final String minioTransformEndpoint;
|
||||
private final String minioAccessKey;
|
||||
private final String minioSecretKey;
|
||||
private volatile MinioClient transformPresignClient;
|
||||
|
||||
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService,
|
||||
IWaybillService waybillService,
|
||||
@@ -95,7 +99,10 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
LoadingManageMapper loadingManageMapper,
|
||||
ApplicationEventPublisher eventPublisher, ObjectProvider<MinioClient> minioClientProvider,
|
||||
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName,
|
||||
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) {
|
||||
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory,
|
||||
@Value("${file.storage.minio.transform-endpoint:${minio.transform-endpoint:}}") String minioTransformEndpoint,
|
||||
@Value("${file.storage.minio.access-key-id:${minio.access-key:}}") String minioAccessKey,
|
||||
@Value("${file.storage.minio.access-key-secret:${minio.secret-key:}}") String minioSecretKey) {
|
||||
this.voucherWaybillBatchMapper = voucherWaybillBatchMapper;
|
||||
this.projectApplyService = projectApplyService;
|
||||
this.waybillService = waybillService;
|
||||
@@ -106,6 +113,9 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
this.minioClientProvider = minioClientProvider;
|
||||
this.minioBucketName = minioBucketName;
|
||||
this.minioRootDirectory = minioRootDirectory;
|
||||
this.minioTransformEndpoint = minioTransformEndpoint;
|
||||
this.minioAccessKey = minioAccessKey;
|
||||
this.minioSecretKey = minioSecretKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1490,14 +1500,72 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
|
||||
private String buildVoucherFileUrl(Long fileId, String objectKey) {
|
||||
try {
|
||||
return minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET).bucket(minioBucketName).object(objectKey).expiry(1, java.util.concurrent.TimeUnit.HOURS).build());
|
||||
String presignedUrl = resolvePresignClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET).bucket(minioBucketName).object(objectKey)
|
||||
.region("us-east-1")
|
||||
.expiry(1, java.util.concurrent.TimeUnit.HOURS).build());
|
||||
return applyTransformPathPrefix(presignedUrl);
|
||||
} catch (Exception exception) {
|
||||
log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", fileId, objectKey, exception);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器预签名优先使用外网地址,避免 HTTPS 页面访问 HTTP MinIO 被 Mixed Content 拦截。
|
||||
* 外网域名通常是业务网关,必须固定 region,否则 SDK 会请求 GetBucketLocation 并解析到 HTML。
|
||||
*/
|
||||
private MinioClient resolvePresignClient() {
|
||||
if (Func.isEmpty(minioTransformEndpoint)) {
|
||||
return minioClient();
|
||||
}
|
||||
MinioClient cachedClient = transformPresignClient;
|
||||
if (cachedClient != null) {
|
||||
return cachedClient;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (transformPresignClient == null) {
|
||||
transformPresignClient = org.springblade.transport.config.VoucherMinioConfig.create(
|
||||
minioTransformEndpoint, minioAccessKey, minioSecretKey);
|
||||
}
|
||||
return transformPresignClient;
|
||||
}
|
||||
}
|
||||
|
||||
private String applyTransformPathPrefix(String presignedUrl) {
|
||||
if (Func.isEmpty(minioTransformEndpoint) || Func.isEmpty(presignedUrl)) {
|
||||
return presignedUrl;
|
||||
}
|
||||
java.net.URI transformUri = java.net.URI.create(minioTransformEndpoint.trim());
|
||||
String pathPrefix = normalizePathPrefix(transformUri.getPath());
|
||||
if (Func.isEmpty(pathPrefix)) {
|
||||
return presignedUrl;
|
||||
}
|
||||
java.net.URI url = java.net.URI.create(presignedUrl);
|
||||
String path = url.getRawPath() == null ? "" : url.getRawPath();
|
||||
if (path.equals(pathPrefix) || path.startsWith(pathPrefix + "/")) {
|
||||
return presignedUrl;
|
||||
}
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append(url.getScheme()).append("://").append(url.getHost());
|
||||
if (url.getPort() > 0) {
|
||||
result.append(':').append(url.getPort());
|
||||
}
|
||||
result.append(pathPrefix).append(path);
|
||||
if (Func.isNotEmpty(url.getRawQuery())) {
|
||||
result.append('?').append(url.getRawQuery());
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private String normalizePathPrefix(String path) {
|
||||
if (Func.isEmpty(path) || "/".equals(path)) {
|
||||
return "";
|
||||
}
|
||||
String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
|
||||
return normalized.startsWith("/") ? normalized : "/" + normalized;
|
||||
}
|
||||
|
||||
private String normalizePlateNo(String plateNo) {
|
||||
return plateNo == null ? null : plateNo.replaceAll("[\\s-]", "").toUpperCase();
|
||||
}
|
||||
|
||||
+55
-3
@@ -342,10 +342,11 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
.eq(WaybillImportBatch::getIsDeleted, 0)
|
||||
.eq(request.getId() != null, WaybillImportBatch::getId, request.getId())
|
||||
.like(Func.isNotEmpty(request.getBatchNo()), WaybillImportBatch::getBatchNo, request.getBatchNo())
|
||||
.apply(Func.isNotEmpty(request.getCarrierId()), "FIND_IN_SET({0}, carrier_ids)", request.getCarrierId())
|
||||
.eq(Func.isNotEmpty(request.getCarrierName()), WaybillImportBatch::getCarrierName, request.getCarrierName())
|
||||
.eq(Func.isNotEmpty(request.getCreateUser()), WaybillImportBatch::getCreateUser, request.getCreateUser())
|
||||
.orderByDesc(WaybillImportBatch::getCreateTime);
|
||||
// 历史批次可能只落了承运商名称、carrier_ids 为空,筛选时需同时按 ID 与名称兼容匹配
|
||||
applyCarrierFilter(queryWrapper, request.getCarrierId());
|
||||
if (Func.isNotEmpty(request.getCreateTimeStart())) {
|
||||
queryWrapper.ge(WaybillImportBatch::getCreateTime,
|
||||
DateUtil.parse(request.getCreateTimeStart(), DateUtil.PATTERN_DATETIME));
|
||||
@@ -361,6 +362,24 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
return resultPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 承运商筛选:优先 FIND_IN_SET(carrier_ids),同时兼容仅有 carrier_name 的历史数据。
|
||||
*/
|
||||
private void applyCarrierFilter(LambdaQueryWrapper<WaybillImportBatch> queryWrapper, Long carrierId) {
|
||||
Long normalizedCarrierId = normalizeOptionalId(carrierId);
|
||||
if (normalizedCarrierId == null) {
|
||||
return;
|
||||
}
|
||||
CustomerArchive carrier = customerArchiveService.getById(normalizedCarrierId);
|
||||
String carrierName = carrier == null ? null : Func.toStr(carrier.getFullName()).trim();
|
||||
queryWrapper.and(wrapper -> {
|
||||
wrapper.apply("FIND_IN_SET({0}, IFNULL(carrier_ids,''))", String.valueOf(normalizedCarrierId));
|
||||
if (Func.isNotEmpty(carrierName)) {
|
||||
wrapper.or().eq(WaybillImportBatch::getCarrierName, carrierName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public BusinessRemoveResultVO removeBatches(String ids) {
|
||||
@@ -405,8 +424,9 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
WaybillImportBatch batch = new WaybillImportBatch();
|
||||
BeanUtil.copyProperties(request, batch);
|
||||
batch.setBatchNo(Func.isEmpty(request.getId()) ? nextCode() : request.getBatchNo());
|
||||
batch.setCarrierIds(joinIds(request.getCarrierIds()));
|
||||
batch.setCarrierName(resolveCarrierNames(request.getCarrierIds(), request.getCarrierName()));
|
||||
List<Long> carrierIds = resolveCarrierIds(request);
|
||||
batch.setCarrierIds(joinIds(carrierIds));
|
||||
batch.setCarrierName(resolveCarrierNames(carrierIds, request.getCarrierName()));
|
||||
batch.setImportStatus(importStatus);
|
||||
batch.setImportType(IMPORT_TYPE_SETTLEMENT.equals(request.getImportType()) ? IMPORT_TYPE_SETTLEMENT : IMPORT_TYPE_WAYBILL);
|
||||
ProjectApply project = projectApplyService.getById(request.getProjectId());
|
||||
@@ -586,6 +606,38 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
return Func.toLongList(carrierIds).stream().findFirst().orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析承运商 ID 列表:优先 carrierIds,其次 carrierId,最后按承运商名称回查客商档案。
|
||||
*/
|
||||
private List<Long> resolveCarrierIds(WaybillImportBatchRequest request) {
|
||||
if (request == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<Long> carrierIds = Func.isEmpty(request.getCarrierIds())
|
||||
? new ArrayList<>()
|
||||
: request.getCarrierIds().stream()
|
||||
.map(this::normalizeOptionalId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
if (Func.isNotEmpty(carrierIds)) {
|
||||
return carrierIds;
|
||||
}
|
||||
Long carrierId = normalizeOptionalId(request.getCarrierId());
|
||||
if (carrierId != null) {
|
||||
return List.of(carrierId);
|
||||
}
|
||||
String carrierName = Func.toStr(request.getCarrierName()).trim();
|
||||
if (Func.isEmpty(carrierName)) {
|
||||
return List.of();
|
||||
}
|
||||
CustomerArchive carrier = customerArchiveService.getOne(Wrappers.<CustomerArchive>lambdaQuery()
|
||||
.eq(CustomerArchive::getFullName, carrierName)
|
||||
.eq(CustomerArchive::getIsDeleted, 0)
|
||||
.last("limit 1"));
|
||||
return carrier == null || carrier.getId() == null ? List.of() : List.of(carrier.getId());
|
||||
}
|
||||
|
||||
private String resolveCarrierNames(List<Long> carrierIds, String carrierName) {
|
||||
if (Func.isNotEmpty(carrierName)) return carrierName;
|
||||
if (Func.isEmpty(carrierIds)) return null;
|
||||
|
||||
+12
-19
@@ -1211,15 +1211,14 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
|
||||
@Override
|
||||
public List<WaybillExcel> exportWaybill(WaybillVO waybill, String ids) {
|
||||
TransportBusinessSupport.validateAllDept(waybill.getAllDept(), "运单管理");
|
||||
List<Long> idList = Func.toLongList(ids);
|
||||
LambdaQueryWrapper<Waybill> queryWrapper;
|
||||
if (Func.isNotEmpty(idList)) {
|
||||
queryWrapper = Wrappers.<Waybill>lambdaQuery().eq(Waybill::getIsDeleted, 0);
|
||||
if (!Objects.equals(waybill.getAllDept(), 1)) {
|
||||
queryWrapper.eq(Waybill::getDeptId, TransportBusinessSupport.currentDeptId("运单管理"));
|
||||
} else if (Func.isNotEmpty(waybill.getDeptId())) {
|
||||
queryWrapper.eq(Waybill::getDeptId, waybill.getDeptId());
|
||||
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(
|
||||
waybill.getAllDept(), waybill.getDeptId(), "运单管理");
|
||||
if (Func.isNotEmpty(deptIdList)) {
|
||||
queryWrapper.eq(Waybill::getDeptId, deptIdList.get(0));
|
||||
}
|
||||
queryWrapper.in(Waybill::getId, idList).orderByDesc(Waybill::getCreateTime);
|
||||
} else {
|
||||
@@ -1344,21 +1343,15 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
target.setFreightJson(source.getFreightJson());
|
||||
target.setAttachmentsJson(source.getAttachmentsJson());
|
||||
target.setRemark(source.getRemark());
|
||||
// 复制后业务状态与原运单保持一致
|
||||
String sourceBusinessStatus = source.getBusinessStatus();
|
||||
target.setBusinessStatus(sourceBusinessStatus);
|
||||
// 复制新建固定为草稿,不沿用原运单业务状态
|
||||
target.setBusinessStatus(STATUS_DRAFT);
|
||||
target.setWaybillNo(nextCode());
|
||||
clearDriverAcceptRecord(target);
|
||||
fillProjectProcessConfig(target);
|
||||
prepare(target);
|
||||
applyDriverAcceptBusinessStatus(target);
|
||||
if (Func.isNotEmpty(sourceBusinessStatus)) {
|
||||
target.setBusinessStatus(sourceBusinessStatus);
|
||||
}
|
||||
target.setBusinessStatus(STATUS_DRAFT);
|
||||
validate(target);
|
||||
save(target);
|
||||
// 原运单已完成时,复制后按合同系统计费规则生成应收应付明细
|
||||
generateReceivablePayableIfCompleted(target);
|
||||
return detail(target.getId());
|
||||
}
|
||||
|
||||
@@ -1610,12 +1603,12 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<Waybill> buildQuery(WaybillVO waybill) {
|
||||
TransportBusinessSupport.validateAllDept(waybill.getAllDept(), "运单管理");
|
||||
LambdaQueryWrapper<Waybill> queryWrapper = Wrappers.<Waybill>lambdaQuery().eq(Waybill::getIsDeleted, 0);
|
||||
if (!Objects.equals(waybill.getAllDept(), 1)) {
|
||||
queryWrapper.eq(Waybill::getDeptId, TransportBusinessSupport.currentDeptId("运单管理"));
|
||||
} else if (Func.isNotEmpty(waybill.getDeptId())) {
|
||||
queryWrapper.eq(Waybill::getDeptId, waybill.getDeptId());
|
||||
// 与常用线路等模块一致:超管默认不按组织过滤;普通用户 allDept!=1 时强制当前组织
|
||||
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(
|
||||
waybill.getAllDept(), waybill.getDeptId(), "运单管理");
|
||||
if (Func.isNotEmpty(deptIdList)) {
|
||||
queryWrapper.eq(Waybill::getDeptId, deptIdList.get(0));
|
||||
}
|
||||
if (Func.isNotEmpty(waybill.getWaybillNo())) {
|
||||
queryWrapper.eq(Waybill::getWaybillNo, waybill.getWaybillNo());
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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.support;
|
||||
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 外部系统来源单据号组装工具
|
||||
* <p>
|
||||
* 统一外部单据号(如金蝶 awrp_src_code)的拼接规则:主单据号 + 子单据号列表按下划线拼接;
|
||||
* 自动去空白、去重,便于后续各链路通用编码与扩展处理
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class ExternalSrcCodeSupport {
|
||||
|
||||
/**
|
||||
* 外部单据号拼接分隔符
|
||||
*/
|
||||
public static final String SEPARATOR = "_";
|
||||
|
||||
private ExternalSrcCodeSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装外部系统来源单据号:主单据号 + 子单据号按下划线拼接
|
||||
* <p>各部分自动去首尾空格、过滤空白并去重;主单据号为空时仅拼接子单据号</p>
|
||||
*
|
||||
* @param mainCode 主单据号(如付款申请号)
|
||||
* @param partCodes 子单据号列表(如结算明细单据号)
|
||||
* @return 拼接后的外部来源单据号,无有效值时返回空串
|
||||
*/
|
||||
public static String buildSrcCode(String mainCode, Collection<String> partCodes) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
if (Func.isNotBlank(mainCode)) {
|
||||
parts.add(mainCode.trim());
|
||||
}
|
||||
if (Func.isNotEmpty(partCodes)) {
|
||||
partCodes.stream().filter(Func::isNotBlank).map(String::trim).distinct().forEach(parts::add);
|
||||
}
|
||||
return String.join(SEPARATOR, parts);
|
||||
}
|
||||
}
|
||||
+1047
File diff suppressed because it is too large
Load Diff
+291
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* 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.support;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillSaveDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillSaveEntryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiArBillVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springblade.thirdparty.bfi.util.BfiUtil;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
|
||||
import org.springblade.transport.service.KingdeeSyncContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 金蝶财务应收单(A302-2)同步支撑组件。
|
||||
* <p>应收(receivable)正式结算单「同步金蝶」推送财务应收单(A302-2),推送前经 A302-4 按防重键幂等预检,
|
||||
* 与应付链路(A102-2/A102-4,{@link KingdeeApBillSyncSupport})同一套逻辑;防重键组装、A302-4 查询、
|
||||
* A302-2 组装保存与应收分录解析收口在本组件,档案取数(合同/组织/币种/客商/货物类型/不含税回链等)
|
||||
* 复用应付支撑组件的公共方法。</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeArBillSyncSupport {
|
||||
|
||||
/** A302-4 防重键批量分批查询单次条数上限(与 A102-4 一致) */
|
||||
private static final int AR_RESULT_QUERY_BATCH = 50;
|
||||
/** A302-4 分页查询页大小 */
|
||||
private static final int AR_RESULT_PAGE_SIZE = 200;
|
||||
|
||||
private final IBfiService bfiService;
|
||||
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
// ==================== 财务应收单防重键(awrp_src_code) ====================
|
||||
|
||||
/**
|
||||
* 财务应收单防重键(awrp_src_code):正式结算单号 + 结算明细单据号下划线拼接(与 A302-2 保存传参一致),
|
||||
* 规则与财务应付单(A102-2)同源,一张应收结算单只生成一张金蝶财务应收单
|
||||
*
|
||||
* @param settlement 正式结算单
|
||||
* @param settlementDetails 该结算单下的结算明细
|
||||
* @return 防重键
|
||||
*/
|
||||
public String arBillSrcCode(FormalSettlement settlement, List<FormalSettlementDetail> settlementDetails) {
|
||||
return ExternalSrcCodeSupport.buildSrcCode(settlement.getFormalSettlementNo(),
|
||||
kingdeeApBillSyncSupport.settlementDocumentNos(settlementDetails));
|
||||
}
|
||||
|
||||
// ==================== A302-4 财务应收单查询 ====================
|
||||
|
||||
/**
|
||||
* A302-4 批量查询已生成财务应收单:来源单据号批量 + 外部系统编号(防重键),自动翻页,
|
||||
* 用于推送前幂等预检与定时任务应收勾稽纠偏(awrp_src_code 支持多值,整批一次查询消除逐单远程查询 N+1)
|
||||
*
|
||||
* @param awrpSrcCodes 来源单据号(正式结算单号_结算明细单据号拼接)批次
|
||||
* @return 已生成财务应收单的 来源单据号->单据编号 映射
|
||||
*/
|
||||
public Map<String, String> queryArBillExistNos(List<String> awrpSrcCodes) {
|
||||
Map<String, String> existBillNos = new LinkedHashMap<>();
|
||||
kingdeeApBillSyncSupport.batchForeach(awrpSrcCodes, AR_RESULT_QUERY_BATCH, batch -> {
|
||||
BfiArBillQueryDTO param = new BfiArBillQueryDTO();
|
||||
param.setAwrpSrcCode(batch);
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
for (BfiArBillVO arBill : BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(AR_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryArBill(param);
|
||||
})) {
|
||||
if (Func.isNotEmpty(arBill.getAwrpSrcCode())) {
|
||||
existBillNos.put(arBill.getAwrpSrcCode(), arBill.getBillno());
|
||||
}
|
||||
}
|
||||
});
|
||||
return existBillNos;
|
||||
}
|
||||
|
||||
/**
|
||||
* A302-4 单防重键查询已生成财务应收单的星瀚内码(A902 附件上传定位用):
|
||||
* 附件补传时本地 kingdee_bill_id 缺失(存量数据/幂等跳过路径未带回 id)的场景回填
|
||||
*
|
||||
* @param arBillSrcCode 财务应收单防重键
|
||||
* @return 星瀚应收单内码,未生成返回 null
|
||||
*/
|
||||
public String queryArBillExistId(String arBillSrcCode) {
|
||||
if (Func.isBlank(arBillSrcCode)) {
|
||||
return null;
|
||||
}
|
||||
BfiArBillQueryDTO param = new BfiArBillQueryDTO();
|
||||
param.setAwrpSrcCode(List.of(arBillSrcCode));
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
for (BfiArBillVO arBill : BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(AR_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryArBill(param);
|
||||
})) {
|
||||
if (arBillSrcCode.equals(arBill.getAwrpSrcCode()) && arBill.getId() != null) {
|
||||
return String.valueOf(arBill.getId());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== A302-2 财务应收单保存 ====================
|
||||
|
||||
/**
|
||||
* 推送金蝶财务应收单(A302-2 财务应收单保存,由应收正式结算单「同步金蝶」触发)。
|
||||
* 推送前先经 A302-4 按防重键(awrp_src_sys + awrp_src_code)查询:金蝶已存在财务应收单时
|
||||
* 幂等跳过保存并返回已有单据编号(说明上次同步已保存成功、在后续环节中断,应续推而非拦截报错)。
|
||||
* 明细分录与应付单(A102-2)同源同口径:页面结算明细按 货物→费用大类→税率 拆分行 + 手动添加的结算合计费用项(每行一条);
|
||||
* 收款计划 planentity 暂不传,由金蝶按明细自动汇总生成
|
||||
*
|
||||
* @param arBillSrcCode 财务应收单防重键(正式结算单号_结算明细单据号拼接)
|
||||
* @param settlement 正式结算单(合同/付款方取数来源)
|
||||
* @param entryGroups 结算明细+结算合计费用项解析出的应收单分录
|
||||
* @param subject 业务单据号(正式结算单号,日志与异常提示用)
|
||||
* @param context 金蝶同步上下文(缓存载体,预检命中与应付共用防重键缓存)
|
||||
* @return 财务应收单推送结果(单据编号 + 是否金蝶已存在幂等跳过)
|
||||
*/
|
||||
public KingdeeApBillSyncSupport.BfiApBillPushOutcome pushArBill(String arBillSrcCode, FormalSettlement settlement,
|
||||
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups,
|
||||
String subject, KingdeeSyncContext context) {
|
||||
// A302-4 幂等预检:按防重键查询金蝶是否已生成财务应收单,已存在则幂等跳过保存,直接返回已有单据编号
|
||||
String existBillNo = context.apBillNoCache().computeIfAbsent(arBillSrcCode, code ->
|
||||
queryArBillExistNos(List.of(code)).getOrDefault(code, ""));
|
||||
if (Func.isNotEmpty(existBillNo)) {
|
||||
log.info("正式结算单[{}]金蝶财务应收单已生成(单据编号[{}]),幂等跳过 A302-2 保存", subject, existBillNo);
|
||||
return new KingdeeApBillSyncSupport.BfiApBillPushOutcome(existBillNo, null, true);
|
||||
}
|
||||
ContractManage contract = kingdeeApBillSyncSupport.requireContract(settlement.getContractId(), context);
|
||||
// 单据类型按映射字典转换:默认服务销售应收,金蝶确认其他类型后修改 BFI_AR_BILL_BILLTYPE_NAME 即可
|
||||
String billtypeNumber = KingdeeBfiConstant.billtypeBy(KingdeeBfiConstant.AR_BILL_BILLTYPE_MAP, KingdeeBfiConstant.BFI_AR_BILL_BILLTYPE_NAME);
|
||||
if (Func.isBlank(billtypeNumber)) {
|
||||
throw new ServiceException("推送金蝶财务应收单失败:单据类型[" + KingdeeBfiConstant.BFI_AR_BILL_BILLTYPE_NAME + "]未在映射字典中维护");
|
||||
}
|
||||
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract);
|
||||
// 币种取合同结算币种
|
||||
String currency = kingdeeApBillSyncSupport.bfiCurrency(contract);
|
||||
// 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式),以同步当日作为应收单据日期
|
||||
String billDateText = kingdeeApBillSyncSupport.bfiDateTimeText(LocalDate.now());
|
||||
// 往来户.编码:应收结算单付款方(客户)关联客商档案客商编码
|
||||
String payerName = settlement.getPayerName();
|
||||
if (Func.isBlank(payerName)) {
|
||||
throw new ServiceException("推送金蝶财务应收单失败,结算单付款方不能为空,请检查正式结算单[" + Func.toStr(subject) + "]付款方信息");
|
||||
}
|
||||
String customerCode = context.customerCodeCache().computeIfAbsent(payerName.trim(), kingdeeApBillSyncSupport::queryCustomerCode);
|
||||
if (Func.isBlank(customerCode)) {
|
||||
throw new ServiceException("推送金蝶财务应收单失败,结算单付款方[" + payerName + "]未在客商档案中维护客商编码,请补充后重试");
|
||||
}
|
||||
BfiArBillSaveDTO param = new BfiArBillSaveDTO();
|
||||
// 【必填】单据类型.编码
|
||||
param.setBilltypeNumber(billtypeNumber);
|
||||
// 【必填】结算组织.编码
|
||||
param.setOrgNumber(orgNumber);
|
||||
// 收款组织.编码:同结算组织
|
||||
param.setRecorgNumber(orgNumber);
|
||||
// 【必填】单据日期
|
||||
param.setBizdate(billDateText);
|
||||
// 【必填】往来类型:应收固定客户
|
||||
param.setAsstacttype(KingdeeBfiConstant.BFI_CUSTOMER_ASSTACT_TYPE);
|
||||
// 【必填】往来户.编码:结算单付款方(客户)
|
||||
param.setAsstactNumber(customerCode);
|
||||
// 【必填】付款方式(CASH:现销, CREDIT:赊销),现销由金蝶自动推收款
|
||||
param.setPaymode(KingdeeBfiConstant.BFI_AR_BILL_PAY_MODE_CASH);
|
||||
// 【必填】结算币别.货币代码
|
||||
param.setCurrencyNumber(currency);
|
||||
// 【必填】款项性质.编码
|
||||
param.setPaypropertyNumber(KingdeeBfiConstant.BFI_AR_BILL_PAY_PROPERTY);
|
||||
// TODO 待确认:结算方式编码(settlementtype_number)与税率编码(taxrateid_number)、到期日(duedate)需金蝶侧确认后补充,当前不传
|
||||
// 录入含税价(非必传)
|
||||
param.setIsincludetax(true);
|
||||
// 录入总价,为 true 后可不录入单价(非必传)
|
||||
param.setIspricetotal(true);
|
||||
// 【必填】外部系统编号(候选键)
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
// 【必填】外部系统来源单据号(候选键):正式结算单号_结算明细单据号1_单据号2...(复用预检已组装的防重键)
|
||||
param.setAwrpSrcCode(arBillSrcCode);
|
||||
// 来源单据(非必传,有上游合同时传 awrp_contract_info),当前与应付单保持一致不传
|
||||
// 【必填】外部单据类型
|
||||
param.setAwrpSrcType(KingdeeBfiConstant.BFI_AR_BILL_SRC_TYPE);
|
||||
|
||||
// 汇率日期:正式结算单页面维护的汇率日期(非必传,未维护不送)
|
||||
if (settlement.getExchangeRateDate() != null) {
|
||||
param.setExratedate(kingdeeApBillSyncSupport.bfiDateTimeText(settlement.getExchangeRateDate()));
|
||||
}
|
||||
// 汇率:正式结算单页面维护的结算汇率(非必传,未维护不送)
|
||||
if (settlement.getExchangeRate() != null) {
|
||||
param.setExchangerate(settlement.getExchangeRate());
|
||||
}
|
||||
|
||||
// 明细分录:与应付单同源同口径——结算明细按 货物→费用大类→税率 拆分行 + 手动添加的结算合计费用项每行一条,无吨位时数量兜底 1
|
||||
List<BfiArBillSaveEntryDTO> entry = new ArrayList<>();
|
||||
for (KingdeeApBillSyncSupport.BfiEntryGroup entryGroup : entryGroups) {
|
||||
BfiArBillSaveEntryDTO row = new BfiArBillSaveEntryDTO();
|
||||
|
||||
// 【必填】物料编码.编码:按装卸大类/项目业务模式/运单运输方式映射(thirdparty.bfi.material.* 配置)
|
||||
row.setEMaterialNumber(entryGroup.materialNumber());
|
||||
// 数量:运输类取运输量,装卸类/其它默认 1
|
||||
row.setEQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE);
|
||||
// 运输货物(金蝶接口新字段):结算明细货物名称,结算合计费用项行不传
|
||||
row.setAwrpTransport(entryGroup.transportGoods());
|
||||
// 车号(金蝶接口新字段):结算明细车号,结算合计费用项行不传
|
||||
row.setAwrpLpn(entryGroup.vehicleNo());
|
||||
|
||||
// 明细.单价:结算金额/数量反算(单据头 ispricetotal=true 录入总价,单价仅作参考)
|
||||
row.setEUnitprice(entryGroup.price());
|
||||
// 应收金额(含税):结算明细行结算金额(含税)/结算合计费用项行结算金额
|
||||
row.setERecamount(entryGroup.amount());
|
||||
// 金额(不含税):结算明细行结算金额(不含税),结算合计费用项行无不含税金额不传
|
||||
row.setEAmount(entryGroup.amountNoTax());
|
||||
// 【必填】项目.项目编码(联调期临时传金蝶测试档案值)
|
||||
row.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST);
|
||||
// 【必填】外部系统来源分录标识(候选键):结算明细行取单据号,结算合计费用项行取结算单号-费用类型-费用项拼接
|
||||
row.setAwrpEntryCode(entryGroup.entryCode());
|
||||
// 分录备注(非必传)
|
||||
// row.setERemark(subject);
|
||||
|
||||
entry.add(row);
|
||||
}
|
||||
param.setEntry(entry);
|
||||
|
||||
// 收款计划 planentity 不传:由金蝶按明细自动汇总生成(启用方案待业务确认后迭代)
|
||||
|
||||
log.info("推送金蝶财务应收单:正式结算单[{}],防重键[{}],明细分录{}条", subject, arBillSrcCode, entry.size());
|
||||
BfiBillSaveResultVO saveResult = bfiService.saveArBill(param);
|
||||
// 应收单id不落库,后续如需可经 A302-4 按防重键(awrp_src_sys + awrp_src_code)查询
|
||||
kingdeeApBillSyncSupport.requireBfiSaveSuccess(saveResult, "推送金蝶财务应收单");
|
||||
String billNo = saveResult.getResult().get(0).getNumber();
|
||||
log.info("金蝶财务应收单保存成功:正式结算单[{}],单据编号[{}],明细分录{}条", subject, billNo, entry.size());
|
||||
// 星瀚应收单内码随结果返回(A902 附件上传定位用)
|
||||
BfiBillSaveResultItemVO savedItem = saveResult.getResult().get(0);
|
||||
return new KingdeeApBillSyncSupport.BfiApBillPushOutcome(savedItem.getNumber(),
|
||||
savedItem.getId() == null ? null : String.valueOf(savedItem.getId()), false);
|
||||
}
|
||||
|
||||
// ==================== 结算明细分录解析 ====================
|
||||
|
||||
/**
|
||||
* 解析应收正式结算明细为应收单分录:与应付单(A102-2)分录解析同源同口径——
|
||||
* 页面结算明细按 货物→费用大类→税率 拆分行 + 手工添加的结算合计费用项每行一条(系统自动汇总行是明细金额的再汇总不送),
|
||||
* 物料/计量单位/数量/单价/金额/分录标识取数规则一致,
|
||||
* 直接复用应付支撑组件解析(应收分录不含税金额取结算明细结算金额(不含税))
|
||||
*
|
||||
* @param settlement 正式结算单
|
||||
* @param settlementDetails 该结算单下的结算明细
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 应收单分录列表(保持行序)
|
||||
*/
|
||||
public List<KingdeeApBillSyncSupport.BfiEntryGroup> resolveArEntryGroups(FormalSettlement settlement,
|
||||
List<FormalSettlementDetail> settlementDetails,
|
||||
KingdeeSyncContext context) {
|
||||
return kingdeeApBillSyncSupport.resolveEntryGroups(settlement, settlementDetails, context);
|
||||
}
|
||||
}
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
/**
|
||||
* 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.support;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import feign.Response;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiAttachmentUploadVO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springblade.transport.constant.KingdeeAttachmentConstant;
|
||||
import org.springblade.transport.mapper.KingdeeAttachmentSyncMapper;
|
||||
import org.springblade.transport.pojo.entity.KingdeeAttachmentSync;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 金蝶附件同步支撑组件(A902 附件上传到指定单据)。
|
||||
* <p>设计不变式:
|
||||
* 1. 附件上传晚于主单推送成功、拿到并落库 billPkId 之后(由各模块接入点保证,本组件在事务提交后调用);
|
||||
* 2. 附件失败永不回滚、不污染主单同步状态(逐附件独立 try/catch);
|
||||
* 3. 同 (biz_type, biz_id, attachment_key) 最多一条成功记录(唯一键 + 成功集合幂等跳过);
|
||||
* 4. 不自动重试,重推主单时自动补传;5. 单文件软限制 20MB,超限/下载失败记原因跳过。</p>
|
||||
* <p>附件来源两类:用户附件(attachmentsJson 数组项,按 url 下载,幂等键=url)与
|
||||
* 票池镜像发票文件(attachmentsJson 对象 {source,path,name},经 A801-2 下载回传,幂等键=path,
|
||||
* path 变化(红冲重开)视为新文件自动触发重传)。</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeAttachmentSyncSupport {
|
||||
|
||||
/** 用户附件下载连接/请求超时 */
|
||||
private static final Duration DOWNLOAD_TIMEOUT = Duration.ofSeconds(60);
|
||||
|
||||
private final IBfiService bfiService;
|
||||
private final KingdeeAttachmentSyncMapper attachmentSyncMapper;
|
||||
|
||||
/** 附件下载客户端(无状态,进程内复用) */
|
||||
private final HttpClient downloadClient = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
|
||||
/**
|
||||
* 待同步附件项
|
||||
*
|
||||
* @param name 附件名称(含扩展名,展示/金蝶侧文件名)
|
||||
* @param key 幂等键:用户附件=附件 url;发票文件=A801-2 的 path
|
||||
* @param size 附件大小(字节,可空:空时下载后按实际字节校验)
|
||||
* @param kingdeePathSource true 表示 key 为星瀚侧文件 path(经 A801-2 下载);false 表示 http(s) 附件 url
|
||||
*/
|
||||
public record KingdeeAttachmentItem(String name, String key, Long size, boolean kingdeePathSource) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 单据附件同步结果(各模块用于追加操作流水与前端提示)
|
||||
*
|
||||
* @param total 参与同步的附件数(不含已成功跳过)
|
||||
* @param success 本次上传成功数
|
||||
* @param failed 失败数
|
||||
* @param skipCount 已成功幂等跳过数
|
||||
* @param failReasons 失败原因明细(附件名:原因)
|
||||
*/
|
||||
public record AttachmentSyncSummary(int total, int success, int failed, int skipCount, List<String> failReasons) {
|
||||
}
|
||||
|
||||
// ==================== 附件同步主流程 ====================
|
||||
|
||||
/**
|
||||
* 同步单据附件至金蝶对应单据附件面板(A902,逐附件独立 try/catch:成功落成功记录、失败落失败记录)。
|
||||
* billPkId 为空(星瀚单据内码未落库)时整单跳过并记日志,由后续重推补传。
|
||||
*
|
||||
* @param bizType 业务单据类型(KingdeeAttachmentConstant.BIZ_*)
|
||||
* @param bizId 业务单据id
|
||||
* @param billPkId 星瀚单据内码
|
||||
* @param items 待同步附件列表
|
||||
* @return 同步结果汇总
|
||||
*/
|
||||
public AttachmentSyncSummary syncAttachments(String bizType, Long bizId, String billPkId, List<KingdeeAttachmentItem> items) {
|
||||
if (Func.isEmpty(items)) {
|
||||
return new AttachmentSyncSummary(0, 0, 0, 0, List.of());
|
||||
}
|
||||
if (Func.isBlank(billPkId)) {
|
||||
log.warn("金蝶附件同步跳过:业务单据[{}:{}]星瀚单据内码为空,待重推补传", bizType, bizId);
|
||||
return new AttachmentSyncSummary(items.size(), 0, 0, 0, List.of());
|
||||
}
|
||||
// 载入已成功集合:命中即幂等跳过(重推主单时 A902 零调用)
|
||||
Set<String> successKeys = successKeys(bizType, bizId);
|
||||
// 载入既有同步记录(含失败记录,重试成功时覆盖为成功)
|
||||
Map<String, KingdeeAttachmentSync> existingByKey = existingByKey(bizType, bizId);
|
||||
String entityNumber = KingdeeAttachmentConstant.entityNumber(bizType);
|
||||
log.info("金蝶附件同步开始:业务单据[{}:{}],星瀚内码={},entityNumber={},待同步附件 {} 个,历史已成功 {} 个",
|
||||
bizType, bizId, billPkId, entityNumber, items.size(), successKeys.size());
|
||||
int success = 0;
|
||||
int skipCount = 0;
|
||||
List<String> failReasons = new ArrayList<>();
|
||||
List<KingdeeAttachmentSync> pendingRecords = new ArrayList<>();
|
||||
for (KingdeeAttachmentItem item : items) {
|
||||
if (Func.isBlank(item.key())) {
|
||||
continue;
|
||||
}
|
||||
if (successKeys.contains(item.key())) {
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
// 声明大小超限直接拦截,不发起下载
|
||||
if (item.size() != null && item.size() > KingdeeAttachmentConstant.MAX_FILE_SIZE_BYTES) {
|
||||
throw new IllegalArgumentException("附件超过20MB大小限制("
|
||||
+ formatSize(item.size()) + "),请压缩后重试");
|
||||
}
|
||||
byte[] content = fetchContent(item);
|
||||
if (content.length > KingdeeAttachmentConstant.MAX_FILE_SIZE_BYTES) {
|
||||
throw new IllegalArgumentException("附件超过20MB大小限制("
|
||||
+ formatSize((long) content.length) + "),请压缩后重试");
|
||||
}
|
||||
String fileName = Func.isBlank(item.name()) ? "attachment" : item.name();
|
||||
BfiAttachmentUploadVO uploadVO = bfiService.uploadAttachment(entityNumber, billPkId,
|
||||
KingdeeAttachmentConstant.CONTROL_KEY, fileName, content);
|
||||
log.info("金蝶附件上传成功:业务单据[{}:{}],附件[{}],kingdeePath={}", bizType, bizId, item.name(), uploadVO.getPath());
|
||||
pendingRecords.add(buildRecord(bizType, bizId, item, KingdeeAttachmentConstant.SYNC_STATUS_SUCCESS,
|
||||
null, uploadVO.getPath(), existingByKey.get(item.key())));
|
||||
success++;
|
||||
} catch (Exception exception) {
|
||||
String reason = exception.getMessage() == null ? exception.getClass().getSimpleName() : exception.getMessage();
|
||||
log.warn("金蝶附件同步失败:业务单据[{}:{}],附件[{}]", bizType, bizId, item.name(), exception);
|
||||
failReasons.add(Func.toStr(item.name()) + ":" + reason);
|
||||
pendingRecords.add(buildRecord(bizType, bizId, item, KingdeeAttachmentConstant.SYNC_STATUS_FAILED,
|
||||
truncateReason(reason), null, existingByKey.get(item.key())));
|
||||
}
|
||||
}
|
||||
for (KingdeeAttachmentSync record : pendingRecords) {
|
||||
if (record.getId() == null) {
|
||||
attachmentSyncMapper.insert(record);
|
||||
} else {
|
||||
// updateById 默认忽略 null 字段,失败转成功时 fail_reason 须显式置空,避免残留历史失败原因
|
||||
attachmentSyncMapper.update(record, Wrappers.<KingdeeAttachmentSync>lambdaUpdate()
|
||||
.eq(KingdeeAttachmentSync::getId, record.getId())
|
||||
.set(KingdeeAttachmentSync::getFailReason, record.getFailReason()));
|
||||
}
|
||||
}
|
||||
int total = success + failReasons.size();
|
||||
log.info("金蝶附件同步完成:业务单据[{}:{}],本次待传 {},成功 {},失败 {},历史已传跳过 {}",
|
||||
bizType, bizId, total, success, failReasons.size(), skipCount);
|
||||
return new AttachmentSyncSummary(total, success, failReasons.size(), skipCount, failReasons);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步用户附件(单据 attachmentsJson):解析后逐个上传
|
||||
*
|
||||
* @param bizType 业务单据类型
|
||||
* @param bizId 业务单据id
|
||||
* @param billPkId 星瀚单据内码
|
||||
* @param attachmentsJson 单据附件JSON(数组,容忍单对象)
|
||||
* @return 同步结果汇总
|
||||
*/
|
||||
public AttachmentSyncSummary syncUserAttachments(String bizType, Long bizId, String billPkId, String attachmentsJson) {
|
||||
return syncAttachments(bizType, bizId, billPkId, parseUserAttachments(attachmentsJson));
|
||||
}
|
||||
|
||||
// ==================== 附件解析 ====================
|
||||
|
||||
/**
|
||||
* 解析单据用户附件(attachmentsJson):取 url/name/size,无 url 的项忽略。
|
||||
* 前端附件行字段容忍取值:名称 originalName/name/fileName,地址 url/link/fileUrl/downloadUrl/domain/src
|
||||
*
|
||||
* @param attachmentsJson 附件JSON
|
||||
* @return 附件项列表
|
||||
*/
|
||||
public List<KingdeeAttachmentItem> parseUserAttachments(String attachmentsJson) {
|
||||
Object parsed = parseJson(attachmentsJson);
|
||||
if (parsed == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<?> rows = parsed instanceof List<?> list ? list : List.of(parsed);
|
||||
List<KingdeeAttachmentItem> items = new ArrayList<>();
|
||||
for (Object row : rows) {
|
||||
if (!(row instanceof Map<?, ?> map)) {
|
||||
continue;
|
||||
}
|
||||
String url = firstText(map, "url", "link", "fileUrl", "downloadUrl", "domain", "src");
|
||||
if (Func.isBlank(url)) {
|
||||
continue;
|
||||
}
|
||||
String name = firstText(map, "originalName", "name", "fileName");
|
||||
Long size = firstLong(map, "size");
|
||||
items.add(new KingdeeAttachmentItem(name, url, size, false));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析票池镜像发票文件(attachmentsJson 单对象 {source,path,name}):幂等键=path(经 A801-2 下载),
|
||||
* path 变化(红冲重开)自动视为新文件触发重传
|
||||
*
|
||||
* @param attachmentsJson 镜像附件JSON
|
||||
* @return 附件项列表(无发票文件时为空)
|
||||
*/
|
||||
public List<KingdeeAttachmentItem> parseInvoicePoolFile(String attachmentsJson) {
|
||||
Object parsed = parseJson(attachmentsJson);
|
||||
if (parsed == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<?> rows = parsed instanceof List<?> list ? list : List.of(parsed);
|
||||
List<KingdeeAttachmentItem> items = new ArrayList<>();
|
||||
for (Object row : rows) {
|
||||
if (!(row instanceof Map<?, ?> map)) {
|
||||
continue;
|
||||
}
|
||||
String path = firstText(map, "path");
|
||||
if (Func.isBlank(path)) {
|
||||
continue;
|
||||
}
|
||||
String name = firstText(map, "name");
|
||||
items.add(new KingdeeAttachmentItem(Func.isBlank(name) ? "发票文件" : name, path, null, true));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// ==================== 列表聚合态 ====================
|
||||
|
||||
/**
|
||||
* 单据附件数量(列表聚合态判定用):解析失败按 0 处理
|
||||
*
|
||||
* @param attachmentsJson 单据附件JSON
|
||||
* @return 附件数量
|
||||
*/
|
||||
public int countAttachments(String attachmentsJson) {
|
||||
return parseUserAttachments(attachmentsJson).size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 各业务单据已成功上传附件数(一次 GROUP BY 查询,避免 N+1)
|
||||
*
|
||||
* @param bizType 业务单据类型
|
||||
* @param bizIds 业务单据id集合
|
||||
* @return 业务单据id -> 已成功附件数
|
||||
*/
|
||||
public Map<Long, Integer> successCountByBizId(String bizType, Collection<Long> bizIds) {
|
||||
if (Func.isEmpty(bizIds)) {
|
||||
return Map.of();
|
||||
}
|
||||
QueryWrapper<KingdeeAttachmentSync> wrapper = new QueryWrapper<KingdeeAttachmentSync>()
|
||||
.select("biz_id", "count(*) AS success_count")
|
||||
.eq("biz_type", bizType)
|
||||
.eq("sync_status", KingdeeAttachmentConstant.SYNC_STATUS_SUCCESS)
|
||||
.in("biz_id", bizIds)
|
||||
.groupBy("biz_id");
|
||||
Map<Long, Integer> result = new HashMap<>();
|
||||
for (Map<String, Object> row : attachmentSyncMapper.selectMaps(wrapper)) {
|
||||
Object bizId = row.get("biz_id");
|
||||
Object count = row.get("success_count");
|
||||
if (bizId != null && count != null) {
|
||||
result.put(Long.valueOf(String.valueOf(bizId)), Integer.parseInt(String.valueOf(count)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚合态判定:无附件 / 未上传 / 部分未传 / 已上传(运行时聚合,非存储字段)
|
||||
*
|
||||
* @param attachmentCount 单据附件数量
|
||||
* @param successCount 已成功上传数量
|
||||
* @return 聚合态(KingdeeAttachmentConstant.AGGREGATE_*)
|
||||
*/
|
||||
public String aggregateStatus(int attachmentCount, int successCount) {
|
||||
if (attachmentCount <= 0) {
|
||||
return KingdeeAttachmentConstant.AGGREGATE_NONE;
|
||||
}
|
||||
if (successCount <= 0) {
|
||||
return KingdeeAttachmentConstant.AGGREGATE_PENDING;
|
||||
}
|
||||
return successCount >= attachmentCount
|
||||
? KingdeeAttachmentConstant.AGGREGATE_DONE : KingdeeAttachmentConstant.AGGREGATE_PARTIAL;
|
||||
}
|
||||
|
||||
// ==================== 内部实现 ====================
|
||||
|
||||
/** 已成功附件幂等键集合 */
|
||||
private Set<String> successKeys(String bizType, Long bizId) {
|
||||
return attachmentSyncMapper.selectList(Wrappers.<KingdeeAttachmentSync>lambdaQuery()
|
||||
.select(KingdeeAttachmentSync::getAttachmentKey)
|
||||
.eq(KingdeeAttachmentSync::getBizType, bizType)
|
||||
.eq(KingdeeAttachmentSync::getBizId, bizId)
|
||||
.eq(KingdeeAttachmentSync::getSyncStatus, KingdeeAttachmentConstant.SYNC_STATUS_SUCCESS))
|
||||
.stream().map(KingdeeAttachmentSync::getAttachmentKey).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/** 既有同步记录(含失败),key -> 记录(重试成功时覆盖为成功) */
|
||||
private Map<String, KingdeeAttachmentSync> existingByKey(String bizType, Long bizId) {
|
||||
Map<String, KingdeeAttachmentSync> result = new LinkedHashMap<>();
|
||||
for (KingdeeAttachmentSync record : attachmentSyncMapper.selectList(Wrappers.<KingdeeAttachmentSync>lambdaQuery()
|
||||
.eq(KingdeeAttachmentSync::getBizType, bizType)
|
||||
.eq(KingdeeAttachmentSync::getBizId, bizId))) {
|
||||
result.putIfAbsent(record.getAttachmentKey(), record);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装同步记录:有既有记录时复用主键覆盖更新(失败转成功/成功后清理失败原因),否则新增
|
||||
*/
|
||||
private KingdeeAttachmentSync buildRecord(String bizType, Long bizId, KingdeeAttachmentItem item, int syncStatus,
|
||||
String failReason, String kingdeePath, KingdeeAttachmentSync existing) {
|
||||
KingdeeAttachmentSync record = existing == null ? new KingdeeAttachmentSync() : existing;
|
||||
record.setBizType(bizType);
|
||||
record.setBizId(bizId);
|
||||
record.setAttachmentKey(item.key());
|
||||
record.setAttachmentName(truncate(item.name(), 250));
|
||||
record.setKingdeePath(kingdeePath);
|
||||
record.setSyncStatus(syncStatus);
|
||||
record.setFailReason(failReason);
|
||||
record.setSyncTime(LocalDateTime.now());
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取附件内容:星瀚侧文件经 A801-2 下载(流式读取,防大文件全量进内存失控);
|
||||
* 用户附件按 http(s) url 下载(流式读取)。调用方负责按 20MB 上限校验
|
||||
*/
|
||||
private byte[] fetchContent(KingdeeAttachmentItem item) throws Exception {
|
||||
if (item.kingdeePathSource()) {
|
||||
Response response = bfiService.downloadInvoice(item.key());
|
||||
try (InputStream inputStream = response.body().asInputStream()) {
|
||||
return readCapped(inputStream);
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(item.key()))
|
||||
.timeout(DOWNLOAD_TIMEOUT)
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<InputStream> response = downloadClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new IllegalStateException("附件下载失败,HTTP状态码 " + response.statusCode());
|
||||
}
|
||||
try (InputStream inputStream = response.body()) {
|
||||
return readCapped(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/** 流式读取并封顶(超出软限制即中断读取,避免大文件占满内存) */
|
||||
private byte[] readCapped(InputStream inputStream) throws IOException {
|
||||
int bufferSize = 64 * 1024;
|
||||
long limit = KingdeeAttachmentConstant.MAX_FILE_SIZE_BYTES;
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream(bufferSize);
|
||||
byte[] chunk = new byte[bufferSize];
|
||||
long total = 0;
|
||||
int read;
|
||||
while ((read = inputStream.read(chunk)) != -1) {
|
||||
if (total + read > limit) {
|
||||
throw new IllegalArgumentException("附件超过20MB大小限制,请压缩后重试");
|
||||
}
|
||||
buffer.write(chunk, 0, read);
|
||||
total += read;
|
||||
}
|
||||
return buffer.toByteArray();
|
||||
}
|
||||
|
||||
private Object parseJson(String json) {
|
||||
if (Func.isBlank(json)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JsonUtil.parse(json.trim(), Object.class);
|
||||
} catch (Exception exception) {
|
||||
log.warn("附件JSON解析失败,按无附件处理:{}", exception.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 附件行字段容忍取值(多个候选键取第一个非空值) */
|
||||
private String firstText(Map<?, ?> map, String... keys) {
|
||||
for (String key : keys) {
|
||||
Object value = map.get(key);
|
||||
if (value != null && Func.isNotBlank(String.valueOf(value))) {
|
||||
return String.valueOf(value).trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long firstLong(Map<?, ?> map, String key) {
|
||||
Object value = map.get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value != null) {
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(value));
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String truncate(String text, int maxLength) {
|
||||
if (text == null || text.length() <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
return text.substring(0, maxLength);
|
||||
}
|
||||
|
||||
private String truncateReason(String reason) {
|
||||
return truncate(reason, 490);
|
||||
}
|
||||
|
||||
private String formatSize(Long bytes) {
|
||||
if (bytes == null) {
|
||||
return "未知";
|
||||
}
|
||||
return String.format("%.1fMB", bytes / 1024.0 / 1024.0);
|
||||
}
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* 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.support;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.thirdparty.bfi.constant.BfiConstant;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceAssignItemDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceAssignRefDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveEntryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springblade.thirdparty.bfi.util.BfiUtil;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.InvoiceReceipt;
|
||||
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
|
||||
import org.springblade.transport.service.KingdeeSyncContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 金蝶收票单同步支撑组件(图05 环节②③:A303-2 收票单批量保存 + A302-6 收票指定应付 + A801-3 收票单查询)。
|
||||
* <p>收票登记「推送金蝶」由 {@link org.springblade.transport.service.IInvoiceReceiptService} 编排前置校验
|
||||
* (审批通过、已绑定镜像、一票一应付、A102-4 应付单已生成预检、A801-3 幂等预检)后调用本组件;
|
||||
* 报文组装取数:主表组织/币种/往来户复用应付支撑组件的合同派生方法,票面字段取自票池镜像
|
||||
* (invoice_type_code 等,A801-1 落库),明细分录与财务应付单 A102-2 同源同口径
|
||||
* (结算明细行+结算合计费用项各一条,{@link KingdeeApBillSyncSupport.BfiEntryGroup},分录标识同键)。
|
||||
* 防重键:收票单 awrp_src_code=收票登记id,awrp_entry_code=结算明细行单据号/结算合计费用项拼接
|
||||
* (与应付单分录同键,便于金蝶侧逐行勾稽);A302-6 按「收票单防重键 ↔ 财务应付单防重键」一票一应付指定。
|
||||
* 财务应付单侧(A102-2/A102-4)取数与校验复用 {@link KingdeeApBillSyncSupport}。</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeReceivedInvoiceSyncSupport {
|
||||
|
||||
/** A801-3 分页查询页大小 */
|
||||
private static final int RECEIVED_QUERY_PAGE_SIZE = 200;
|
||||
/** A801-3/A303-2 统一时间格式 */
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
/** A801-3 发票状态:作废(2)/红冲(3)/部分红冲(7)/全部红冲(8) 视为金蝶侧异常,回收推送状态 */
|
||||
private static final List<String> ABNORMAL_INVOICE_STATUS = List.of("2", "3", "7", "8");
|
||||
|
||||
private final IBfiService bfiService;
|
||||
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
// ==================== A801-3 收票单查询(幂等预检/定时回写共用) ====================
|
||||
|
||||
/**
|
||||
* A801-3 按购方名称+修改时间窗口分页拉取收票单,本地按外部系统编号+来源单据号(防重键)过滤。
|
||||
* 契约无来源单据号查询参数,防重键匹配在返回行上进行;查询失败中断本轮,由任务层记录异常
|
||||
*
|
||||
* @param buyerName 购方名称(接口必填,取镜像受票单位)
|
||||
* @param srcCodes 待匹配的收票单防重键(收票登记id,与 A303-2 的 awrp_src_code 同源)集合
|
||||
* @param windowStart 修改时间-开始(窗口不超一个月,调用方控制)
|
||||
* @param windowEnd 修改时间-结束
|
||||
* @return 防重键(收票登记id) -> 收票单行(同键多行取修改时间最新)
|
||||
*/
|
||||
public Map<String, BfiReceivedInvoiceVO> queryReceivedInvoiceBySrcCodes(String buyerName, Collection<String> srcCodes,
|
||||
LocalDateTime windowStart, LocalDateTime windowEnd) {
|
||||
Map<String, BfiReceivedInvoiceVO> matched = new LinkedHashMap<>();
|
||||
if (Func.isBlank(buyerName) || Func.isEmpty(srcCodes)) {
|
||||
return matched;
|
||||
}
|
||||
BfiReceivedInvoiceQueryDTO param = new BfiReceivedInvoiceQueryDTO();
|
||||
param.setBuyername(buyerName.trim());
|
||||
param.setModifytimeS(windowStart.format(TIME_FORMATTER));
|
||||
param.setModifytimeE(windowEnd.format(TIME_FORMATTER));
|
||||
List<BfiReceivedInvoiceVO> rows = BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(RECEIVED_QUERY_PAGE_SIZE);
|
||||
return bfiService.queryReceivedInvoice(param);
|
||||
});
|
||||
for (BfiReceivedInvoiceVO row : rows) {
|
||||
if (!bfiProperties.getSrcSys().equals(row.getAwrpSrcSys()) || Func.isBlank(row.getAwrpSrcCode())
|
||||
|| !srcCodes.contains(row.getAwrpSrcCode())) {
|
||||
continue;
|
||||
}
|
||||
BfiReceivedInvoiceVO exist = matched.get(row.getAwrpSrcCode());
|
||||
// 同防重键多行(删除重建等)取修改时间最新一行
|
||||
if (exist == null || compareModifyTime(row, exist) >= 0) {
|
||||
matched.put(row.getAwrpSrcCode(), row);
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶侧发票状态是否异常(作废/红冲/部分红冲/全部红冲)
|
||||
*
|
||||
* @param invoiceStatus A801-3 invoicestatus
|
||||
* @return true 表示须回收推送状态、提示人工
|
||||
*/
|
||||
public boolean isAbnormalInvoiceStatus(String invoiceStatus) {
|
||||
return Func.isNotBlank(invoiceStatus) && ABNORMAL_INVOICE_STATUS.contains(invoiceStatus.trim());
|
||||
}
|
||||
|
||||
private int compareModifyTime(BfiReceivedInvoiceVO left, BfiReceivedInvoiceVO right) {
|
||||
String leftTime = left.getModifytime() == null ? "" : left.getModifytime();
|
||||
String rightTime = right.getModifytime() == null ? "" : right.getModifytime();
|
||||
return leftTime.compareTo(rightTime);
|
||||
}
|
||||
|
||||
// ==================== A303-2 收票单批量保存 ====================
|
||||
|
||||
/**
|
||||
* 推送金蝶收票单(A303-2 批量保存,调用方已完成 A801-3 幂等预检)。
|
||||
* 主表:组织/币种/往来户编码按分摊结算单关联合同派生(与应付单同源),销方=开票单位、购方=受票单位;
|
||||
* 明细分录与财务应付单 A102-2 同源同口径:结算明细按 货物→费用大类→税率 拆分行+手动添加结算合计费用项每行一条,物料/数量/单价/金额取自
|
||||
* 与应付单分录同一解析结果({@link KingdeeApBillSyncSupport.BfiEntryGroup}),分录标识同键。
|
||||
* 联调口径沿用应付侧:先只送 DTO 标注【必填】字段与票面核心字段(发票号码),
|
||||
* 非必填的税号/地址/电话/开户行等 setter 注释保留,联调确认后放开
|
||||
*
|
||||
* @param receipt 收票登记(审批通过、已绑定镜像)
|
||||
* @param mirror 票池镜像(票面主表字段取数来源)
|
||||
* @param contract 分摊应付结算单关联合同(组织/币种/往来户派生)
|
||||
* @param entryGroups 分摊结算单解析出的分录(与应付单 A102-2 同一解析结果)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 推送结果(单据编号 + 星瀚收票单内码,A902 附件上传定位用)
|
||||
*/
|
||||
public ReceivedInvoicePushOutcome pushReceivedInvoice(InvoiceReceipt receipt, KingdeeInvoicePool mirror, ContractManage contract,
|
||||
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups, KingdeeSyncContext context) {
|
||||
if (Func.isBlank(mirror.getInvoiceTypeCode())) {
|
||||
throw new ServiceException("推送金蝶收票单失败:镜像缺少票易通发票类型编码,请先重新绑定票池发票");
|
||||
}
|
||||
BfiReceivedInvoiceSaveDTO param = new BfiReceivedInvoiceSaveDTO();
|
||||
// 【必填】外部系统编号(候选键)
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
// 【必填】外部系统来源单据号(候选键):发票号码(一票一登记天然唯一,重推同键幂等)
|
||||
param.setAwrpSrcCode(String.valueOf(receipt.getId()));
|
||||
// 发票号码(非必填但为收票单核心业务字段,必送)
|
||||
param.setInvoiceno(receipt.getInvoiceNo());
|
||||
// 【必填】收票日期:以推送当日为收票日期
|
||||
param.setReceivedate(LocalDate.now().atStartOfDay().format(TIME_FORMATTER));
|
||||
// 【必填】开票日期:镜像票面开票日期
|
||||
param.setIssuedate(kingdeeApBillSyncSupport.bfiDateTimeText(mirror.getInvoiceDate()));
|
||||
// 【必填】业务类型(PUR 采购,待金蝶确认)
|
||||
param.setBiztype(KingdeeBfiConstant.BFI_RECEIVED_BIZ_TYPE);
|
||||
// 【必填】发票类型.编码:票易通原始编码(26/27 等)
|
||||
param.setInvoicetypef7Number(mirror.getInvoiceTypeCode());
|
||||
// 【必填】付款方式:(与应付单一致,不允许金蝶自动推付款)
|
||||
param.setPaymenttype(BfiConstant.PAY_MODE_CASH);
|
||||
// 【必填】往来类型:进项收票固定供应商
|
||||
param.setAsstacttype(BfiConstant.ASSTACT_TYPE_SUPPLIER);
|
||||
// 【必填】结算组织.编码:合同所属组织(与应付单同源)
|
||||
param.setOrgNumber(kingdeeApBillSyncSupport.bfiOrgNumber(contract));
|
||||
// 【必填】结算币别:合同结算币种
|
||||
param.setCurrencyNumber(kingdeeApBillSyncSupport.bfiCurrency(contract));
|
||||
// 【必填】往来户.编码:合同乙方(开票供应商)客商编码
|
||||
param.setReceivablessuppNumber(kingdeeApBillSyncSupport.bfiAsstactNumber(contract, context));
|
||||
// 【必填】汇率表.编码
|
||||
param.setExratetableNumber(BfiConstant.EXRATE_TABLE_DEFAULT);
|
||||
// 【必填】销方名称:镜像开票单位
|
||||
param.setAsstactname(mirror.getIssuerName());
|
||||
// 【必填】购方名称:镜像受票单位
|
||||
param.setBuyername(mirror.getReceiverName());
|
||||
// 录入价税合计(非必传,与应付/应收侧口径一致传 true)
|
||||
param.setIsincludetax(true);
|
||||
// 销方税号(非必传,联调确认后放开)
|
||||
param.setSellertin(mirror.getSalerTaxNo());
|
||||
// 购方税号(非必传,联调确认后放开)
|
||||
param.setBuyertin(mirror.getBuyerTaxNo());
|
||||
// 销方地址/电话/开户行/账号(非必传,被金蝶强校验时从镜像 bank_name/bank_account/issuing_bank/phone 补齐)
|
||||
// param.setSelleraddr(null);
|
||||
param.setSellertel(mirror.getPhone());
|
||||
param.setSellerbank(mirror.getBankName());
|
||||
param.setSelleracct(mirror.getBankAccount());
|
||||
// 备注(非必传)
|
||||
// param.setRemark(receipt.getRemark());
|
||||
// 【必填】外部单据类型
|
||||
param.setAwrpSrcType(KingdeeBfiConstant.BFI_RECEIVED_INVOICE_SRC_TYPE);
|
||||
// 【必填】明细分录:与应付单 A102-2 同源同口径,结算明细行+结算合计费用项各一条(不聚合)
|
||||
param.setEntry(buildEntries(entryGroups));
|
||||
|
||||
BfiBillSaveResultVO saveResult = bfiService.saveReceivedInvoice(param);
|
||||
kingdeeApBillSyncSupport.requireBfiSaveSuccess(saveResult, "推送金蝶收票单");
|
||||
BfiBillSaveResultItemVO savedItem = saveResult.getResult().get(0);
|
||||
return new ReceivedInvoicePushOutcome(savedItem.getNumber(),
|
||||
savedItem.getId() == null ? null : String.valueOf(savedItem.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* A303-2 收票单推送结果
|
||||
*
|
||||
* @param billNo 金蝶收票单单据编号
|
||||
* @param billId 星瀚收票单内码(A902 附件上传定位用)
|
||||
*/
|
||||
public record ReceivedInvoicePushOutcome(String billNo, String billId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分录组装与应付单 A102-2 明细同源同口径:结算明细按 货物→费用大类→税率 拆分行 + 手动添加的结算合计费用项每行一条,
|
||||
* 物料/数量/单价/含税/不含税金额
|
||||
* 取自 {@link KingdeeApBillSyncSupport.BfiEntryGroup}(与应付单分录同一解析结果),分录标识同键,
|
||||
* 便于金蝶侧收票单明细与应付单明细逐行勾稽;数量无效时兜底 1;
|
||||
* 票面明细行(items_json)不再作为分录取数来源,税收分类/税率/税额随应付口径暂不送(联调强校验时再补)
|
||||
*
|
||||
* @param entryGroups 分摊结算单解析出的分录(与应付单 A102-2 同一解析结果)
|
||||
* @return A303-2 明细分录
|
||||
*/
|
||||
private List<BfiReceivedInvoiceSaveEntryDTO> buildEntries(List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups) {
|
||||
List<BfiReceivedInvoiceSaveEntryDTO> entries = new ArrayList<>();
|
||||
for (KingdeeApBillSyncSupport.BfiEntryGroup entryGroup : entryGroups) {
|
||||
BfiReceivedInvoiceSaveEntryDTO entry = new BfiReceivedInvoiceSaveEntryDTO();
|
||||
// 【必填】价税合计:该行含税金额(与应付单 e_pricetaxtotal 同源取数)
|
||||
entry.setEPricetaxtotal(kingdeeApBillSyncSupport.money(entryGroup.amount()));
|
||||
// 【必填】数量:该行运输数量,无吨位数据时兜底 1(与应付单口径一致)
|
||||
entry.setQuantity(entryGroup.quantity().compareTo(BigDecimal.ZERO) > 0 ? entryGroup.quantity() : BigDecimal.ONE);
|
||||
// 【必填】单价:该行运输单价(与应付单同源;单据头 isincludetax=true 录入价税合计,单价仅作参考)
|
||||
entry.setPrice(entryGroup.price());
|
||||
// 【必填】物料编码.编码:按装卸大类/项目业务模式/运单运输方式映射(thirdparty.bfi.material.* 配置,与应付单同源)
|
||||
entry.setMaterialNumber(entryGroup.materialNumber());
|
||||
// 金额(不含税):该行结算金额(不含税)(与应付单 e_amount 同源取数;结算合计费用项行无不含税金额不传)
|
||||
if (entryGroup.amountNoTax() != null) {
|
||||
entry.setEAmount(kingdeeApBillSyncSupport.money(entryGroup.amountNoTax()));
|
||||
}
|
||||
// 【必填】外部系统来源分录标识(候选键):与应付单分录同键(结算明细行单据号/结算合计费用项拼接),便于逐行勾稽
|
||||
entry.setAwrpEntryCode(entryGroup.entryCode());
|
||||
|
||||
entries.add(entry);
|
||||
}
|
||||
if (entries.isEmpty()) {
|
||||
throw new ServiceException("推送金蝶收票单失败:分摊结算单无结算明细与结算合计费用项,无法组装收票单明细");
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
// ==================== A302-6 收票指定应付 ====================
|
||||
|
||||
/**
|
||||
* 收票单指定财务应付单(A302-6,一票一应付):按「收票单防重键(收票登记ID)↔ 财务应付单防重键
|
||||
* (正式结算单号_结算明细单据号拼接)」建立绑定;金蝶侧解除指定(unassign)不自动执行,人工处理后重推
|
||||
*
|
||||
* @param invoiceSrcCode 收票单防重键(收票登记ID,即 A303-2 的 awrp_src_code = receipt.getId())
|
||||
* @param apBillSrcCode 财务应付单防重键(即 A102-2 的 awrp_src_code = 正式结算单号_结算明细单据号拼接)
|
||||
*/
|
||||
public void assignApBill(String invoiceSrcCode, String apBillSrcCode) {
|
||||
BfiInvoiceAssignItemDTO item = new BfiInvoiceAssignItemDTO();
|
||||
item.setApInvoice(List.of(buildAssignRef(invoiceSrcCode)));
|
||||
item.setApFinapbill(List.of(buildAssignRef(apBillSrcCode)));
|
||||
BfiBillSaveResultVO assignResult = bfiService.invoiceAssign("assign", List.of(item));
|
||||
kingdeeApBillSyncSupport.requireBfiSaveSuccess(assignResult, "金蝶收票单指定应付");
|
||||
}
|
||||
|
||||
private BfiInvoiceAssignRefDTO buildAssignRef(String awrpSrcCode) {
|
||||
BfiInvoiceAssignRefDTO ref = new BfiInvoiceAssignRefDTO();
|
||||
ref.setAwrpSrcCode(awrpSrcCode);
|
||||
ref.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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.support;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.redis.lock.LockType;
|
||||
import org.springblade.core.redis.lock.RedisLockClient;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 金蝶定时同步 single-flight 分布式锁支撑组件(模式同全票池同步 syncKingdeeInvoicePool):
|
||||
* 多副本部署的定时任务与手动触发端点共用一道闸,同一轮同步全局最多一份在跑。
|
||||
* <p>零等待抢锁:已有轮次进行中则立刻放弃本轮并返回跳过摘要(定时任务下轮幂等重试,手动触发稍后再试);
|
||||
* 租约须大于最坏一轮耗时(含翻页远程查询),否则租约到期后另一副本会并发起跑;进程异常退出时锁在租约到期后自动释放;
|
||||
* 未启用 blade.lock.enabled 时降级为直接执行(多副本各自起跑,联调环境可接受,生产须开启)。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeSyncLockSupport {
|
||||
|
||||
/** 分布式锁客户端,仅在 blade.lock.enabled=true 时存在,缺失时同步降级为无 single-flight 保护 */
|
||||
private final ObjectProvider<RedisLockClient> redisLockClientProvider;
|
||||
|
||||
/**
|
||||
* 在 single-flight 分布式锁保护下执行一轮同步
|
||||
*
|
||||
* @param lockPrefix 锁键前缀(业务侧隔离,组件内自动追加租户标识)
|
||||
* @param leaseSeconds 锁租约秒数,须大于最坏一轮耗时
|
||||
* @param taskName 同步任务名,用于日志与跳过摘要
|
||||
* @param action 同步主体
|
||||
* @return 同步摘要;已有轮次进行中时返回跳过摘要
|
||||
*/
|
||||
public String runSingleFlight(String lockPrefix, long leaseSeconds, String taskName, Supplier<String> action) {
|
||||
RedisLockClient redisLockClient = redisLockClientProvider.getIfAvailable();
|
||||
if (redisLockClient == null) {
|
||||
// 未启用 blade.lock.enabled:多副本部署会各自起跑同一轮同步,联调环境可接受,生产须开启
|
||||
log.warn("未启用分布式锁(blade.lock.enabled=false),{}无 single-flight 保护", taskName);
|
||||
return action.get();
|
||||
}
|
||||
String lockKey = lockPrefix + currentTenant();
|
||||
boolean locked = false;
|
||||
try {
|
||||
// 零等待抢锁:已有轮次在跑就立刻放弃本轮,多副本与手动触发共用这一道闸
|
||||
locked = redisLockClient.tryLock(lockKey, LockType.REENTRANT, 0, leaseSeconds, TimeUnit.SECONDS);
|
||||
if (!locked) {
|
||||
log.warn("{}已有轮次进行中,本轮跳过,锁键={}", taskName, lockKey);
|
||||
return taskName + "进行中,本轮跳过";
|
||||
}
|
||||
return action.get();
|
||||
} catch (InterruptedException interruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ServiceException("等待" + taskName + "分布式锁被中断");
|
||||
} finally {
|
||||
if (locked) {
|
||||
redisLockClient.unLock(lockKey, LockType.REENTRANT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 租户标识:定时任务上下文无登录用户时以 default 加锁,锁粒度与数据隔离边界一致 */
|
||||
private String currentTenant() {
|
||||
String tenantId = AuthUtil.getTenantId();
|
||||
return Func.isEmpty(tenantId) ? "default" : tenantId;
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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.task;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 金蝶客商数据定时同步任务
|
||||
* <p>
|
||||
* 对应业财融合「客商数据定时同步子流程」(图01,ERP 泳道):
|
||||
* ① 待推送客商(审批通过且未推送/推送失败)自动执行 A902-1 复查 + A902-2 推送;
|
||||
* ② 已同步客商回写金蝶使用状态,金蝶禁用(enable=0)联动停用客商档案。
|
||||
* A901-1 供应商查询待金蝶 SDK(blade-bfi-api)提供能力后扩展。
|
||||
* 业务逻辑收敛在 {@link ICustomerArchiveService#syncKingdeeCustomerData()},便于手动触发联调。
|
||||
* <p>
|
||||
* 调度配置(Nacos thirdParty.bfi.customer.*):syncEnabled 开关、cron 表达式。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerKingdeeSyncTask {
|
||||
|
||||
private final ICustomerArchiveService customerArchiveService;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
/** 客商金蝶数据同步调度,默认每天 02:00 执行 */
|
||||
@Scheduled(cron = "${thirdParty.bfi.customer.cron:0 0 2 * * ?}")
|
||||
public void syncKingdeeCustomerData() {
|
||||
if (!bfiProperties.getCustomer().isSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String summary = customerArchiveService.syncKingdeeCustomerData();
|
||||
log.info("定时同步金蝶客商数据:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时同步金蝶客商数据异常", exception);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 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 the License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package org.springblade.transport.task;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.service.IInvoiceReceiptService;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 收票管理金蝶全票池发票定时同步任务(瘦身版)
|
||||
* <p>
|
||||
* 定时按增量窗口分页拉取金蝶发票云全票池发票数据(A801-1,经 BFI 网关 rim 契约透传),
|
||||
* 镜像表 blade_kingdee_invoice_pool 全列批量 upsert,供选票统一查本地(真分页/名称模糊/认领登记过滤)。
|
||||
* 业务逻辑收敛在 {@link IInvoiceReceiptService#syncKingdeeInvoicePool()},任务类只做开关与调度,便于手动触发联调。
|
||||
* <p>
|
||||
* 调度配置(Nacos thirdParty.bfi.invoiceReceipt.*):syncEnabled 开关、cron 表达式、incrementDays 增量窗口。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class InvoiceReceiptKingdeeSyncTask {
|
||||
|
||||
private final IInvoiceReceiptService invoiceReceiptService;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
/** 金蝶全票池同步调度,默认每30分钟执行一次 */
|
||||
@Scheduled(cron = "${thirdParty.bfi.invoiceReceipt.cron:0 16/30 * * * ?}")
|
||||
public void syncKingdeeInvoicePool() {
|
||||
if (!bfiProperties.getInvoiceReceipt().isSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String summary = invoiceReceiptService.syncKingdeeInvoicePool();
|
||||
log.info("定时同步金蝶全票池发票:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时同步金蝶全票池发票异常", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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.task;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IInvoiceApplicationService;
|
||||
import org.springblade.transport.service.IInvoiceReceiptService;
|
||||
import org.springblade.transport.service.IPaymentApplicationService;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 金蝶自动同步失败定时重推任务(兜底):仅重推各域 failed 单据,不扫描 unsynced——
|
||||
* 存量"审批通过但从未推送"的历史单不自动补推(避免上线即批量推历史数据),仍走手动按钮。
|
||||
* <p>
|
||||
* 前置未就绪(如上游应付单未生成)→ 预检拦截 → 再次 failed → 下轮继续;
|
||||
* 前置就绪 → 自动续推成功(节点硬顺序由既有 A102-4/A302-4/A303-4/A801-3 预检保证,不做级联)。
|
||||
* <p>
|
||||
* ponytail: 重推无次数上限,永久性坏数据(如联行号未维护)会每轮失败一次;
|
||||
* 噪音可接受后,再加 retry_count 上限列。
|
||||
* <p>
|
||||
* 调度配置(Nacos thirdParty.bfi.autoSync.*):retryEnabled 开关、retryCron 表达式。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeAutoSyncRetryTask {
|
||||
|
||||
private final IPaymentApplicationService paymentApplicationService;
|
||||
private final IFormalSettlementService formalSettlementService;
|
||||
private final IInvoiceApplicationService invoiceApplicationService;
|
||||
private final IInvoiceReceiptService invoiceReceiptService;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
/** failed 定时重推调度,默认每10分钟执行一次 */
|
||||
@Scheduled(cron = "${thirdParty.bfi.autoSync.retryCron:0 5/10 * * * ?}")
|
||||
public void retryKingdeeFailed() {
|
||||
if (!bfiProperties.getAutoSync().isRetryEnabled()) {
|
||||
return;
|
||||
}
|
||||
// ① 结算单(A102-2/A302-2)
|
||||
try {
|
||||
String summary = formalSettlementService.retryKingdeeFailed();
|
||||
log.info("定时重推金蝶生成失败结算单:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时重推金蝶生成失败结算单异常", exception);
|
||||
}
|
||||
// ② 付款申请(A103-1/A103-3)
|
||||
try {
|
||||
String summary = paymentApplicationService.retryKingdeeFailed();
|
||||
log.info("定时重推金蝶生成失败付款申请:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时重推金蝶生成失败付款申请异常", exception);
|
||||
}
|
||||
// ③ 开票申请(A303-1)
|
||||
try {
|
||||
String summary = invoiceApplicationService.retryKingdeeFailed();
|
||||
log.info("定时重推金蝶生成失败开票申请:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时重推金蝶生成失败开票申请异常", exception);
|
||||
}
|
||||
// ④ 收票登记(A303-2+A302-6)
|
||||
try {
|
||||
String summary = invoiceReceiptService.retryKingdeeFailed();
|
||||
log.info("定时重推金蝶推送失败收票登记:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时重推金蝶推送失败收票登记异常", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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.task;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.service.IKingdeeBillLockService;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定补偿定时任务
|
||||
* <p>
|
||||
* 扫描锁定失败的锁记录(lock_status=failed 且未超重试上限)重调 lockBusiData,
|
||||
* 补偿推送成功但锁单失败的单据,不依赖人工重推「同步金蝶」。
|
||||
* 业务逻辑收敛在 {@link IKingdeeBillLockService#retryFailedLocks()},任务类只做开关与调度(模式同 PaymentKingdeeResultTask)。
|
||||
* <p>
|
||||
* 调度配置(Nacos thirdParty.bfi.billLock.*):retryEnabled 开关、retryCron 表达式。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeBillLockRetryTask {
|
||||
|
||||
private final IKingdeeBillLockService kingdeeBillLockService;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
/** 锁定补偿调度,默认每5分钟执行一次 */
|
||||
@Scheduled(cron = "${thirdParty.bfi.billLock.retryCron:0 2/5 * * * ?}")
|
||||
public void retryFailedBillLocks() {
|
||||
if (!bfiProperties.getBillLock().isRetryEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String summary = kingdeeBillLockService.retryFailedLocks();
|
||||
log.info("定时补偿金蝶单据锁定:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时补偿金蝶单据锁定异常", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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.task;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IInvoiceApplicationService;
|
||||
import org.springblade.transport.service.IInvoiceReceiptService;
|
||||
import org.springblade.transport.service.IPaymentApplicationService;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 金蝶结果定时回写任务
|
||||
* <p>
|
||||
* 定时查询金蝶(星瀚)结果并回写 TMS 单据,编排三个子流程(各自独立 try/catch,互不影响):
|
||||
* ① A103-2/A103-4 付款结果:回填付款申请单据编号、单据状态与付款记录;
|
||||
* ② A302-4 应收勾稽纠偏:应收结算单回填金蝶应收单编号、生成失败纠偏为已同步;
|
||||
* ③ A303-5 开票结果回写:开票申请发票结果落库并推进开票完成。
|
||||
* 各子流程业务逻辑收敛在对应服务方法,便于手动触发联调。
|
||||
* <p>
|
||||
* 另有金蝶收票单结果回写(A801-3,图05 环节②③异常回收/指定补偿)独立调度,
|
||||
* 配置 thirdParty.bfi.receivedInvoiceResult.*。
|
||||
* <p>
|
||||
* 调度配置(Nacos thirdParty.bfi.payResult.*):syncEnabled 开关、cron 表达式。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentKingdeeResultTask {
|
||||
|
||||
private final IPaymentApplicationService paymentApplicationService;
|
||||
private final IFormalSettlementService formalSettlementService;
|
||||
private final IInvoiceApplicationService invoiceApplicationService;
|
||||
private final IInvoiceReceiptService invoiceReceiptService;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
/** 金蝶结果回写调度,默认每10分钟执行一次 */
|
||||
@Scheduled(cron = "${thirdParty.bfi.payResult.cron:0 1/10 * * * ?}")
|
||||
public void syncKingdeePaymentResult() {
|
||||
if (!bfiProperties.getPayResult().isSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
// ① A103-2/A103-4 付款结果回写
|
||||
try {
|
||||
String summary = paymentApplicationService.syncKingdeePaymentResult();
|
||||
log.info("定时回写金蝶付款结果:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时回写金蝶付款结果异常", exception);
|
||||
}
|
||||
// ② A302-4 应收勾稽纠偏
|
||||
try {
|
||||
String summary = formalSettlementService.syncKingdeeArResult();
|
||||
log.info("定时勾稽金蝶应收单:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时勾稽金蝶应收单异常", exception);
|
||||
}
|
||||
// ③ A303-5 开票结果回写
|
||||
try {
|
||||
String summary = invoiceApplicationService.syncKingdeeInvoiceResult();
|
||||
log.info("定时回写金蝶开票结果:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时回写金蝶开票结果异常", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/** 金蝶收票单结果回写调度(异常回收/指定补偿/单号补登),默认每10分钟执行一次 */
|
||||
@Scheduled(cron = "${thirdParty.bfi.receivedInvoiceResult.cron:0 8/10 * * * ?}")
|
||||
public void syncKingdeeReceivedInvoiceResult() {
|
||||
if (!bfiProperties.getReceivedInvoiceResult().isSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String summary = invoiceReceiptService.syncKingdeeReceivedInvoiceResult();
|
||||
log.info("定时回写金蝶收票单结果:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时回写金蝶收票单结果异常", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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
|
||||
* is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for their own commercial purposes.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this license agreement and terms of use,
|
||||
* and you expressly agree to be bound by the terms of use of this software.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
|
||||
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.task;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.service.IReceiptFlowService;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 收款认领推送金蝶定时任务(A301-3 收款认领结果推送)
|
||||
* <p>
|
||||
* 认领动作只在本地登记,本任务扫描「认领完成且未推送过」的收款流水自动推送金蝶,
|
||||
* 与收款流水页「推送金蝶」手动按钮共用同一服务方法(推送前按认领人重查 A301-2 最新公告对账)。
|
||||
* 单条流水推送失败独立消化、可重试,不影响同批其他流水。
|
||||
* <p>
|
||||
* 调度配置(Nacos thirdParty.bfi.receiptClaimPush.*):syncEnabled 开关、cron 表达式。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ReceiptClaimPushTask {
|
||||
|
||||
private final IReceiptFlowService receiptFlowService;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
/** 收款认领推送调度,默认每10分钟执行一次 */
|
||||
@Scheduled(cron = "${thirdParty.bfi.receiptClaimPush.cron:0 4/10 * * * ?}")
|
||||
public void pushClaimToKingdee() {
|
||||
if (!bfiProperties.getReceiptClaimPush().isSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String summary = receiptFlowService.pushClaimByTask();
|
||||
log.info("定时推送收款认领:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时推送收款认领异常", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -58,6 +58,7 @@ public class InvoiceApplicationWrapper extends BaseEntityWrapper<InvoiceApplicat
|
||||
vo.setKingdeeStatusName(switch (entity.getKingdeeStatus() == null ? "" : entity.getKingdeeStatus()) {
|
||||
case "unsynced" -> "未同步";
|
||||
case "synced" -> "已同步";
|
||||
case "invoiced" -> "开票完成";
|
||||
case "failed" -> "同步失败";
|
||||
default -> entity.getKingdeeStatus();
|
||||
});
|
||||
|
||||
+5
-4
@@ -57,10 +57,11 @@ public class InvoiceReceiptWrapper extends BaseEntityWrapper<InvoiceReceipt, Inv
|
||||
case "voided" -> "已作废";
|
||||
default -> entity.getApprovalStatus();
|
||||
});
|
||||
vo.setKingdeeStatusName(switch (entity.getKingdeeStatus() == null ? "" : entity.getKingdeeStatus()) {
|
||||
case "synced" -> "已同步";
|
||||
case "failed" -> "同步失败";
|
||||
default -> "未同步";
|
||||
vo.setKingdeePushStatusName(switch (entity.getKingdeePushStatus() == null ? "" : entity.getKingdeePushStatus()) {
|
||||
case "pushed" -> "收票单已保存";
|
||||
case "assigned" -> "已指定应付";
|
||||
case "failed" -> "推送失败";
|
||||
default -> "未推送";
|
||||
});
|
||||
return vo;
|
||||
}
|
||||
|
||||
+8
-3
@@ -22,6 +22,8 @@ package org.springblade.transport.wrapper;
|
||||
import org.springblade.core.mp.support.BaseEntityWrapper;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.pojo.entity.PaymentApplication;
|
||||
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
|
||||
|
||||
@@ -38,7 +40,7 @@ public class PaymentApplicationWrapper extends BaseEntityWrapper<PaymentApplicat
|
||||
vo.setPaymentTypeName(switch (entity.getPaymentType() == null ? "" : entity.getPaymentType()) {
|
||||
case "project_advance" -> "项目预付";
|
||||
case "progress_advance" -> "进度预付";
|
||||
case "settlement_payment" -> "尾款付款";
|
||||
case TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT -> "尾款付款";
|
||||
default -> entity.getPaymentType();
|
||||
});
|
||||
vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) {
|
||||
@@ -50,8 +52,11 @@ public class PaymentApplicationWrapper extends BaseEntityWrapper<PaymentApplicat
|
||||
default -> entity.getApprovalStatus();
|
||||
});
|
||||
vo.setKingdeeStatusName(switch (entity.getKingdeeStatus() == null ? "" : entity.getKingdeeStatus()) {
|
||||
case "synced" -> "已生成";
|
||||
case "failed" -> "生成失败";
|
||||
case KingdeeBfiConstant.KINGDEE_STATUS_SYNCED -> "已生成";
|
||||
case KingdeeBfiConstant.KINGDEE_STATUS_PAYING -> "付款中";
|
||||
case KingdeeBfiConstant.KINGDEE_STATUS_PAID -> "已付款";
|
||||
case KingdeeBfiConstant.KINGDEE_STATUS_CLOSED -> "已关闭";
|
||||
case KingdeeBfiConstant.KINGDEE_STATUS_FAILED -> "生成失败";
|
||||
default -> "未生成";
|
||||
});
|
||||
return vo;
|
||||
|
||||
+20
-2
@@ -53,9 +53,27 @@ public class ReceiptFlowWrapper extends BaseEntityWrapper<KingdeeReceiptFlow, Re
|
||||
case "claimed" -> "认领完成";
|
||||
default -> "未认领";
|
||||
});
|
||||
BigDecimal receiptAmount = money(entity.getReceiptAmount());
|
||||
vo.setKingdeeClaimStatusName(switch (Objects.toString(entity.getKingdeeClaimStatus(), "")) {
|
||||
case "1" -> "已认领";
|
||||
case "0" -> "未认领";
|
||||
default -> Objects.toString(entity.getKingdeeClaimStatus(), "");
|
||||
});
|
||||
vo.setPushStatusName(switch (Objects.toString(entity.getPushStatus(), "")) {
|
||||
case "pushed" -> "已推送";
|
||||
case "failed" -> "推送失败";
|
||||
default -> "未推送";
|
||||
});
|
||||
vo.setKingdeeBillStatusName(switch (Objects.toString(entity.getKingdeeBillStatus(), "")) {
|
||||
case "pushed" -> "已推送";
|
||||
case "approved" -> "审核通过";
|
||||
case "received" -> "已收款";
|
||||
case "failed" -> "处理失败";
|
||||
default -> "未推送";
|
||||
});
|
||||
// 剩余可认领金额 = 公告未认领金额 − 累计认领金额(认领上限为公告口径)
|
||||
BigDecimal unclaimedAmount = money(entity.getUnclaimedAmount());
|
||||
BigDecimal claimedAmount = money(entity.getClaimedAmount());
|
||||
vo.setRemainingAmount(receiptAmount.subtract(claimedAmount).max(BigDecimal.ZERO));
|
||||
vo.setRemainingAmount(unclaimedAmount.subtract(claimedAmount).max(BigDecimal.ZERO));
|
||||
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
|
||||
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
|
||||
return vo;
|
||||
|
||||
+12
@@ -48,7 +48,19 @@ public class TransportVehicleWrapper extends BaseEntityWrapper<TransportVehicle,
|
||||
public TransportVehicleVO entityVO(TransportVehicle vehicle) {
|
||||
TransportVehicleVO vo = Objects.requireNonNull(BeanUtil.copyProperties(vehicle, TransportVehicleVO.class));
|
||||
vo.setCreateUserName(UserCache.getUserRealName(vehicle.getCreateUser()));
|
||||
// BladeX 数字 null 常序列化为 -1;详情展示时把未填尺寸还原为空
|
||||
vo.setOuterLength(normalizeOptionalInteger(vo.getOuterLength()));
|
||||
vo.setOuterWidth(normalizeOptionalInteger(vo.getOuterWidth()));
|
||||
vo.setOuterHeight(normalizeOptionalInteger(vo.getOuterHeight()));
|
||||
vo.setTractionMassKg(normalizeOptionalInteger(vo.getTractionMassKg()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
private Integer normalizeOptionalInteger(Integer value) {
|
||||
if (value == null || value == -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
server:
|
||||
port: 8110
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: blade-transport
|
||||
config:
|
||||
import:
|
||||
- nacos:blade.yaml?group=DEFAULT_GROUP&refreshEnabled=true
|
||||
- nacos:blade-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true
|
||||
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml?group=DEFAULT_GROUP&refreshEnabled=true
|
||||
cloud:
|
||||
nacos:
|
||||
username: ${NACOS_USERNAME:${NACOS_PROD_USERNAME:nacos}}
|
||||
password: ${NACOS_PASSWORD:${NACOS_PROD_PASSWORD:nacos}}
|
||||
server-addr: ${NACOS_HOST:${NACOS_PROD_HOST:127.0.0.1:8848}}
|
||||
discovery:
|
||||
namespace: "${NACOS_NAMESPACE:}"
|
||||
config:
|
||||
file-extension: yaml
|
||||
namespace: "${NACOS_NAMESPACE:}"
|
||||
datasource:
|
||||
url: ${blade.datasource.${spring.profiles.active}.url}
|
||||
username: ${blade.datasource.${spring.profiles.active}.username}
|
||||
password: ${blade.datasource.${spring.profiles.active}.password}
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springblade.thirdparty.bfi.feign.IBfiClient: DEBUG
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* 客商档案金蝶客商数据定时同步测试:手动触发 syncKingdeeCustomerData 定时任务逻辑
|
||||
* <p>
|
||||
* 依赖本地环境(Nacos / 数据库 / Redis),需先启动对应中间件再执行;
|
||||
* 该逻辑与定时任务 CustomerKingdeeSyncTask 共用,开关默认关闭时可通过本测试手动触发联调。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@SpringBootTest(classes = TransportApplication.class, properties = {
|
||||
"spring.profiles.active=dev",
|
||||
"spring.main.allow-bean-definition-overriding=true"
|
||||
})
|
||||
class CustomerArchiveSyncKingdeeDataTest {
|
||||
|
||||
@Autowired
|
||||
private ICustomerArchiveService customerArchiveService;
|
||||
|
||||
@Test
|
||||
void syncKingdeeCustomerData() {
|
||||
String result = customerArchiveService.syncKingdeeCustomerData();
|
||||
System.out.println("金蝶客商数据定时同步执行结果:" + result);
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 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 the 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;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.transport.support.KingdeeApBillSyncSupport;
|
||||
import org.springblade.transport.support.KingdeeApBillSyncSupport.FeeLineSplit;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 金蝶应付/应收单分录拆分逻辑测试(纯函数,无环境依赖):
|
||||
* 结算明细费用项按 费用大类(运输/装卸)→ 税率 拆分行,验证三个联调场景与残差勾稽
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
class KingdeeEntrySplitTest {
|
||||
|
||||
/** 场景1:同费用大类(运输)、多税率 → 按税率拆两行 */
|
||||
@Test
|
||||
void splitFreightByTaxRate() {
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
feeItems.put("运输费", new BigDecimal("2000"));
|
||||
feeItems.put("附加运输补贴", new BigDecimal("1120"));
|
||||
Map<String, BigDecimal> taxRateByRule = Map.of(
|
||||
"运输费", new BigDecimal("9"),
|
||||
"附加运输补贴", new BigDecimal("6"));
|
||||
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, Map.of(), taxRateByRule, new BigDecimal("3120"));
|
||||
assertEquals(2, splits.size());
|
||||
assertFalse(splits.get(0).isLoading());
|
||||
assertEquals(new BigDecimal("9"), splits.get(0).getTaxRate());
|
||||
assertEquals(new BigDecimal("2000"), splits.get(0).getAmount());
|
||||
assertEquals("-9", splits.get(0).taxRateSuffix());
|
||||
assertFalse(splits.get(1).isLoading());
|
||||
assertEquals(new BigDecimal("6"), splits.get(1).getTaxRate());
|
||||
assertEquals(new BigDecimal("1120"), splits.get(1).getAmount());
|
||||
assertEquals("-6", splits.get(1).taxRateSuffix());
|
||||
}
|
||||
|
||||
/** 场景2:运输费 + 装卸费 → 运输行 + 装卸行(装卸行税率独立) */
|
||||
@Test
|
||||
void splitFreightAndLoading() {
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
feeItems.put("运输费", new BigDecimal("3120"));
|
||||
feeItems.put("装卸费", new BigDecimal("800"));
|
||||
Map<String, BigDecimal> taxRateByRule = Map.of(
|
||||
"运输费", new BigDecimal("9"),
|
||||
"装卸费", new BigDecimal("6"));
|
||||
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, Map.of(), taxRateByRule, new BigDecimal("3920"));
|
||||
assertEquals(2, splits.size());
|
||||
assertFalse(splits.get(0).isLoading());
|
||||
assertEquals(new BigDecimal("3120"), splits.get(0).getAmount());
|
||||
assertTrue(splits.get(1).isLoading());
|
||||
assertEquals(new BigDecimal("6"), splits.get(1).getTaxRate());
|
||||
assertEquals(new BigDecimal("800"), splits.get(1).getAmount());
|
||||
}
|
||||
|
||||
/** 费用项档案分类判定:名称不含“装卸”的费用项,档案费用类型名称含“装卸”即归装卸大类 */
|
||||
@Test
|
||||
void loadingCategoryFromFeeItemArchive() {
|
||||
Map<String, BigDecimal> feeItems = Map.of("吊装作业", new BigDecimal("500"));
|
||||
Map<String, String> feeCategoryByName = Map.of("吊装作业", "装卸");
|
||||
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, feeCategoryByName, Map.of(), new BigDecimal("500"));
|
||||
assertEquals(1, splits.size());
|
||||
assertTrue(splits.get(0).isLoading());
|
||||
}
|
||||
|
||||
/** 残差勾稽:费用项JSON未含运费(历史数据)时,结算金额与费用项合计的差额并入运输类无税率组 */
|
||||
@Test
|
||||
void residualGoesToFreightDefaultGroup() {
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
feeItems.put("运输费", new BigDecimal("2000"));
|
||||
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, Map.of(), Map.of("运输费", new BigDecimal("9")), new BigDecimal("2020"));
|
||||
assertEquals(2, splits.size());
|
||||
assertEquals(new BigDecimal("2000"), splits.get(0).getAmount());
|
||||
assertFalse(splits.get(1).isLoading());
|
||||
assertNull(splits.get(1).getTaxRate());
|
||||
assertEquals(new BigDecimal("20"), splits.get(1).getAmount());
|
||||
assertEquals("", splits.get(1).taxRateSuffix());
|
||||
}
|
||||
|
||||
/** 费用项JSON为空:整行结算金额归运输类无税率组单行 */
|
||||
@Test
|
||||
void emptyFeeItemsFallbackToFreightLine() {
|
||||
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(Map.of(), Map.of(), Map.of(), new BigDecimal("3120"));
|
||||
assertEquals(1, splits.size());
|
||||
assertFalse(splits.get(0).isLoading());
|
||||
assertNull(splits.get(0).getTaxRate());
|
||||
assertEquals(new BigDecimal("3120"), splits.get(0).getAmount());
|
||||
}
|
||||
|
||||
/** 税率未配置的费用项归入无税率组,同大类同税率费用项金额合并 */
|
||||
@Test
|
||||
void mergeSameCategorySameRate() {
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
feeItems.put("装卸费", new BigDecimal("500"));
|
||||
feeItems.put("装卸吊装费", new BigDecimal("300"));
|
||||
feeItems.put("其他费用", new BigDecimal("100"));
|
||||
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, Map.of(), Map.of(), new BigDecimal("900"));
|
||||
assertEquals(2, splits.size());
|
||||
assertTrue(splits.get(0).isLoading());
|
||||
assertEquals(new BigDecimal("800"), splits.get(0).getAmount());
|
||||
assertNull(splits.get(0).getTaxRate());
|
||||
assertEquals("装卸费", splits.get(0).getFirstFeeItem());
|
||||
assertFalse(splits.get(1).isLoading());
|
||||
assertEquals(new BigDecimal("100"), splits.get(1).getAmount());
|
||||
}
|
||||
|
||||
/** 0元费用项跳过:不生成0元分录(FJS2026101300005 结算单8条分录问题的回归用例) */
|
||||
@Test
|
||||
void zeroAmountFeeItemsSkipped() {
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
feeItems.put("运输费", new BigDecimal("3120"));
|
||||
feeItems.put("附加运输补贴", BigDecimal.ZERO);
|
||||
feeItems.put("装卸费", new BigDecimal("800"));
|
||||
feeItems.put("仓储服务费", BigDecimal.ZERO);
|
||||
Map<String, BigDecimal> taxRateByRule = Map.of(
|
||||
"运输费", new BigDecimal("9"),
|
||||
"附加运输补贴", new BigDecimal("6"),
|
||||
"装卸费", new BigDecimal("6"));
|
||||
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, Map.of(), taxRateByRule, new BigDecimal("3920"));
|
||||
assertEquals(2, splits.size());
|
||||
assertFalse(splits.get(0).isLoading());
|
||||
assertEquals(new BigDecimal("3120"), splits.get(0).getAmount());
|
||||
assertTrue(splits.get(1).isLoading());
|
||||
assertEquals(new BigDecimal("800"), splits.get(1).getAmount());
|
||||
}
|
||||
|
||||
/** 全部费用项为0且结算金额为0:拆分结果为空(调用方跳过该0元货物行) */
|
||||
@Test
|
||||
void allZeroFeeItemsYieldEmptySplits() {
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
feeItems.put("运输费", BigDecimal.ZERO);
|
||||
feeItems.put("装卸费", BigDecimal.ZERO);
|
||||
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, Map.of(), Map.of(), BigDecimal.ZERO);
|
||||
assertTrue(splits.isEmpty());
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -110,7 +110,7 @@ class CertAuditServiceImplTest {
|
||||
ArgumentCaptor<TransportVehicle> captor = ArgumentCaptor.forClass(TransportVehicle.class);
|
||||
verify(transportVehicleService).updateById(captor.capture());
|
||||
TransportVehicle patched = captor.getValue();
|
||||
assertEquals("桂物物流集团", patched.getOwnerName(), "所有人要跟着生效值更新");
|
||||
assertEquals("广西现代物流集团有限公司", patched.getOwnerName(), "所有人要跟着生效值更新");
|
||||
assertEquals("管理挂靠", patched.getBusinessRelation());
|
||||
assertEquals(18000, patched.getApprovedLoadKg());
|
||||
assertEquals(RECORD_ID, patched.getEffectiveCertRecordId(), "内容与指针必须是同一次 UPDATE");
|
||||
@@ -441,7 +441,7 @@ class CertAuditServiceImplTest {
|
||||
*/
|
||||
static String completeVehicleSnapshot() {
|
||||
return "{\"drivingLicenseNo\":\"450100123456\",\"approvedLoadKg\":18000,"
|
||||
+ "\"ownerName\":\"桂物物流集团\",\"businessRelation\":\"管理挂靠\","
|
||||
+ "\"ownerName\":\"广西现代物流集团有限公司\",\"businessRelation\":\"管理挂靠\","
|
||||
+ "\"vehicleType\":\"重型货车\",\"driveLicenseImage\":\"http://oss/xsz.jpg\"}";
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -464,13 +464,13 @@ class DriverCertServiceImplTest {
|
||||
assertEquals("重型货车", vehicleCaptor.getValue().getVehicleType());
|
||||
assertNull(vehicleCaptor.getValue().getEffectiveCertRecordId(),
|
||||
"新建的车不得自带生效快照——是否已认证只能由审核通过决定");
|
||||
assertEquals("桂物物流集团", vehicleCaptor.getValue().getOrganizationName(),
|
||||
assertEquals("广西现代物流集团有限公司", vehicleCaptor.getValue().getOrganizationName(),
|
||||
"所属组织必须从司机档案带出:该列 NOT NULL 且无默认值,空着这条 INSERT 直接报错");
|
||||
assertEquals("管理挂靠", vehicleCaptor.getValue().getBusinessRelation(),
|
||||
"业务关系由司机在表单里选,服务端照实落库、不替用户猜");
|
||||
assertNotNull(vehicleCaptor.getValue().getRegistrationNo(),
|
||||
"机动车登记编号该列 NOT NULL:司机端选填,缺值要写空串而不是 null");
|
||||
assertEquals("桂物物流集团", vehicleCaptor.getValue().getOwnerName(),
|
||||
assertEquals("广西现代物流集团有限公司", vehicleCaptor.getValue().getOwnerName(),
|
||||
"所有人是行驶证上的登记主体,与所属组织是两个字段");
|
||||
|
||||
// 绑定:司机的 driving_vehicle 被追加该车牌
|
||||
@@ -663,7 +663,7 @@ class DriverCertServiceImplTest {
|
||||
VehicleCertSubmitRequest request = new VehicleCertSubmitRequest();
|
||||
request.setPlateNo(plateNo);
|
||||
request.setVehicleType("重型货车");
|
||||
request.setOwnerName("桂物物流集团");
|
||||
request.setOwnerName("广西现代物流集团有限公司");
|
||||
request.setPlateColor("黄牌");
|
||||
request.setEnergyType("柴油");
|
||||
request.setBusinessRelation("管理挂靠");
|
||||
@@ -691,7 +691,7 @@ class DriverCertServiceImplTest {
|
||||
driver.setDriverName("张三");
|
||||
// 车辆表的 organization_name 是 NOT NULL 且无默认值:司机缺组织时建车必然失败,
|
||||
// 故建车链路要求它非空,fixture 也必须给
|
||||
driver.setOrganizationName("桂物物流集团");
|
||||
driver.setOrganizationName("广西现代物流集团有限公司");
|
||||
driver.setDrivingVehicle(drivingVehicle);
|
||||
return driver;
|
||||
}
|
||||
|
||||
+4
-4
@@ -32,7 +32,7 @@ class OcrFieldSupportTest {
|
||||
"words_result": {
|
||||
"号牌号码": {"words": "桂A32523"},
|
||||
"车辆类型": {"words": "重型厢式货车"},
|
||||
"所有人": {"words": "桂物物流集团"},
|
||||
"所有人": {"words": "广西现代物流集团有限公司"},
|
||||
"车辆识别代码": {"words": "LGAX2A130J1001234"},
|
||||
"档案编号": {"words": "450100123456"},
|
||||
"核定载质量": {"words": "18000kg"},
|
||||
@@ -48,7 +48,7 @@ class OcrFieldSupportTest {
|
||||
|
||||
assertEquals("桂A32523", fields.get("plateNo"));
|
||||
assertEquals("重型厢式货车", fields.get("vehicleType"));
|
||||
assertEquals("桂物物流集团", fields.get("ownerName"));
|
||||
assertEquals("广西现代物流集团有限公司", fields.get("ownerName"));
|
||||
assertEquals("LGAX2A130J1001234", fields.get("vin"));
|
||||
assertEquals("450100123456", fields.get("drivingLicenseNo"));
|
||||
assertEquals(18000, fields.get("approvedLoadKg"), "带 kg 单位也要能取到数字");
|
||||
@@ -160,7 +160,7 @@ class OcrFieldSupportTest {
|
||||
{
|
||||
"words_result": {
|
||||
"道路运输证号": {"words": "桂交运管桂字450100123456号"},
|
||||
"业户名称": {"words": "桂物物流集团"},
|
||||
"业户名称": {"words": "广西现代物流集团有限公司"},
|
||||
"有效期": {"words": "2020-01-01 至 2030-01-01"}
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,7 @@ class OcrFieldSupportTest {
|
||||
Map<String, Object> fields = OcrFieldSupport.extract(BaiduOcrType.ROAD_TRANSPORT_CERTIFICATE, null, result);
|
||||
|
||||
assertEquals("桂交运管桂字450100123456号", fields.get("roadTransportCertNo"));
|
||||
assertEquals("桂物物流集团", fields.get("ownerName"));
|
||||
assertEquals("广西现代物流集团有限公司", fields.get("ownerName"));
|
||||
assertEquals("2030-01-01", fields.get("roadTransportCertEndDate"), "止期字段取区间的最后一段,取第一段会把有效期起填成止");
|
||||
assertEquals("2020-01-01", fields.get("roadTransportCertStartDate"), "区间要同时带出起止,否则用户得照图再敲一遍");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user