!1 tms对接金蝶
Merge pull request !1 from mindless/feature/payment-apply-bfi-sync
This commit is contained in:
@@ -34,3 +34,5 @@ Thumbs.db
|
||||
.claude
|
||||
/outputs/
|
||||
/.codex-work/
|
||||
.serena/
|
||||
.workbuddy/
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
# TMS(运输业务中台)—— 业财集成语境
|
||||
|
||||
TMS 与集团财务系统(金蝶星瀚)、合同合规系统(HCS)集成,单据在三个系统间流转。
|
||||
本表只收录集成语境下易混淆的业务术语,不含实现细节。
|
||||
|
||||
## Language
|
||||
|
||||
**合同合规系统(HCS)**:
|
||||
集团合同合规管控系统(桂物数字合同合规),接收 TMS 推送的项目、合同、履约与付款数据并做合规审批。
|
||||
_Avoid_: 桂物系统、合同系统、数字合同系统
|
||||
|
||||
**相对方**:
|
||||
HCS 对签约主体(甲乙方)的称谓,与 TMS 的"客商"同指一个外部单位。
|
||||
_Avoid_: 在 HCS 集成语境用"客户/供应商"
|
||||
|
||||
**来源单据键**:
|
||||
TMS 单据在 HCS 侧的对账主键(接口字段 `fd_kingdee_id`,字段名沿用金蝶),统一存 TMS 业务单据主键。
|
||||
_Avoid_: 外部ID、kingdeeId(泛指金蝶单据号时易混淆)
|
||||
|
||||
**来源标识**:
|
||||
TMS 推送给 HCS 时声明自身身份的常量(接口字段 `fd_sources`,约定值 "TMS")。
|
||||
_Avoid_: XH(那是星瀚侧的来源标识)
|
||||
|
||||
**履约规则**:
|
||||
合同维度的资金计划阶段(付款/收款、款项性质、计划金额、计划日期、开票规则),由 TMS 维护并同步到 HCS。
|
||||
_Avoid_: 履约计划(旧版已废弃接口的叫法)、付款计划
|
||||
|
||||
**履约阶段**:
|
||||
履约规则表中的一行;HCS 为其生成阶段ID,后续更新/删除必须凭该ID。
|
||||
_Avoid_: 阶段项、plan 行
|
||||
|
||||
**履约记录**:
|
||||
付款/收款执行事实在 HCS 的登记(一笔付款申请一条记录,随实际付款推进更新),与履约规则是"计划 vs 执行"的关系。
|
||||
_Avoid_: 履约流水
|
||||
|
||||
**额度校验**:
|
||||
付款提交时由 HCS 判定申请金额是否超出合同/项目剩余额度的同步检查;超额不阻断提交,转入特批链路。
|
||||
_Avoid_: 余额检查
|
||||
|
||||
**特批流程**:
|
||||
额度超额(或风险不通过)时在 HCS 发起的审批流程;TMS 持有其流程ID,流程结束由 HCS 经 B103 回传结果。
|
||||
_Avoid_: 特殊审批、绿色通道
|
||||
|
||||
**特批放行**:
|
||||
特批结果为"通过"后对**本次**付款申请解除推送拦截的动作;只改特批状态,不触发推送,单次有效。
|
||||
_Avoid_: 特批通过自动付款、放行推送
|
||||
|
||||
**锁单(B901)**:
|
||||
金蝶确认接收 TMS 推送的单据后,经 B901 通知 TMS 将本地源单据置为不可编辑、不可作废、不可重推的状态;金蝶反审核及删除后经 B901 解锁。
|
||||
_Avoid_: 冻结、金蝶锁定(那是金蝶侧单据自己的状态)
|
||||
|
||||
**解锁**:
|
||||
B901 通知金蝶侧单据已反审核及删除后,TMS 解除锁单、同步状态置失败、清金蝶单号留痕;不自动重推,人工核实后重新推送。
|
||||
_Avoid_: 反审核(那是金蝶侧的动作)、自动重推
|
||||
|
||||
**合规附件ID**:
|
||||
TMS 附件先上传 HCS 附件服务换取的 fdId;同一附件以 TMS 附件元数据为键缓存映射,不重复上传。
|
||||
_Avoid_: 附件fdId(指代不明)
|
||||
|
||||
**同步状态**:
|
||||
TMS 单据对 HCS 的推送生命周期(待同步/已同步/失败),权威状态在 TMS 侧字段,HCS 不回写业务状态。
|
||||
_Avoid_: 推送标志
|
||||
|
||||
**二次认领**:
|
||||
金蝶司库完成第一轮自动认领(客户维度)后,TMS 把收款流水认领到合同/结算单维度的动作;只在 TMS 本地登记,本身不产生金蝶收款单。
|
||||
_Avoid_: 收款认领(泛指整条链路时易与金蝶第一轮自动认领混淆)
|
||||
|
||||
**公告未认领金额**:
|
||||
金蝶认领公告(A301-2)上尚未被认领的金额(字段 unclaimamount),是 TMS 二次认领的上限和"认领完成"的判定基准;以金蝶侧为准,推送前须重查最新值。
|
||||
_Avoid_: 剩余可认领金额(那是本地展示口径)、未认领金额(不指明公告侧/本地侧时指代不明)
|
||||
|
||||
**认领完成**:
|
||||
一条收款流水的累计有效认领金额等于公告未认领金额的状态;只有认领完成的流水才满足推送金蝶的条件,不认领完就不推。
|
||||
_Avoid_: 认领完毕、全部认领(未指明对照基准)
|
||||
|
||||
**认领状态**:
|
||||
收款流水本地认领进度(未认领/部分认领/认领完成),对照公告未认领金额计算;作废的认领分摊不计入累计。
|
||||
_Avoid_: 与推送状态混用
|
||||
|
||||
**推送状态**:
|
||||
收款流水对金蝶 A301-3 的推送生命周期(未推送/已推送/推送失败),权威在流水级,认领主单仅冗余展示;已推送的流水不可二次推送。
|
||||
_Avoid_: 金蝶状态(那是 A301-1 回写的单据生命周期:已推送/审核通过/已收款)
|
||||
+14
@@ -32,6 +32,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import feign.Logger;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.common.feign.CustomFeignLogger;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
/**
|
||||
* 公共封装包配置类
|
||||
@@ -49,4 +56,11 @@ public class BladeCommonConfiguration {
|
||||
return new OperationApiLogAspect();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnClass(name = "feign.Logger")
|
||||
public Logger feignLogger() {
|
||||
return new CustomFeignLogger();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* 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.common.feign;
|
||||
|
||||
import feign.Logger;
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
import feign.Util;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 自定义Feign日志记录器,只记录出入参信息
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
public class CustomFeignLogger extends Logger {
|
||||
|
||||
private static final String BINARY_CONTENT_TYPE_PREFIX = "application/octet-stream";
|
||||
private static final String IMAGE_CONTENT_TYPE_PREFIX = "image/";
|
||||
private static final String AUDIO_CONTENT_TYPE_PREFIX = "audio/";
|
||||
private static final String VIDEO_CONTENT_TYPE_PREFIX = "video/";
|
||||
private static final String MULTIPART_CONTENT_TYPE_PREFIX = "multipart/";
|
||||
|
||||
@Override
|
||||
protected void log(String configKey, String format, Object... args) {
|
||||
// 不处理默认日志格式
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void logRequest(String configKey, Level logLevel, Request request) {
|
||||
String url = request.url();
|
||||
String method = request.httpMethod().name();
|
||||
Map<String, Collection<String>> headers = request.headers();
|
||||
byte[] body = request.body();
|
||||
|
||||
String contentType = getContentType(headers);
|
||||
String bodyStr;
|
||||
if (body == null || body.length == 0) {
|
||||
bodyStr = "[无]";
|
||||
} else if (isMultipartContent(contentType)) {
|
||||
bodyStr = parseMultipartBody(body, contentType);
|
||||
} else if (isBinaryContent(contentType)) {
|
||||
bodyStr = "[二进制数据,长度=" + body.length + "字节]";
|
||||
} else {
|
||||
bodyStr = new String(body, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
String queryParams = extractQueryParams(url);
|
||||
if ("GET".equalsIgnoreCase(method) && !queryParams.isEmpty()) {
|
||||
log.info("===> [{}] {} {} | Headers: {} | Params: {} | Body: {}", configKey, method, url, headers, queryParams, flattenBody(bodyStr));
|
||||
} else {
|
||||
log.info("===> [{}] {} {} | Headers: {} | Body: {}", configKey, method, url, headers, flattenBody(bodyStr));
|
||||
}
|
||||
}
|
||||
|
||||
private String extractQueryParams(String url) {
|
||||
int queryStart = url.indexOf('?');
|
||||
if (queryStart < 0 || queryStart >= url.length() - 1) {
|
||||
return "";
|
||||
}
|
||||
String queryString = url.substring(queryStart + 1);
|
||||
Map<String, String> params = new LinkedHashMap<>();
|
||||
for (String pair : queryString.split("&")) {
|
||||
int eqPos = pair.indexOf('=');
|
||||
if (eqPos > 0) {
|
||||
String key = URLDecoder.decode(pair.substring(0, eqPos), StandardCharsets.UTF_8);
|
||||
String value = URLDecoder.decode(pair.substring(eqPos + 1), StandardCharsets.UTF_8);
|
||||
params.put(key, value);
|
||||
} else if (!pair.isEmpty()) {
|
||||
params.put(URLDecoder.decode(pair, StandardCharsets.UTF_8), "");
|
||||
}
|
||||
}
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long millis) throws IOException {
|
||||
String url = response.request().url();
|
||||
int status = response.status();
|
||||
Map<String, Collection<String>> headers = response.headers();
|
||||
|
||||
if (response.body() == null) {
|
||||
log.info("<==== [{}] {} | Status: {} | Duration: {}ms | Body: [空响应]", configKey, url, status, millis);
|
||||
return response;
|
||||
}
|
||||
|
||||
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
|
||||
String bodyStr;
|
||||
if (bodyData.length == 0) {
|
||||
bodyStr = "[空响应]";
|
||||
} else if (isBinaryContent(getContentType(headers))) {
|
||||
bodyStr = "[二进制数据]";
|
||||
} else {
|
||||
bodyStr = new String(bodyData, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
log.info("<==== [{}] {} | Status: {} | Duration: {}ms | Body: {}", configKey, url, status, millis, flattenBody(bodyStr));
|
||||
|
||||
return response.toBuilder().body(bodyData).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将报文内容压缩为单行显示(换行、回车、制表符等空白序列替换为单个空格)
|
||||
*/
|
||||
private String flattenBody(String bodyStr) {
|
||||
if (bodyStr == null) {
|
||||
return null;
|
||||
}
|
||||
return bodyStr.replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Content-Type
|
||||
*/
|
||||
private String getContentType(Map<String, Collection<String>> headers) {
|
||||
Collection<String> contentTypes = headers.get("Content-Type");
|
||||
if (contentTypes != null && !contentTypes.isEmpty()) {
|
||||
return contentTypes.iterator().next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为二进制内容
|
||||
*/
|
||||
private boolean isBinaryContent(String contentType) {
|
||||
if (contentType == null || contentType.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String lowerContentType = contentType.toLowerCase();
|
||||
return lowerContentType.startsWith(BINARY_CONTENT_TYPE_PREFIX)
|
||||
|| lowerContentType.startsWith(IMAGE_CONTENT_TYPE_PREFIX)
|
||||
|| lowerContentType.startsWith(AUDIO_CONTENT_TYPE_PREFIX)
|
||||
|| lowerContentType.startsWith(VIDEO_CONTENT_TYPE_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为multipart内容
|
||||
*/
|
||||
private boolean isMultipartContent(String contentType) {
|
||||
return contentType != null && contentType.toLowerCase().startsWith(MULTIPART_CONTENT_TYPE_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析multipart请求体,文件部分用占位符替代,文本字段保留打印
|
||||
*/
|
||||
private String parseMultipartBody(byte[] body, String contentType) {
|
||||
String boundary = extractBoundary(contentType);
|
||||
if (boundary == null) {
|
||||
return "[multipart数据,长度=" + body.length + "字节]";
|
||||
}
|
||||
String bodyStr = new String(body, StandardCharsets.UTF_8);
|
||||
String delimiter = "--" + boundary;
|
||||
String[] parts = bodyStr.split(delimiter);
|
||||
|
||||
List<String> fields = new ArrayList<>();
|
||||
for (String part : parts) {
|
||||
if (part.isEmpty() || part.startsWith("--") || part.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int headerEnd = part.indexOf("\r\n\r\n");
|
||||
if (headerEnd < 0) {
|
||||
continue;
|
||||
}
|
||||
String headers = part.substring(0, headerEnd);
|
||||
String value = part.substring(headerEnd + 4);
|
||||
if (value.endsWith("\r\n")) {
|
||||
value = value.substring(0, value.length() - 2);
|
||||
}
|
||||
|
||||
String name = extractParam(headers, "name");
|
||||
|
||||
if (headers.contains("filename=")) {
|
||||
String filename = extractParam(headers, "filename");
|
||||
fields.add(name + "=[文件: " + filename + "]");
|
||||
} else {
|
||||
fields.add(name + "=" + value);
|
||||
}
|
||||
}
|
||||
return fields.isEmpty() ? "[multipart数据]" : "{" + String.join(", ", fields) + "}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Content-Type中提取boundary
|
||||
*/
|
||||
private String extractBoundary(String contentType) {
|
||||
for (String param : contentType.split(";")) {
|
||||
param = param.trim();
|
||||
if (param.toLowerCase().startsWith("boundary=")) {
|
||||
return param.substring("boundary=".length()).trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从headers中提取参数值(如name、filename)
|
||||
*/
|
||||
private String extractParam(String headers, String paramName) {
|
||||
Pattern pattern = Pattern.compile(paramName + "=\"([^\"]*)\"");
|
||||
Matcher matcher = pattern.matcher(headers);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return paramName;
|
||||
}
|
||||
}
|
||||
+18
@@ -73,7 +73,9 @@ public interface ISysClient {
|
||||
String REGION = API_PREFIX + "/region";
|
||||
String FEE_ITEMS = API_PREFIX + "/fee-items";
|
||||
String CARGO_TYPES = API_PREFIX + "/cargo-types";
|
||||
String MEASUREMENT_UNITS = API_PREFIX + "/measurement-units";
|
||||
String PERMISSIONS = API_PREFIX + "/permissions";
|
||||
String INVOICE_ITEMS = API_PREFIX + "/invoice-items";
|
||||
|
||||
/**
|
||||
* 获取菜单
|
||||
@@ -317,6 +319,14 @@ public interface ISysClient {
|
||||
@GetMapping(CARGO_TYPES)
|
||||
R<List<CargoType>> getCargoTypes();
|
||||
|
||||
/**
|
||||
* 获取计量单位档案(金蝶推送按计费单位名称匹配取计量单位编码)
|
||||
*
|
||||
* @return 计量单位集合
|
||||
*/
|
||||
@GetMapping(MEASUREMENT_UNITS)
|
||||
R<List<MeasurementUnit>> getMeasurementUnits();
|
||||
|
||||
/**
|
||||
* 获取角色权限标识集合(按钮编号)
|
||||
*
|
||||
@@ -326,4 +336,12 @@ public interface ISysClient {
|
||||
@GetMapping(PERMISSIONS)
|
||||
R<List<String>> getPermissions(@RequestParam("roleId") String roleId);
|
||||
|
||||
/**
|
||||
* 获取启用的开票项目(含税收分类编码,开票申请推送金蝶按分类+简称匹配取数)
|
||||
*
|
||||
* @return 开票项目集合
|
||||
*/
|
||||
@GetMapping(INVOICE_ITEMS)
|
||||
R<List<InvoiceItem>> getInvoiceItems();
|
||||
|
||||
}
|
||||
|
||||
+10
@@ -174,9 +174,19 @@ public class ISysClientFallback implements ISysClient {
|
||||
return R.fail("获取数据失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<List<MeasurementUnit>> getMeasurementUnits() {
|
||||
return R.fail("获取数据失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<List<String>> getPermissions(String roleId) {
|
||||
return R.fail("获取数据失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<List<InvoiceItem>> getInvoiceItems() {
|
||||
return R.fail("获取数据失败");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ public class InvoiceApplicationSaveRequest implements Serializable {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
private String goodsCategory;
|
||||
private String goodsName;
|
||||
private String taxClassificationCode;
|
||||
private String unit;
|
||||
private BigDecimal quantity;
|
||||
private BigDecimal unitPriceNoTax;
|
||||
|
||||
-3
@@ -42,7 +42,6 @@ import java.util.List;
|
||||
public class InvoiceReceiptSaveRequest implements Serializable {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
private Long id;
|
||||
private Long kingdeeInvoicePoolId;
|
||||
private String invoiceNo;
|
||||
private LocalDate invoiceDate;
|
||||
private String invoiceType;
|
||||
@@ -54,8 +53,6 @@ public class InvoiceReceiptSaveRequest implements Serializable {
|
||||
private String bankName;
|
||||
private String bankAccount;
|
||||
private String issuingBank;
|
||||
private String kingdeeBillNo;
|
||||
private String kingdeeStatus;
|
||||
private String phone;
|
||||
private String customerEmails;
|
||||
private String departmentEmails;
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 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.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 金蝶票据池镜像统一查询条件(收票登记选票与正式结算单认领共用,查本地镜像)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "金蝶票据池镜像统一查询条件")
|
||||
public class KingdeeInvoicePoolQueryDTO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 当前页
|
||||
*/
|
||||
@Schema(description = "当前页")
|
||||
private Long current = 1L;
|
||||
|
||||
/**
|
||||
* 每页条数
|
||||
*/
|
||||
@Schema(description = "每页条数")
|
||||
private Long size = 10L;
|
||||
|
||||
/**
|
||||
* 发票号码,模糊
|
||||
*/
|
||||
@Schema(description = "发票号码(模糊)")
|
||||
private String invoiceNo;
|
||||
|
||||
/**
|
||||
* 开票日期起 yyyy-MM-dd
|
||||
*/
|
||||
@Schema(description = "开票日期起 yyyy-MM-dd")
|
||||
private String invoiceDateStart;
|
||||
|
||||
/**
|
||||
* 开票日期止 yyyy-MM-dd
|
||||
*/
|
||||
@Schema(description = "开票日期止 yyyy-MM-dd")
|
||||
private String invoiceDateEnd;
|
||||
|
||||
/**
|
||||
* 开票单位(销方名称),模糊
|
||||
*/
|
||||
@Schema(description = "开票单位(销方名称,模糊)")
|
||||
private String salerName;
|
||||
|
||||
/**
|
||||
* 受票单位(购方名称),模糊
|
||||
*/
|
||||
@Schema(description = "受票单位(购方名称,模糊)")
|
||||
private String buyerName;
|
||||
|
||||
/**
|
||||
* 发票类型编码(票易通字典,如 26/27,镜像按 invoice_type_code 精确匹配)
|
||||
*/
|
||||
@Schema(description = "发票类型编码(票易通字典,如 26/27)")
|
||||
private String invoiceType;
|
||||
|
||||
/**
|
||||
* 销方税号,精确
|
||||
*/
|
||||
@Schema(description = "销方税号")
|
||||
private String salerTaxNo;
|
||||
|
||||
/**
|
||||
* 购方税号,精确
|
||||
*/
|
||||
@Schema(description = "购方税号")
|
||||
private String buyerTaxNo;
|
||||
|
||||
/**
|
||||
* 认领过滤:true 仅已认领,false 仅未认领,null 不过滤(已认领=存在非作废正式结算单发票行)
|
||||
*/
|
||||
@Schema(description = "认领过滤:true已认领/false未认领/null不限")
|
||||
private Boolean claimed;
|
||||
|
||||
/**
|
||||
* 登记过滤:true 仅已登记,false 仅未登记,null 不过滤(已登记=存在非作废收票登记)
|
||||
*/
|
||||
@Schema(description = "登记过滤:true已登记/false未登记/null不限")
|
||||
private Boolean registered;
|
||||
}
|
||||
+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
|
||||
* 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.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 收款认领推送金蝶结果(A301-3)
|
||||
* <p>
|
||||
* 推送失败不抛异常跨事务边界(保证失败状态落库不被回滚),以本结果对象向调用方报告成败与原因。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "收款认领推送金蝶结果")
|
||||
public class ReceiptClaimPushResult implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "是否推送成功")
|
||||
private boolean success;
|
||||
|
||||
@Schema(description = "金蝶收款单号(成功时返回,可能为空:金蝶响应未返回单号)")
|
||||
private String kingdeeBillNo;
|
||||
|
||||
@Schema(description = "失败原因(成功时为空)")
|
||||
private String message;
|
||||
|
||||
public static ReceiptClaimPushResult success(String kingdeeBillNo) {
|
||||
ReceiptClaimPushResult result = new ReceiptClaimPushResult();
|
||||
result.setSuccess(true);
|
||||
result.setKingdeeBillNo(kingdeeBillNo);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static ReceiptClaimPushResult fail(String message) {
|
||||
ReceiptClaimPushResult result = new ReceiptClaimPushResult();
|
||||
result.setSuccess(false);
|
||||
result.setMessage(message);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+9
@@ -161,4 +161,13 @@ public class CustomerArchive extends TenantEntity {
|
||||
@Schema(description = "审核通过时间")
|
||||
private LocalDateTime approvedTime;
|
||||
|
||||
@Schema(description = "金蝶协同状态:none未推送/exist金蝶已有/synced已推送/failed推送失败")
|
||||
private String kingdeeStatus;
|
||||
|
||||
@Schema(description = "金蝶推送失败原因")
|
||||
private String kingdeeFailReason;
|
||||
|
||||
@Schema(description = "金蝶推送时间")
|
||||
private LocalDateTime kingdeeSyncTime;
|
||||
|
||||
}
|
||||
|
||||
+108
@@ -21,6 +21,8 @@ import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 正式结算单实体类
|
||||
* <p>
|
||||
* 结算单主表,明细快照见 {@link FormalSettlementDetail},合计费用见 {@link FormalSettlementSummaryFee}
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@@ -30,38 +32,144 @@ import java.time.LocalDateTime;
|
||||
@Schema(description = "正式结算单")
|
||||
public class FormalSettlement extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 正式结算单号(同租户唯一)
|
||||
*/
|
||||
private String formalSettlementNo;
|
||||
/**
|
||||
* 来源类型:预结算合并/应收应付/混合来源
|
||||
*/
|
||||
private String sourceType;
|
||||
/**
|
||||
* 结算类型:receivable-应收 payable-应付
|
||||
*/
|
||||
private String settlementType;
|
||||
/**
|
||||
* 所属项目ID
|
||||
*/
|
||||
private Long projectId;
|
||||
/**
|
||||
* 所属项目名称
|
||||
*/
|
||||
private String projectName;
|
||||
/**
|
||||
* 所属组织ID
|
||||
*/
|
||||
private Long deptId;
|
||||
/**
|
||||
* 所属组织名称
|
||||
*/
|
||||
private String deptName;
|
||||
/**
|
||||
* 结算合同ID
|
||||
*/
|
||||
private Long contractId;
|
||||
/**
|
||||
* 结算合同编号
|
||||
*/
|
||||
private String contractNo;
|
||||
/**
|
||||
* 结算合同名称
|
||||
*/
|
||||
private String contractName;
|
||||
/**
|
||||
* 付款方名称
|
||||
*/
|
||||
private String payerName;
|
||||
/**
|
||||
* 收款方名称
|
||||
*/
|
||||
private String payeeName;
|
||||
/**
|
||||
* 结算币种,默认 RMB
|
||||
*/
|
||||
private String currency;
|
||||
/**
|
||||
* 结算金额(结算币种,含全部合计费用)
|
||||
*/
|
||||
private BigDecimal settlementAmount;
|
||||
/**
|
||||
* 本位币,默认 RMB
|
||||
*/
|
||||
private String localCurrency;
|
||||
/**
|
||||
* 本位币结算金额 = 结算金额 × 结算汇率
|
||||
*/
|
||||
private BigDecimal localSettlementAmount;
|
||||
/**
|
||||
* 已申请付款金额,含来源预结算的预付
|
||||
*/
|
||||
private BigDecimal appliedPaymentAmount;
|
||||
/**
|
||||
* 已收/已付金额合计,含来源预结算的预付
|
||||
*/
|
||||
private BigDecimal paidAmount;
|
||||
/**
|
||||
* 剩余可付金额 = 结算金额 - 已收已付合计,最小为 0
|
||||
*/
|
||||
private BigDecimal remainingPayableAmount;
|
||||
/**
|
||||
* 已开票/已收票金额,取开票申请与收票登记的分摊合计
|
||||
*/
|
||||
private BigDecimal invoiceAmount;
|
||||
/**
|
||||
* 汇率日期
|
||||
*/
|
||||
private LocalDate exchangeRateDate;
|
||||
/**
|
||||
* 结算汇率
|
||||
*/
|
||||
private BigDecimal exchangeRate;
|
||||
/**
|
||||
* 开票状态(应收为开票、应付为收票):unreceived-未开票 partial-部分开票 completed-已开票
|
||||
*/
|
||||
private String invoiceStatus;
|
||||
/**
|
||||
* 收付款状态:unpaid-未收付款 partial-部分收付款 paid-已收付款
|
||||
*/
|
||||
private String paymentStatus;
|
||||
/**
|
||||
* 审核状态:draft-草稿 reviewing-审批中 approved-审批通过 returned-已驳回 voided-已作废
|
||||
*/
|
||||
private String approvalStatus;
|
||||
/**
|
||||
* 当前审批节点,如「草稿」「财务审核」「审批通过」
|
||||
*/
|
||||
private String currentNode;
|
||||
/**
|
||||
* 当前处理人姓名
|
||||
*/
|
||||
private String currentProcessor;
|
||||
/**
|
||||
* 金蝶单据编号
|
||||
*/
|
||||
private String kingdeeBillNo;
|
||||
/**
|
||||
* 金蝶单据id(星瀚内部主键,A902 附件上传定位用)
|
||||
*/
|
||||
private String kingdeeBillId;
|
||||
/**
|
||||
* 金蝶同步状态:unsynced-未同步 synced-已同步 failed-同步失败
|
||||
*/
|
||||
private String kingdeeSyncStatus;
|
||||
/**
|
||||
* 附件JSON
|
||||
*/
|
||||
private String attachmentsJson;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
/**
|
||||
* 审核通过时间
|
||||
*/
|
||||
private LocalDateTime approvedTime;
|
||||
/**
|
||||
* 同步金蝶时间
|
||||
*/
|
||||
private LocalDateTime syncedTime;
|
||||
/**
|
||||
* 作废原因,驳回时记录驳回原因
|
||||
*/
|
||||
private String voidReason;
|
||||
}
|
||||
|
||||
+99
@@ -27,37 +27,136 @@ import java.time.LocalDateTime;
|
||||
@TableName("blade_formal_settlement_detail")
|
||||
public class FormalSettlementDetail extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 正式结算单ID
|
||||
*/
|
||||
private Long formalSettlementId;
|
||||
/**
|
||||
* 来源预结算单ID
|
||||
*/
|
||||
private Long sourcePreSettlementId;
|
||||
/**
|
||||
* 来源预结算明细ID
|
||||
*/
|
||||
private Long sourcePreSettlementDetailId;
|
||||
/**
|
||||
* 来源应收应付明细ID
|
||||
*/
|
||||
private Long sourceDetailId;
|
||||
/**
|
||||
* 行号
|
||||
*/
|
||||
private Integer lineNo;
|
||||
/**
|
||||
* 单据编号
|
||||
*/
|
||||
private String documentNo;
|
||||
/**
|
||||
* 运单ID
|
||||
*/
|
||||
private Long waybillId;
|
||||
/**
|
||||
* 运单号
|
||||
*/
|
||||
private String waybillNo;
|
||||
/**
|
||||
* 车牌号
|
||||
*/
|
||||
private String vehicleNo;
|
||||
/**
|
||||
* 发货地址
|
||||
*/
|
||||
private String departureAddress;
|
||||
/**
|
||||
* 收货地址
|
||||
*/
|
||||
private String arrivalAddress;
|
||||
/**
|
||||
* 发货联系人
|
||||
*/
|
||||
private String departureContact;
|
||||
/**
|
||||
* 发货联系方式
|
||||
*/
|
||||
private String departurePhone;
|
||||
/**
|
||||
* 收货联系人
|
||||
*/
|
||||
private String arrivalContact;
|
||||
/**
|
||||
* 收货联系方式
|
||||
*/
|
||||
private String arrivalPhone;
|
||||
/**
|
||||
* 实际发货时间
|
||||
*/
|
||||
private LocalDateTime actualDepartureTime;
|
||||
/**
|
||||
* 实际完成时间
|
||||
*/
|
||||
private LocalDateTime actualCompletionTime;
|
||||
/**
|
||||
* 运输类型
|
||||
*/
|
||||
private String transportType;
|
||||
/**
|
||||
* 货物名称
|
||||
*/
|
||||
private String cargoName;
|
||||
/**
|
||||
* 货物类型
|
||||
*/
|
||||
private String cargoType;
|
||||
/**
|
||||
* 运输总量
|
||||
*/
|
||||
private BigDecimal transportQuantity;
|
||||
/**
|
||||
* 数量单位
|
||||
*/
|
||||
private String quantityUnit;
|
||||
/**
|
||||
* 里程
|
||||
*/
|
||||
private BigDecimal mileage;
|
||||
/**
|
||||
* 批次号
|
||||
*/
|
||||
private String batchNo;
|
||||
/**
|
||||
* 运输单价
|
||||
*/
|
||||
private BigDecimal unitPrice;
|
||||
/**
|
||||
* 运费
|
||||
*/
|
||||
private BigDecimal freightAmount;
|
||||
/**
|
||||
* 费用项JSON
|
||||
*/
|
||||
private String feeItemsJson;
|
||||
/**
|
||||
* 原金额
|
||||
*/
|
||||
private BigDecimal originalAmount;
|
||||
/**
|
||||
* 调整金额
|
||||
*/
|
||||
private BigDecimal adjustAmount;
|
||||
/**
|
||||
* 结算金额(含税)
|
||||
*/
|
||||
private BigDecimal settlementAmountTax;
|
||||
/**
|
||||
* 结算金额(不含税)
|
||||
*/
|
||||
private BigDecimal settlementAmountNoTax;
|
||||
/**
|
||||
* 币种
|
||||
*/
|
||||
private String currency;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
}
|
||||
|
||||
+48
@@ -26,20 +26,68 @@ import java.math.BigDecimal;
|
||||
@TableName("blade_formal_settlement_detail_fee")
|
||||
public class FormalSettlementDetailFee extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 正式结算明细ID
|
||||
*/
|
||||
private Long formalSettlementDetailId;
|
||||
/**
|
||||
* 源费用行ID
|
||||
*/
|
||||
private Long sourceFeeId;
|
||||
/**
|
||||
* 行号
|
||||
*/
|
||||
private String lineNo;
|
||||
/**
|
||||
* 货物名称
|
||||
*/
|
||||
private String cargoName;
|
||||
/**
|
||||
* 货物类型
|
||||
*/
|
||||
private String cargoType;
|
||||
/**
|
||||
* 运输量
|
||||
*/
|
||||
private BigDecimal transportQuantity;
|
||||
/**
|
||||
* 数量单位
|
||||
*/
|
||||
private String quantityUnit;
|
||||
/**
|
||||
* 里程
|
||||
*/
|
||||
private BigDecimal mileage;
|
||||
/**
|
||||
* 运输单价
|
||||
*/
|
||||
private BigDecimal unitPrice;
|
||||
/**
|
||||
* 运费
|
||||
*/
|
||||
private BigDecimal freightAmount;
|
||||
/**
|
||||
* 费用项JSON
|
||||
*/
|
||||
private String feeItemsJson;
|
||||
/**
|
||||
* 原金额
|
||||
*/
|
||||
private BigDecimal originalAmount;
|
||||
/**
|
||||
* 调整金额
|
||||
*/
|
||||
private BigDecimal adjustAmount;
|
||||
/**
|
||||
* 结算金额(含税)
|
||||
*/
|
||||
private BigDecimal settlementAmountTax;
|
||||
/**
|
||||
* 结算金额(不含税)
|
||||
*/
|
||||
private BigDecimal settlementAmountNoTax;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
}
|
||||
|
||||
+8
@@ -26,12 +26,20 @@ import java.math.BigDecimal;
|
||||
@TableName("blade_formal_settlement_payment")
|
||||
public class FormalSettlementPayment extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/** 正式结算单id */
|
||||
private Long formalSettlementId;
|
||||
/** 付款单号(租户内唯一) */
|
||||
private String paymentNo;
|
||||
/** 付款类型(final:结算尾款) */
|
||||
private String paymentType;
|
||||
/** 申请付款金额 */
|
||||
private BigDecimal appliedAmount;
|
||||
/** 已付金额 */
|
||||
private BigDecimal paidAmount;
|
||||
/** 单据状态(reviewing:审核中,approved:审核通过,returned:已退回,voided:已作废,paid:已付款) */
|
||||
private String billStatus;
|
||||
/** 金蝶(星瀚)单据编号 */
|
||||
private String kingdeeBillNo;
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
|
||||
+27
@@ -28,13 +28,40 @@ import java.math.BigDecimal;
|
||||
@Schema(description = "正式结算合计费用")
|
||||
public class FormalSettlementSummaryFee extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 正式结算单ID
|
||||
*/
|
||||
private Long formalSettlementId;
|
||||
/**
|
||||
* 行号
|
||||
*/
|
||||
private Integer lineNo;
|
||||
/**
|
||||
* 费用类型
|
||||
*/
|
||||
private String feeType;
|
||||
/**
|
||||
* 费用项
|
||||
*/
|
||||
private String feeItem;
|
||||
/**
|
||||
* 原金额
|
||||
*/
|
||||
private BigDecimal originalAmount;
|
||||
/**
|
||||
* 调整金额
|
||||
*/
|
||||
private BigDecimal adjustAmount;
|
||||
/**
|
||||
* 结算金额
|
||||
*/
|
||||
private BigDecimal settlementAmount;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
/**
|
||||
* 是否手工添加
|
||||
*/
|
||||
private Integer manualFlag;
|
||||
}
|
||||
|
||||
+103
@@ -47,37 +47,140 @@ import java.time.LocalDateTime;
|
||||
@Schema(description = "开票申请")
|
||||
public class InvoiceApplication extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 开票申请单号(同租户唯一,A303-1 billno 幂等候选键)
|
||||
*/
|
||||
private String applicationNo;
|
||||
/**
|
||||
* 所属项目ID(结算单快照带出)
|
||||
*/
|
||||
private Long projectId;
|
||||
/**
|
||||
* 所属项目名称(结算单快照带出)
|
||||
*/
|
||||
private String projectName;
|
||||
/**
|
||||
* 申请部门(所属组织)ID
|
||||
*/
|
||||
private Long deptId;
|
||||
/**
|
||||
* 申请部门(所属组织)名称
|
||||
*/
|
||||
private String deptName;
|
||||
/**
|
||||
* 开票单位名称(销方=结算单收款方快照,金蝶 salername 取数来源)
|
||||
*/
|
||||
private String issuerName;
|
||||
/**
|
||||
* 受票方客商档案ID
|
||||
*/
|
||||
private Long receiverCustomerId;
|
||||
/**
|
||||
* 受票方单位名称(客商开票信息企业全称快照,金蝶 buyername)
|
||||
*/
|
||||
private String receiverName;
|
||||
/**
|
||||
* 发票类型:electronic_normal-数电普票 / electronic_special-数电专票
|
||||
*/
|
||||
private String invoiceType;
|
||||
/**
|
||||
* 可开票金额(关联结算单剩余可开票额度合计)
|
||||
*/
|
||||
private BigDecimal availableInvoiceAmount;
|
||||
/**
|
||||
* 本次开票金额(商品行价税合计,金蝶 totalamount)
|
||||
*/
|
||||
private BigDecimal invoiceAmount;
|
||||
/**
|
||||
* 申请日期(提交审批时置当日)
|
||||
*/
|
||||
private LocalDate applicationDate;
|
||||
/**
|
||||
* 申请人姓名
|
||||
*/
|
||||
private String applicantName;
|
||||
/**
|
||||
* 承办部门ID(结算单所属组织快照)
|
||||
*/
|
||||
private Long undertakingDeptId;
|
||||
/**
|
||||
* 承办部门名称(结算单所属组织快照)
|
||||
*/
|
||||
private String undertakingDeptName;
|
||||
/**
|
||||
* 部门邮箱(发票推送邮箱,多个以分号分隔,最多3个,金蝶 buyeremail)
|
||||
*/
|
||||
private String departmentEmails;
|
||||
/**
|
||||
* 受票方客商开票信息ID(blade_customer_invoice_info)
|
||||
*/
|
||||
private Long receiverInvoiceInfoId;
|
||||
/**
|
||||
* 受票方纳税人识别号(开票信息快照,金蝶 buyertaxno)
|
||||
*/
|
||||
private String taxpayerNo;
|
||||
/**
|
||||
* 受票方开户行(开票信息快照)
|
||||
*/
|
||||
private String bankName;
|
||||
/**
|
||||
* 受票方开户账号(开票信息快照)
|
||||
*/
|
||||
private String bankAccount;
|
||||
/**
|
||||
* 受票方注册地址(开票信息快照)
|
||||
*/
|
||||
private String registeredAddress;
|
||||
/**
|
||||
* 受票方联系人
|
||||
*/
|
||||
private String contactName;
|
||||
/**
|
||||
* 受票方联系电话
|
||||
*/
|
||||
private String contactPhone;
|
||||
/**
|
||||
* 受票方邮箱
|
||||
*/
|
||||
private String email;
|
||||
/**
|
||||
* 审批状态:draft-草稿 reviewing-审批中 approved-审批通过 returned-已驳回 voided-已作废
|
||||
*/
|
||||
private String approvalStatus;
|
||||
/**
|
||||
* 当前审批节点,如「草稿」「开票审核」「审批通过」
|
||||
*/
|
||||
private String currentNode;
|
||||
/**
|
||||
* 当前处理人姓名
|
||||
*/
|
||||
private String currentProcessor;
|
||||
/**
|
||||
* 金蝶开票申请单编号(A303-1 返回,A303-5 查询 serialNo)
|
||||
*/
|
||||
private String kingdeeBillNo;
|
||||
/**
|
||||
* 金蝶开票申请单id(星瀚内部主键,A902 附件上传定位用)
|
||||
*/
|
||||
private String kingdeeBillId;
|
||||
/**
|
||||
* 金蝶同步状态:unsynced-未同步 synced-已同步 failed-同步失败 invoiced-开票完成(终态)
|
||||
*/
|
||||
private String kingdeeStatus;
|
||||
/**
|
||||
* 同步金蝶时间
|
||||
*/
|
||||
private LocalDateTime syncedTime;
|
||||
/**
|
||||
* 附件JSON
|
||||
*/
|
||||
private String attachmentsJson;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
/**
|
||||
* 作废原因,驳回时记录驳回原因
|
||||
*/
|
||||
private String voidReason;
|
||||
}
|
||||
|
||||
+63
@@ -44,25 +44,88 @@ import java.time.LocalDateTime;
|
||||
@TableName("blade_invoice_application_detail")
|
||||
public class InvoiceApplicationDetail extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 开票申请ID
|
||||
*/
|
||||
private Long invoiceApplicationId;
|
||||
/**
|
||||
* 正式结算单ID
|
||||
*/
|
||||
private Long formalSettlementId;
|
||||
/**
|
||||
* 来源正式结算明细ID
|
||||
*/
|
||||
private Long formalSettlementDetailId;
|
||||
/**
|
||||
* 行号
|
||||
*/
|
||||
private Integer lineNo;
|
||||
/**
|
||||
* 结算明细单据号(快照)
|
||||
*/
|
||||
private String documentNo;
|
||||
/**
|
||||
* 运单号(快照)
|
||||
*/
|
||||
private String waybillNo;
|
||||
/**
|
||||
* 车牌号(快照)
|
||||
*/
|
||||
private String vehicleNo;
|
||||
/**
|
||||
* 起运地(快照)
|
||||
*/
|
||||
private String departureAddress;
|
||||
/**
|
||||
* 到达地(快照)
|
||||
*/
|
||||
private String arrivalAddress;
|
||||
/**
|
||||
* 实际发运时间(快照)
|
||||
*/
|
||||
private LocalDateTime actualDepartureTime;
|
||||
/**
|
||||
* 实际完成时间(快照)
|
||||
*/
|
||||
private LocalDateTime actualCompletionTime;
|
||||
/**
|
||||
* 运输方式(快照,字典 transport_type)
|
||||
*/
|
||||
private String transportType;
|
||||
/**
|
||||
* 货物名称(快照)
|
||||
*/
|
||||
private String cargoName;
|
||||
/**
|
||||
* 货物类型(快照)
|
||||
*/
|
||||
private String cargoType;
|
||||
/**
|
||||
* 运输数量(快照)
|
||||
*/
|
||||
private BigDecimal transportQuantity;
|
||||
/**
|
||||
* 数量单位(快照)
|
||||
*/
|
||||
private String quantityUnit;
|
||||
/**
|
||||
* 里程(快照)
|
||||
*/
|
||||
private BigDecimal mileage;
|
||||
/**
|
||||
* 批次号(快照)
|
||||
*/
|
||||
private String batchNo;
|
||||
/**
|
||||
* 运费金额(快照)
|
||||
*/
|
||||
private BigDecimal freightAmount;
|
||||
/**
|
||||
* 费用项明细JSON(快照)
|
||||
*/
|
||||
private String feeItemsJson;
|
||||
/**
|
||||
* 结算金额(含税,快照)
|
||||
*/
|
||||
private BigDecimal settlementAmountTax;
|
||||
}
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
|
||||
* Copyright of this software remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.core.tenant.mp.TenantEntity;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 开票申请金蝶发票结果实体类
|
||||
* <p>
|
||||
* 定时任务经 A303-5 开票申请发票查询回写的发票明细(发票号唯一键幂等落库),
|
||||
* 发票文件地址(板式文件/数电PDF/XML/预览图)独立落库供详情页查看下载,
|
||||
* 原文 JSON 整体留档便于联调与追溯
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("blade_invoice_application_invoice")
|
||||
@Schema(description = "开票申请金蝶发票结果")
|
||||
public class InvoiceApplicationInvoice extends TenantEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "开票申请ID")
|
||||
private Long invoiceApplicationId;
|
||||
|
||||
@Schema(description = "金蝶开票申请单编号(A303-5 serialNo)")
|
||||
private String serialNo;
|
||||
|
||||
@Schema(description = "发票号码")
|
||||
private String invoiceNo;
|
||||
|
||||
@Schema(description = "开票日期")
|
||||
private LocalDateTime invoiceDate;
|
||||
|
||||
@Schema(description = "价税合计")
|
||||
private BigDecimal invoiceAmount;
|
||||
|
||||
@Schema(description = "板式文件下载地址(数电票为OFD,航信百望税盘普通电票为PDF,电子专票/UKey电票为OFD)")
|
||||
private String invoiceFileUrl;
|
||||
|
||||
@Schema(description = "PDF图片预览地址")
|
||||
private String invoiceImageUrl;
|
||||
|
||||
@Schema(description = "数电PDF下载地址(发票云5.0.025版本返回)")
|
||||
private String invoicePdfFileUrl;
|
||||
|
||||
@Schema(description = "数电XML下载地址(发票云5.0.025版本返回)")
|
||||
private String invoiceXmlFileUrl;
|
||||
|
||||
@Schema(description = "A303-5 返回原文JSON留档")
|
||||
private String rawJson;
|
||||
|
||||
}
|
||||
+46
@@ -43,18 +43,64 @@ import java.math.BigDecimal;
|
||||
@TableName("blade_invoice_application_line")
|
||||
public class InvoiceApplicationLine extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 开票申请ID
|
||||
*/
|
||||
private Long invoiceApplicationId;
|
||||
/**
|
||||
* 所属发票张次ID
|
||||
*/
|
||||
private Long invoiceSheetId;
|
||||
/**
|
||||
* 行号(张内序号)
|
||||
*/
|
||||
private Integer lineNo;
|
||||
/**
|
||||
* 商品和服务分类名称(开票项目主数据选择,金蝶 goodssimplename)
|
||||
*/
|
||||
private String goodsCategory;
|
||||
/**
|
||||
* 货物或服务简称(开票项目主数据选择,金蝶 goodsname)
|
||||
*/
|
||||
private String goodsName;
|
||||
/**
|
||||
* 税收分类编码(选择开票内容时快照,推送金蝶 A303-1 分录直接取该值)
|
||||
*/
|
||||
private String taxClassificationCode;
|
||||
/**
|
||||
* 单位
|
||||
*/
|
||||
private String unit;
|
||||
/**
|
||||
* 数量
|
||||
*/
|
||||
private BigDecimal quantity;
|
||||
/**
|
||||
* 不含税单价
|
||||
*/
|
||||
private BigDecimal unitPriceNoTax;
|
||||
/**
|
||||
* 不含税金额
|
||||
*/
|
||||
private BigDecimal amountNoTax;
|
||||
/**
|
||||
* 含税金额(历史兼容字段,与含税合计同值)
|
||||
*/
|
||||
private BigDecimal amountWithTax;
|
||||
/**
|
||||
* 税率(0-100 百分比值,推送金蝶时转小数文本)
|
||||
*/
|
||||
private BigDecimal taxRate;
|
||||
/**
|
||||
* 税额
|
||||
*/
|
||||
private BigDecimal taxAmount;
|
||||
/**
|
||||
* 价税合计(含税合计,金蝶 taxamount)
|
||||
*/
|
||||
private BigDecimal totalAmount;
|
||||
/**
|
||||
* 行备注
|
||||
*/
|
||||
private String remark;
|
||||
}
|
||||
|
||||
+24
@@ -42,12 +42,36 @@ import java.io.Serial;
|
||||
@TableName("blade_invoice_application_record")
|
||||
public class InvoiceApplicationRecord extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 开票申请ID
|
||||
*/
|
||||
private Long invoiceApplicationId;
|
||||
/**
|
||||
* 操作类型:create/save/submit/approve/return/void/sync 等
|
||||
*/
|
||||
private String actionType;
|
||||
/**
|
||||
* 操作名称(展示用,如「提交审批」「同步金蝶」)
|
||||
*/
|
||||
private String actionName;
|
||||
/**
|
||||
* 操作前审批状态
|
||||
*/
|
||||
private String fromStatus;
|
||||
/**
|
||||
* 操作后审批状态
|
||||
*/
|
||||
private String toStatus;
|
||||
/**
|
||||
* 操作人姓名
|
||||
*/
|
||||
private String operatorName;
|
||||
/**
|
||||
* 原因说明(驳回/作废原因等)
|
||||
*/
|
||||
private String reason;
|
||||
/**
|
||||
* 金蝶单据编号(同步类操作记录)
|
||||
*/
|
||||
private String kingdeeBillNo;
|
||||
}
|
||||
|
||||
+18
@@ -43,10 +43,28 @@ import java.math.BigDecimal;
|
||||
@TableName("blade_invoice_application_settlement")
|
||||
public class InvoiceApplicationSettlement extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 开票申请ID
|
||||
*/
|
||||
private Long invoiceApplicationId;
|
||||
/**
|
||||
* 正式结算单ID
|
||||
*/
|
||||
private Long formalSettlementId;
|
||||
/**
|
||||
* 正式结算单号(关联时快照)
|
||||
*/
|
||||
private String formalSettlementNo;
|
||||
/**
|
||||
* 结算金额(关联时快照)
|
||||
*/
|
||||
private BigDecimal settlementAmount;
|
||||
/**
|
||||
* 可开票金额(关联时快照)
|
||||
*/
|
||||
private BigDecimal availableInvoiceAmount;
|
||||
/**
|
||||
* 本申请分摊开票金额(结算单开票状态按此汇总)
|
||||
*/
|
||||
private BigDecimal allocatedInvoiceAmount;
|
||||
}
|
||||
|
||||
+13
@@ -43,7 +43,20 @@ import java.math.BigDecimal;
|
||||
@TableName("blade_invoice_application_sheet")
|
||||
public class InvoiceApplicationSheet extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 开票申请ID
|
||||
*/
|
||||
private Long invoiceApplicationId;
|
||||
/**
|
||||
* 发票张序号(从 1 开始)
|
||||
*/
|
||||
private Integer sheetNo;
|
||||
/**
|
||||
* 本张开票金额(张内商品行价税合计)
|
||||
*/
|
||||
private BigDecimal invoiceAmount;
|
||||
/**
|
||||
* 金蝶开票申请单编号(本张发票对应,A303-1 返回,A303-5 查询 serialNo)
|
||||
*/
|
||||
private String kingdeeBillNo;
|
||||
}
|
||||
|
||||
+102
-2
@@ -36,6 +36,7 @@ import org.springblade.core.tenant.mp.TenantEntity;
|
||||
import java.io.Serial;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 收票登记实体类
|
||||
@@ -48,36 +49,135 @@ import java.time.LocalDate;
|
||||
@Schema(description = "收票登记")
|
||||
public class InvoiceReceipt extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 关联金蝶票据池镜像主键(blade_kingdee_invoice_pool.id)
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long kingdeeInvoicePoolId;
|
||||
/**
|
||||
* 发票号码(同租户唯一,一张发票仅可登记一次)
|
||||
*/
|
||||
private String invoiceNo;
|
||||
/**
|
||||
* 开票日期
|
||||
*/
|
||||
private LocalDate invoiceDate;
|
||||
/**
|
||||
* 发票类型(如「数电发票(增值税专用发票)」)
|
||||
*/
|
||||
private String invoiceType;
|
||||
/**
|
||||
* 税率(百分数,13 表示 13%)
|
||||
*/
|
||||
private BigDecimal taxRate;
|
||||
/**
|
||||
* 开票金额
|
||||
*/
|
||||
private BigDecimal invoiceAmount;
|
||||
/**
|
||||
* 税额
|
||||
*/
|
||||
private BigDecimal taxAmount;
|
||||
/**
|
||||
* 受票单位(购方)
|
||||
*/
|
||||
private String receiverName;
|
||||
/**
|
||||
* 开票单位(销方)
|
||||
*/
|
||||
private String issuerName;
|
||||
/**
|
||||
* 所属项目id
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long projectId;
|
||||
/**
|
||||
* 所属项目名称
|
||||
*/
|
||||
private String projectName;
|
||||
/**
|
||||
* 所属组织id
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long deptId;
|
||||
/**
|
||||
* 所属组织名称
|
||||
*/
|
||||
private String deptName;
|
||||
/**
|
||||
* 付款方
|
||||
*/
|
||||
private String payerName;
|
||||
/**
|
||||
* 收款方
|
||||
*/
|
||||
private String payeeName;
|
||||
/**
|
||||
* 开票单位开户银行
|
||||
*/
|
||||
private String bankName;
|
||||
/**
|
||||
* 开票单位银行账号
|
||||
*/
|
||||
private String bankAccount;
|
||||
/**
|
||||
* 开票单位开户网点
|
||||
*/
|
||||
private String issuingBank;
|
||||
/**
|
||||
* 联系电话
|
||||
*/
|
||||
private String phone;
|
||||
/**
|
||||
* 客户邮箱(分号分隔,最多 3 个)
|
||||
*/
|
||||
private String customerEmails;
|
||||
/**
|
||||
* 部门邮箱(分号分隔,最多 3 个)
|
||||
*/
|
||||
private String departmentEmails;
|
||||
/**
|
||||
* 审批状态(draft 草稿 / reviewing 审批中 / approved 审批通过 / returned 已驳回 / voided 已作废)
|
||||
*/
|
||||
private String approvalStatus;
|
||||
/**
|
||||
* 当前审批节点
|
||||
*/
|
||||
private String currentNode;
|
||||
/**
|
||||
* 当前处理人
|
||||
*/
|
||||
private String currentProcessor;
|
||||
private String kingdeeBillNo;
|
||||
private String kingdeeStatus;
|
||||
/**
|
||||
* 金蝶收票单单据编号(A303-2 批量保存生成/幂等预检命中回填)
|
||||
*/
|
||||
private String kingdeeReceivedInvoiceNo;
|
||||
/**
|
||||
* 金蝶收票单id(星瀚内部主键,A902 附件上传定位用)
|
||||
*/
|
||||
private String kingdeeReceivedInvoiceId;
|
||||
/**
|
||||
* 收票单推送状态(unsynced 未推送 / pushed 已保存收票单 / assigned 已指定应付 / failed 推送失败)
|
||||
*/
|
||||
private String kingdeePushStatus;
|
||||
/**
|
||||
* 收票单推送失败原因(金蝶侧删除/红冲/查验不通过等异常提示人工)
|
||||
*/
|
||||
private String kingdeePushMsg;
|
||||
/**
|
||||
* 最近一次收票单推送/回写时间
|
||||
*/
|
||||
private LocalDateTime kingdeePushTime;
|
||||
/**
|
||||
* 附件信息JSON
|
||||
*/
|
||||
private String attachmentsJson;
|
||||
/**
|
||||
* 备注(最多 200 字)
|
||||
*/
|
||||
private String remark;
|
||||
/**
|
||||
* 作废原因
|
||||
*/
|
||||
private String voidReason;
|
||||
}
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.core.tenant.mp.TenantEntity;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 金蝶附件同步实体类(A902 附件上传到指定单据,单附件行级状态)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("blade_kingdee_attachment_sync")
|
||||
@Schema(description = "金蝶附件同步")
|
||||
public class KingdeeAttachmentSync extends TenantEntity {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 业务单据类型(pay_application/formal_settlement_ap/formal_settlement_ar/invoice_receipt/invoice_application)
|
||||
*/
|
||||
private String bizType;
|
||||
/**
|
||||
* 业务单据id
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long bizId;
|
||||
/**
|
||||
* 附件幂等键(用户附件=附件url;发票文件=A801-2 的 path)
|
||||
*/
|
||||
private String attachmentKey;
|
||||
/**
|
||||
* 附件名称(展示/排查用)
|
||||
*/
|
||||
private String attachmentName;
|
||||
/**
|
||||
* 金蝶侧文件路径(A902 返回 data.path)
|
||||
*/
|
||||
private String kingdeePath;
|
||||
/**
|
||||
* 同步状态:1 成功 / 0 失败
|
||||
*/
|
||||
private Integer syncStatus;
|
||||
/**
|
||||
* 失败原因(超限/下载失败/金蝶报错)
|
||||
*/
|
||||
private String failReason;
|
||||
/**
|
||||
* 最近上传时间
|
||||
*/
|
||||
private LocalDateTime syncTime;
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.core.tenant.mp.TenantEntity;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定记录实体类
|
||||
* <p>
|
||||
* 一行 = 一张金蝶目标单:推送成功后 lockBusiData 锁定(FAILED→LOCKED),金蝶经 unLockNotifyBusiData
|
||||
* 回调解锁(→UNLOCKED,DELETE 时废除源单并还原关联数据);同时承担当前锁状态(编辑守卫/回调定位)
|
||||
* 与锁定补偿扫表(FAILED+try_count)、锁定/解锁留痕
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("blade_kingdee_bill_lock")
|
||||
@Schema(description = "金蝶单据锁定记录")
|
||||
public class KingdeeBillLock extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* WLTMS 单据类型(SETTLEMENT/INVOICE_APPLY,预留 PAY_APPLY/RECEIVED_INVOICE)
|
||||
*/
|
||||
private String sourceBillType;
|
||||
/**
|
||||
* WLTMS 业务主键
|
||||
*/
|
||||
private Long sourceBillId;
|
||||
/**
|
||||
* WLTMS 单号(正式结算单号/开票申请单号)
|
||||
*/
|
||||
private String sourceBillNo;
|
||||
/**
|
||||
* 金蝶单据类型(ApFin_other_BT_S/arfin_other_BT_S/cdm_paybill_bank_elect_BT_S 等固定值)
|
||||
*/
|
||||
private String targetBillType;
|
||||
/**
|
||||
* 金蝶单据编号(即业务单 kingdeeBillNo,开票申请按发票张次逐张一行)
|
||||
*/
|
||||
private String targetBillNo;
|
||||
/**
|
||||
* 锁状态:failed-锁定失败(补偿重试对象)/locked-金蝶锁定中/unlocked-已解锁(终态)
|
||||
*/
|
||||
private String lockStatus;
|
||||
/**
|
||||
* 最近动作来源操作类型:SAVE-单据新增,DELETE-单据删除(DELETE 回调触发废除还原)
|
||||
*/
|
||||
private String operationType;
|
||||
/**
|
||||
* 锁定重试次数(补偿任务用,超上限停试)
|
||||
*/
|
||||
private Integer tryCount;
|
||||
/**
|
||||
* 锁定时间
|
||||
*/
|
||||
private LocalDateTime lockTime;
|
||||
/**
|
||||
* 解锁时间
|
||||
*/
|
||||
private LocalDateTime unlockTime;
|
||||
/**
|
||||
* 推单发起人(锁定操作人)
|
||||
*/
|
||||
private String lockOperator;
|
||||
/**
|
||||
* 回调操作人(金蝶侧操作人)
|
||||
*/
|
||||
private String notifyOperator;
|
||||
/**
|
||||
* 锁定失败原因
|
||||
*/
|
||||
private String failReason;
|
||||
}
|
||||
+86
@@ -47,22 +47,108 @@ import java.time.LocalDateTime;
|
||||
@Schema(description = "金蝶进项发票票据池镜像")
|
||||
public class KingdeeInvoicePool extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 发票号码(同租户唯一,A801-1 invoiceNo,镜像 upsert 匹配键)
|
||||
*/
|
||||
private String invoiceNo;
|
||||
/**
|
||||
* 金蝶发票流水号(票池唯一标识,A801-1 serialNo)
|
||||
*/
|
||||
private String serialNo;
|
||||
/**
|
||||
* 开票日期
|
||||
*/
|
||||
private LocalDate invoiceDate;
|
||||
/**
|
||||
* 发票类型(票易通编码转名称后落库,如「数电发票(增值税专用发票)」)
|
||||
*/
|
||||
private String invoiceType;
|
||||
/**
|
||||
* 票易通原始发票类型编码(A801-1 invoiceType,如 26/27,A303-2 invoicetypef7_number 取数)
|
||||
*/
|
||||
private String invoiceTypeCode;
|
||||
/**
|
||||
* 税率(百分数,13 表示 13%)
|
||||
*/
|
||||
private BigDecimal taxRate;
|
||||
/**
|
||||
* 开票金额(A801-1 价税合计 totalAmount)
|
||||
*/
|
||||
private BigDecimal invoiceAmount;
|
||||
/**
|
||||
* 税额
|
||||
*/
|
||||
private BigDecimal taxAmount;
|
||||
/**
|
||||
* 受票单位(购方名称,A801-1 buyerName)
|
||||
*/
|
||||
private String receiverName;
|
||||
/**
|
||||
* 购方税号(A801-1 buyerTaxNo)
|
||||
*/
|
||||
private String buyerTaxNo;
|
||||
/**
|
||||
* 开票单位(销方名称,A801-1 salerName)
|
||||
*/
|
||||
private String issuerName;
|
||||
/**
|
||||
* 销方税号(A801-1 salerTaxNo)
|
||||
*/
|
||||
private String salerTaxNo;
|
||||
/**
|
||||
* 开票单位开户银行
|
||||
*/
|
||||
private String bankName;
|
||||
/**
|
||||
* 开票单位银行账号
|
||||
*/
|
||||
private String bankAccount;
|
||||
/**
|
||||
* 开票单位开户网点
|
||||
*/
|
||||
private String issuingBank;
|
||||
/**
|
||||
* 联系电话
|
||||
*/
|
||||
private String phone;
|
||||
/**
|
||||
* 客户邮箱(分号分隔,最多 3 个)
|
||||
*/
|
||||
private String customerEmails;
|
||||
/**
|
||||
* 部门邮箱(分号分隔,最多 3 个)
|
||||
*/
|
||||
private String departmentEmails;
|
||||
/**
|
||||
* 金蝶单据号(票池发票取发票流水号 serialNo)
|
||||
*/
|
||||
private String kingdeeBillNo;
|
||||
/**
|
||||
* 金蝶同步状态(unsynced 未同步 / synced 已同步 / failed 同步失败)
|
||||
*/
|
||||
private String kingdeeStatus;
|
||||
/**
|
||||
* 查验状态(A801-1 checkStatus,0 未查验 / 1 通过 / 2 不通过)
|
||||
*/
|
||||
private String checkStatus;
|
||||
/**
|
||||
* 附件信息JSON(结构 {"source":"piaozone","path":"A801-2下载path","name":"文件名"})
|
||||
*/
|
||||
private String attachmentsJson;
|
||||
/**
|
||||
* A801-1 发票明细行JSON(BfiInvoicePoolItemVO 列表,票池存档;收票单分录已改与应付单同源结算费用行)
|
||||
*/
|
||||
private String itemsJson;
|
||||
/**
|
||||
* 票池数据更新时间(A801-1 billCreateTime,镜像排序字段)
|
||||
*/
|
||||
private LocalDateTime sourceUpdatedTime;
|
||||
/**
|
||||
* 剩余可用金额(A801-1 availableAmount,金蝶可用税额口径,选票本地过滤用:>0 才可选)
|
||||
*/
|
||||
private BigDecimal availableAmount;
|
||||
/**
|
||||
* 金额使用状态(A801-1 amountUseStatus,0 未使用可选 / 2 全部使用;选票本地过滤用)
|
||||
*/
|
||||
private String amountUseStatus;
|
||||
}
|
||||
|
||||
+91
@@ -26,6 +26,8 @@
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -49,16 +51,105 @@ public class KingdeeReceiptFlow extends TenantEntity {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 金蝶认领公告id(A301-2 返回 id,认领推送 A301-3 必填)
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long billId;
|
||||
/**
|
||||
* 金蝶付款人id(A301-2 公告对应往来单位 id,认领推送 A301-3 用)
|
||||
*/
|
||||
private String payerId;
|
||||
/**
|
||||
* 公告认领状态(A301-2 claimstatus:0未认领/1已认领)
|
||||
*/
|
||||
private String kingdeeClaimStatus;
|
||||
/**
|
||||
* 我已认领(A301-2 claimme)
|
||||
*/
|
||||
private Boolean claimMe;
|
||||
/**
|
||||
* 未认领金额(A301-2 unclaimamount)
|
||||
*/
|
||||
private BigDecimal unclaimedAmount;
|
||||
/**
|
||||
* 手续费(A301-2 fee)
|
||||
*/
|
||||
private BigDecimal fee;
|
||||
/**
|
||||
* 收款人(A301-2 recpayee)
|
||||
*/
|
||||
private String payeeName;
|
||||
/**
|
||||
* 收款通知单号(A301-2 billno)
|
||||
*/
|
||||
private String receiptNoticeNo;
|
||||
/**
|
||||
* 付款人名称(A301-2 recviewpayer)
|
||||
*/
|
||||
private String payerName;
|
||||
/**
|
||||
* 收款金额(A301-2 reamount)
|
||||
*/
|
||||
private BigDecimal receiptAmount;
|
||||
/**
|
||||
* 对方单位名称(A301-2 oppunit)
|
||||
*/
|
||||
private String counterpartyName;
|
||||
/**
|
||||
* 对方银行账号(A301-2 oppbanknumber)
|
||||
*/
|
||||
private String counterpartyAccount;
|
||||
/**
|
||||
* 对方开户行(A301-2 oppbank)
|
||||
*/
|
||||
private String counterpartyBank;
|
||||
/**
|
||||
* 摘要(A301-2 description)
|
||||
*/
|
||||
private String summary;
|
||||
/**
|
||||
* 交易日期(A301-2 tradetime)
|
||||
*/
|
||||
private LocalDateTime transactionTime;
|
||||
/**
|
||||
* 明细流水号(A301-2 detailid,回退 tradedetailno / bankdetailno,幂等键)
|
||||
*/
|
||||
private String detailSerialNo;
|
||||
/**
|
||||
* 已认领金额(本地认领驱动,初始为0)
|
||||
*/
|
||||
private BigDecimal claimedAmount;
|
||||
/**
|
||||
* 认领状态(unclaimed未认领/partial部分认领/claimed认领完成,累计认领与公告未认领金额对表)
|
||||
*/
|
||||
private String claimStatus;
|
||||
/**
|
||||
* 推送状态(none未推送/pushed已推送/failed推送失败;已推送为终态闸门,不允许二次推送)
|
||||
*/
|
||||
private String pushStatus;
|
||||
/**
|
||||
* 最近一次推送失败原因(成功后清空)
|
||||
*/
|
||||
private String pushFailReason;
|
||||
/**
|
||||
* 首次推送金蝶成功时间
|
||||
*/
|
||||
private LocalDateTime pushedTime;
|
||||
/**
|
||||
* 金蝶推送来源单据编码(分摊结算单号下划线拼接,推送时生成落库;分摊不变则重试编码不变,A301-1 回写匹配键)
|
||||
*/
|
||||
private String awrpSrcCode;
|
||||
/**
|
||||
* 金蝶收款单号(推送响应回填 / A301-1 回写推进)
|
||||
*/
|
||||
private String kingdeeBillNo;
|
||||
/**
|
||||
* 金蝶回写状态(none未推送/pushed已推送/approved审核通过/received已收款/failed失败)
|
||||
*/
|
||||
private String kingdeeBillStatus;
|
||||
/**
|
||||
* 来源更新时间(取交易日期,同步时刷新)
|
||||
*/
|
||||
private LocalDateTime sourceUpdatedTime;
|
||||
}
|
||||
|
||||
+54
-7
@@ -2,18 +2,24 @@
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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)
|
||||
*/
|
||||
@@ -34,43 +40,84 @@ import java.time.LocalDate;
|
||||
@TableName("blade_payment_application")
|
||||
public class PaymentApplication extends TenantEntity {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
/** 付款申请单号 */
|
||||
private String paymentNo;
|
||||
/** 付款类型:project_advance 项目预付 / progress_advance 进度预付 / settlement_payment 结算付款 */
|
||||
private String paymentType;
|
||||
/** 正式结算单id */
|
||||
private Long settlementId;
|
||||
/** 正式结算单号 */
|
||||
private String settlementNo;
|
||||
/** 预结算单id */
|
||||
private Long preSettlementId;
|
||||
/** 预结算单号 */
|
||||
private String preSettlementNo;
|
||||
/** 项目id */
|
||||
private Long projectId;
|
||||
/** 项目名称 */
|
||||
private String projectName;
|
||||
/** 所属组织id */
|
||||
private Long deptId;
|
||||
/** 所属组织名称 */
|
||||
private String deptName;
|
||||
/** 合同id */
|
||||
private Long contractId;
|
||||
/** 合同编号 */
|
||||
private String contractNo;
|
||||
/** 合同名称 */
|
||||
private String contractName;
|
||||
/** 付款方 */
|
||||
private String payerName;
|
||||
/** 收款方 */
|
||||
private String payeeName;
|
||||
/** 结算金额 */
|
||||
private BigDecimal settlementAmount;
|
||||
/** 应付金额 */
|
||||
private BigDecimal payableAmount;
|
||||
/** 来源单据类型:项目预付 / 正式结算单 / 预结算单 */
|
||||
private String billType;
|
||||
/** 付款比例(%) */
|
||||
private BigDecimal paymentRatio;
|
||||
/** 申请付款金额 */
|
||||
private BigDecimal appliedAmount;
|
||||
/** 付款方式,业务字典 pay_method */
|
||||
private String paymentMethod;
|
||||
/** 汇票台账id(票据付款时关联) */
|
||||
private Long billLedgerId;
|
||||
/** 票据号码 */
|
||||
private String billNo;
|
||||
/** 收款账户id */
|
||||
private Long receiptAccountId;
|
||||
/** 收款账户名称 */
|
||||
private String receiptAccountName;
|
||||
/** 收款银行名称 */
|
||||
private String bankName;
|
||||
/** 收款账号 */
|
||||
private String bankAccount;
|
||||
/** 申请人 */
|
||||
private String applicantName;
|
||||
/** 申请日期 */
|
||||
private LocalDate applyDate;
|
||||
/** 发票匹配状态:unmatched 未匹配 / matched 已匹配 */
|
||||
private String invoiceStatus;
|
||||
/** 匹配发票金额 */
|
||||
private BigDecimal matchedInvoiceAmount;
|
||||
/** 已付款金额 */
|
||||
private BigDecimal paidAmount;
|
||||
/** 审批状态:draft 草稿 / reviewing 审批中 / approved 审批通过 / returned 已驳回 / voided 已作废 */
|
||||
private String approvalStatus;
|
||||
/** 当前审批节点 */
|
||||
private String currentNode;
|
||||
/** 当前处理人 */
|
||||
private String currentProcessor;
|
||||
/** 金蝶单据号(A103-2 查询回填的星瀚单据编号 billno,如 FKSQ-xxxx) */
|
||||
private String kingdeeBillNo;
|
||||
/** 金蝶单据id(A103-1/A103-3 推送返回的星瀚内部主键,附件上传等使用) */
|
||||
private String kingdeeBillId;
|
||||
/** 金蝶同步状态:unsynced 未生成 / synced 已生成 / paying 付款中 / paid 已付款 / closed 已关闭 / failed 生成失败 */
|
||||
private String kingdeeStatus;
|
||||
/** 附件信息JSON */
|
||||
private String attachmentsJson;
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
|
||||
+6
@@ -65,7 +65,13 @@ public class ReceiptClaim extends TenantEntity {
|
||||
private String attachmentsJson;
|
||||
private String remark;
|
||||
private String claimStatus;
|
||||
/**
|
||||
* 金蝶收款单号(冗余展示:推送成功/A301-1 回写时由流水级联刷新)
|
||||
*/
|
||||
private String kingdeeBillNo;
|
||||
/**
|
||||
* 金蝶回写状态(冗余展示:与流水 kingdee_bill_status 同步刷新)
|
||||
*/
|
||||
private String kingdeeBillStatus;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long voidedBy;
|
||||
|
||||
+13
@@ -9,6 +9,7 @@
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
@@ -18,6 +19,7 @@ import org.springblade.transport.pojo.entity.FormalSettlementInvoice;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementSource;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementPayment;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serial;
|
||||
@@ -39,6 +41,15 @@ public class FormalSettlementVO extends FormalSettlement {
|
||||
@TableField(exist = false) private String ids;
|
||||
@TableField(exist = false) private String approvalStatusName;
|
||||
@TableField(exist = false) private String settlementTypeName;
|
||||
@TableField(exist = false) private Integer attachmentCount;
|
||||
@TableField(exist = false) private String attachmentSyncStatus;
|
||||
@TableField(exist = false) private String attachmentSyncStatusName;
|
||||
@TableField(exist = false) @Schema(description = "金蝶锁状态:locked-锁定中/failed-锁定失败;unlocked为终态不返回,null=无锁或已解锁")
|
||||
private String kingdeeLockStatus;
|
||||
@TableField(exist = false) @Schema(description = "金蝶锁失败原因(仅锁定失败时有值)")
|
||||
private String kingdeeLockFailReason;
|
||||
@TableField(exist = false) @Schema(description = "金蝶锁重试次数(达上限5后不再自动补偿,需重推同步金蝶)")
|
||||
private Integer kingdeeTryCount;
|
||||
@TableField(exist = false) private String preSettlementNos;
|
||||
@TableField(exist = false) private String preSettlementNo;
|
||||
@TableField(exist = false) private List<FormalSettlementSource> sources;
|
||||
@@ -46,6 +57,8 @@ public class FormalSettlementVO extends FormalSettlement {
|
||||
@TableField(exist = false) private List<FormalSettlementSummaryFee> summaryFees;
|
||||
@TableField(exist = false) private List<FormalSettlementPayment> payments;
|
||||
@TableField(exist = false) private List<FormalSettlementInvoice> invoices;
|
||||
@TableField(exist = false) @Schema(description = "开票申请经金蝶回写的发票结果(含文件地址,只读展示)")
|
||||
private List<InvoiceApplicationInvoice> issuedInvoices;
|
||||
@TableField(exist = false) private List<PaymentApplicationVO> paymentApplications;
|
||||
@TableField(exist = false) private List<SettlementAdjustmentVO> adjustments;
|
||||
@TableField(exist = false) private List<FormalSettlementChangeRecord> changeRecords;
|
||||
|
||||
+8
@@ -30,6 +30,7 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplication;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationDetail;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationRecord;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
|
||||
|
||||
@@ -48,9 +49,16 @@ public class InvoiceApplicationVO extends InvoiceApplication {
|
||||
@TableField(exist = false) private String settlementNos;
|
||||
@TableField(exist = false) private String approvalStatusName;
|
||||
@TableField(exist = false) private String kingdeeStatusName;
|
||||
@TableField(exist = false) private Integer attachmentCount;
|
||||
@TableField(exist = false) private String attachmentSyncStatus;
|
||||
@TableField(exist = false) private String attachmentSyncStatusName;
|
||||
@TableField(exist = false) private String kingdeeLockStatus;
|
||||
@TableField(exist = false) private String kingdeeLockFailReason;
|
||||
@TableField(exist = false) private Integer kingdeeTryCount;
|
||||
@TableField(exist = false) private String createUserName;
|
||||
@TableField(exist = false) private List<InvoiceApplicationSettlement> settlements;
|
||||
@TableField(exist = false) private List<InvoiceApplicationSheetVO> sheets;
|
||||
@TableField(exist = false) private List<InvoiceApplicationDetail> details;
|
||||
@TableField(exist = false) private List<InvoiceApplicationRecord> records;
|
||||
@TableField(exist = false) private List<InvoiceApplicationInvoice> invoices;
|
||||
}
|
||||
|
||||
+4
-1
@@ -48,7 +48,10 @@ public class InvoiceReceiptVO extends InvoiceReceipt {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
@TableField(exist = false) private String settlementNos;
|
||||
@TableField(exist = false) private String approvalStatusName;
|
||||
@TableField(exist = false) private String kingdeeStatusName;
|
||||
@TableField(exist = false) private String kingdeePushStatusName;
|
||||
@TableField(exist = false) private Integer attachmentCount;
|
||||
@TableField(exist = false) private String attachmentSyncStatus;
|
||||
@TableField(exist = false) private String attachmentSyncStatusName;
|
||||
@TableField(exist = false) private String createUserName;
|
||||
@TableField(exist = false) private String updateUserName;
|
||||
@TableField(exist = false) private List<InvoiceReceiptSettlement> settlements;
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 金蝶票据池镜像统一视图(含本地认领/登记标记;认领口径:可匹配与默认认领金额=价税合计)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "金蝶票据池镜像统一视图")
|
||||
public class KingdeeInvoicePoolVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 镜像主键,仅已入镜像(已登记)的票非空 */
|
||||
@Schema(description = "镜像主键")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/** 发票号码 */
|
||||
@Schema(description = "发票号码")
|
||||
private String invoiceNo;
|
||||
|
||||
/** 金蝶发票流水号(票池唯一标识) */
|
||||
@Schema(description = "金蝶发票流水号")
|
||||
private String serialNo;
|
||||
|
||||
/** 开票日期 yyyy-MM-dd */
|
||||
@Schema(description = "开票日期")
|
||||
private String invoiceDate;
|
||||
|
||||
/** 发票类型名称(镜像落库时已由编码转名称) */
|
||||
@Schema(description = "发票类型名称")
|
||||
private String invoiceType;
|
||||
|
||||
/** 税率百分数,如 13 */
|
||||
@Schema(description = "税率百分数")
|
||||
private BigDecimal taxRate;
|
||||
|
||||
/** 价税合计 */
|
||||
@Schema(description = "发票金额(价税合计)")
|
||||
private BigDecimal invoiceAmount;
|
||||
|
||||
/** 税额 */
|
||||
@Schema(description = "税额")
|
||||
private BigDecimal taxAmount;
|
||||
|
||||
/** 受票单位(购方名称) */
|
||||
@Schema(description = "受票单位")
|
||||
private String receiverName;
|
||||
|
||||
/** 购方税号 */
|
||||
@Schema(description = "购方税号")
|
||||
private String buyerTaxNo;
|
||||
|
||||
/** 开票单位(销方名称) */
|
||||
@Schema(description = "开票单位")
|
||||
private String issuerName;
|
||||
|
||||
/** 销方税号 */
|
||||
@Schema(description = "销方税号")
|
||||
private String salerTaxNo;
|
||||
|
||||
/** 销方开户行名称 */
|
||||
@Schema(description = "销方开户行名称")
|
||||
private String bankName;
|
||||
|
||||
/** 销方银行账号 */
|
||||
@Schema(description = "销方银行账号")
|
||||
private String bankAccount;
|
||||
|
||||
/** 销方开户行及账号 */
|
||||
@Schema(description = "开户行及账号")
|
||||
private String issuingBank;
|
||||
|
||||
/** 销方电话 */
|
||||
@Schema(description = "电话")
|
||||
private String phone;
|
||||
|
||||
/** 客户邮箱 JSON 串 */
|
||||
@Schema(description = "客户邮箱JSON")
|
||||
private String customerEmails;
|
||||
|
||||
/** 部门邮箱 JSON 串 */
|
||||
@Schema(description = "部门邮箱JSON")
|
||||
private String departmentEmails;
|
||||
|
||||
/** 金蝶流水号(登记侧展示为金蝶单据号) */
|
||||
@Schema(description = "金蝶单据号")
|
||||
private String kingdeeBillNo;
|
||||
|
||||
/** 同步状态 unsynced/synced/failed */
|
||||
@Schema(description = "金蝶同步状态")
|
||||
private String kingdeeStatus;
|
||||
|
||||
/** 查验状态 */
|
||||
@Schema(description = "查验状态")
|
||||
private String checkStatus;
|
||||
|
||||
/** 附件 JSON(票易通下载 path) */
|
||||
@Schema(description = "附件信息JSON")
|
||||
private String attachmentsJson;
|
||||
|
||||
/** 可认领金额,口径同发票金额 */
|
||||
@Schema(description = "可认领金额")
|
||||
private BigDecimal availableInvoiceAmount;
|
||||
|
||||
/** 剩余可用金额(金蝶可用税额口径,与可认领金额的价税合计口径不同) */
|
||||
@Schema(description = "剩余可用金额")
|
||||
private BigDecimal availableTaxAmount;
|
||||
|
||||
/** 默认认领金额,口径同发票金额 */
|
||||
@Schema(description = "默认认领金额")
|
||||
private BigDecimal matchedAmount;
|
||||
|
||||
/** 是否已被正式结算单认领(实时查询返回行的本地装饰标记,仅展示不作过滤) */
|
||||
@Schema(description = "是否已认领")
|
||||
private Boolean claimed;
|
||||
|
||||
/** 是否已存在非作废收票登记(实时查询返回行的本地装饰标记,仅展示不作过滤) */
|
||||
@Schema(description = "是否已登记")
|
||||
private Boolean registered;
|
||||
}
|
||||
+6
@@ -43,6 +43,12 @@ public class PaymentApplicationVO extends PaymentApplication {
|
||||
@TableField(exist = false) private String paymentTypeName;
|
||||
@TableField(exist = false) private String approvalStatusName;
|
||||
@TableField(exist = false) private String kingdeeStatusName;
|
||||
@TableField(exist = false) private Integer attachmentCount;
|
||||
@TableField(exist = false) private String attachmentSyncStatus;
|
||||
@TableField(exist = false) private String attachmentSyncStatusName;
|
||||
@TableField(exist = false) private String kingdeeLockStatus;
|
||||
@TableField(exist = false) private String kingdeeLockFailReason;
|
||||
@TableField(exist = false) private Integer kingdeeTryCount;
|
||||
@TableField(exist = false) private List<PaymentApplicationInvoice> invoices;
|
||||
@TableField(exist = false) private List<PaymentApplicationRecord> paymentRecords;
|
||||
@TableField(exist = false) private List<PaymentApplicationSettlement> settlements;
|
||||
|
||||
+5
@@ -76,6 +76,11 @@ public class ReceiptClaimRecordVO extends ReceiptClaim {
|
||||
private String claimStatusName;
|
||||
@TableField(exist = false)
|
||||
private String kingdeeBillStatusName;
|
||||
/**
|
||||
* 关联流水推送状态(none未推送/pushed已推送/failed推送失败,查询条件)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String pushStatus;
|
||||
@TableField(exist = false)
|
||||
private List<ReceiptClaimSettlement> settlements;
|
||||
@TableField(exist = false)
|
||||
|
||||
+16
@@ -56,6 +56,12 @@ public class ReceiptFlowVO extends KingdeeReceiptFlow {
|
||||
@TableField(exist = false)
|
||||
private String claimStatusName;
|
||||
@TableField(exist = false)
|
||||
private String kingdeeClaimStatusName;
|
||||
@TableField(exist = false)
|
||||
private String pushStatusName;
|
||||
@TableField(exist = false)
|
||||
private String kingdeeBillStatusName;
|
||||
@TableField(exist = false)
|
||||
private BigDecimal remainingAmount;
|
||||
@TableField(exist = false)
|
||||
private String createUserName;
|
||||
@@ -73,6 +79,16 @@ public class ReceiptFlowVO extends KingdeeReceiptFlow {
|
||||
@TableField(exist = false)
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime transactionEndTime;
|
||||
/**
|
||||
* 收款金额区间-最小值(查询条件)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private BigDecimal receiptAmountStart;
|
||||
/**
|
||||
* 收款金额区间-最大值(查询条件)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private BigDecimal receiptAmountEnd;
|
||||
@TableField(exist = false)
|
||||
private List<ReceiptFlowRecord> claimRecords = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ public class SysClient implements ISysClient {
|
||||
|
||||
private final IFeeItemService feeItemService;
|
||||
private final ICargoTypeService cargoTypeService;
|
||||
private final IMeasurementUnitService measurementUnitService;
|
||||
private final IInvoiceItemService invoiceItemService;
|
||||
|
||||
@Override
|
||||
@GetMapping(MENU)
|
||||
@@ -231,10 +233,27 @@ public class SysClient implements ISysClient {
|
||||
.orderByAsc(CargoType::getCargoCode)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(MEASUREMENT_UNITS)
|
||||
public R<List<MeasurementUnit>> getMeasurementUnits() {
|
||||
return R.data(measurementUnitService.list(Wrappers.<MeasurementUnit>lambdaQuery()
|
||||
.eq(MeasurementUnit::getIsDeleted, 0)
|
||||
.orderByAsc(MeasurementUnit::getUnitCode)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(PERMISSIONS)
|
||||
public R<List<String>> getPermissions(String roleId) {
|
||||
return R.data(menuService.permissionCodes(roleId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(INVOICE_ITEMS)
|
||||
public R<List<InvoiceItem>> getInvoiceItems() {
|
||||
return R.data(invoiceItemService.list(Wrappers.<InvoiceItem>lambdaQuery()
|
||||
.eq(InvoiceItem::getStatus, 1)
|
||||
.eq(InvoiceItem::getIsDeleted, 0)
|
||||
.orderByAsc(InvoiceItem::getCategoryName, InvoiceItem::getShortName)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -47,10 +47,19 @@
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-process-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-bfi-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-amqp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
|
||||
+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 {
|
||||
|
||||
|
||||
+413
@@ -0,0 +1,413 @@
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* 锁定补偿开关,默认关闭(安全兜底)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
+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() {
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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_SETTLEMENT = "settlement_payment";
|
||||
|
||||
/** 审批状态:草稿(允许编辑、删除)。 */
|
||||
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);
|
||||
}
|
||||
}
|
||||
+22
-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,21 @@ 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) {
|
||||
try {
|
||||
customerArchiveService.syncKingdee(id);
|
||||
} catch (Exception exception) {
|
||||
log.error("客商审批通过后推送金蝶异常,id={}", id, exception);
|
||||
}
|
||||
}
|
||||
return R.status(result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,6 +184,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -29,6 +29,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.PaymentApplicationSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
|
||||
import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO;
|
||||
@@ -81,7 +82,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 +113,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));
|
||||
}
|
||||
|
||||
|
||||
+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;
|
||||
|
||||
}
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
+7
@@ -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,10 @@
|
||||
current_node,
|
||||
current_processor,
|
||||
approved_time,
|
||||
kingdee_status,
|
||||
kingdee_fail_reason,
|
||||
kingdee_sync_time
|
||||
approved_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">
|
||||
|
||||
+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
|
||||
|
||||
+23
@@ -188,6 +188,29 @@ public interface ICustomerArchiveService extends BaseService<CustomerArchive> {
|
||||
*/
|
||||
boolean removeDraft(String ids);
|
||||
|
||||
/**
|
||||
* 推送金蝶(A902-1 复查 + A902-2 保存),幂等:已推送/金蝶已有直接返回
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 更新后的客商档案
|
||||
*/
|
||||
CustomerArchive syncKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 定时同步金蝶客商数据:待推送客商自动推送 + 已同步客商使用状态回写
|
||||
*
|
||||
* @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> {
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
|
||||
import org.springblade.transport.pojo.entity.KingdeeBillLock;
|
||||
|
||||
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,
|
||||
* targetBillNo 支持逗号拼接批量,逐张按 targetBillType+targetBillNo 定位最新 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);
|
||||
|
||||
/**
|
||||
* 锁定补偿:扫 failed 且 try_count 未达重试上限(RETRY_MAX_COUNT)的锁记录,逐张重调 lockBusiData
|
||||
* (接口 1对1:单次一个源单号对一个金蝶单号,单张成败互不影响;定时任务定时触发,亦可人工触发);
|
||||
* 成功翻 locked,失败 try_count+1 并记失败原因;
|
||||
* 计数烧满上限后停止自动补偿(人工重推「同步金蝶」会重置计数重新接管)。锁记录落库均为批量更新
|
||||
*
|
||||
* @return 补偿摘要(待补偿/成功张数),供调用方记日志或页面提示
|
||||
*/
|
||||
String retryFailedLocks();
|
||||
}
|
||||
+82
@@ -21,6 +21,8 @@ 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;
|
||||
@@ -41,5 +43,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();
|
||||
}
|
||||
|
||||
+2
-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);
|
||||
|
||||
|
||||
+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, "作废原因"));
|
||||
|
||||
+551
-13
@@ -28,6 +28,7 @@ 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;
|
||||
@@ -39,9 +40,21 @@ 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,17 +84,17 @@ 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.Transactional;
|
||||
|
||||
@@ -101,6 +114,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 +126,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 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 +220,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 +302,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 +498,365 @@ 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;
|
||||
}
|
||||
pushCustomerToKingdee(id, customer, after, detail(id));
|
||||
updateById(after);
|
||||
return after;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 A902-2 报文推送金蝶并回写协同状态;detail 由调用方准备,
|
||||
* 定时同步批量组装报文数据,避免逐条 detail 查询的 N+1 问题
|
||||
*/
|
||||
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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
CustomerArchiveVO detail = buildSyncArchiveVO(customer,
|
||||
receiptAccountsMap.getOrDefault(customer.getId(), new ArrayList<>()));
|
||||
CustomerArchive after = copyCustomer(customer);
|
||||
after.setKingdeeSyncTime(LocalDateTime.now());
|
||||
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);
|
||||
}
|
||||
}
|
||||
// ②③ 共用:对已推送(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 +925,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 +1032,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 +1336,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 +1802,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 +1814,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 +1863,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
}
|
||||
}
|
||||
if (changedFields.isEmpty()) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
CustomerChangeRecord record = new CustomerChangeRecord();
|
||||
record.setId(IdWorker.getId());
|
||||
@@ -1355,7 +1875,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 +2006,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 +2096,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 +2117,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);
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
}
|
||||
+639
-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,24 @@ 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.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 +111,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 +129,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;
|
||||
@@ -135,6 +171,12 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
private final IContractManageService contractManageService;
|
||||
private final IPreSettlementService preSettlementService;
|
||||
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 +205,66 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充附件同步聚合态(无附件/未上传/部分未传/已上传):按结算类型分业务类型一次 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 +303,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 +315,7 @@ 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));
|
||||
Set<Long> relationApplicationIds = paymentApplicationSettlementMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.select(PaymentApplicationSettlement::getPaymentApplicationId)
|
||||
@@ -233,7 +330,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 +401,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 +433,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 +509,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");
|
||||
@@ -443,27 +565,30 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
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 +596,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 +986,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 +1049,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 +1065,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 +1417,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 +1600,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 +1717,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 +1794,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 +1819,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 +1835,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 +1857,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);
|
||||
}
|
||||
|
||||
+970
-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 {
|
||||
}
|
||||
+360
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* 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.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.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) {
|
||||
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) {
|
||||
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.getTargetBillNo())) {
|
||||
throw new ServiceException("金蝶解锁回调参数非法:targetBillNo不能为空");
|
||||
}
|
||||
// 批量回调:targetBillNo 逗号拼接,逐张处理;未匹配锁定中记录的幂等跳过(重复通知防金蝶重试风暴)
|
||||
int processed = 0;
|
||||
for (String targetBillNo : Func.toStrList(param.getTargetBillNo())) {
|
||||
KingdeeBillLock lockRecord = this.getOne(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.eq(KingdeeBillLock::getTargetBillType, param.getTargetBillType())
|
||||
.eq(KingdeeBillLock::getTargetBillNo, targetBillNo)
|
||||
.orderByDesc(KingdeeBillLock::getId)
|
||||
.last("limit 1"), false);
|
||||
if (lockRecord == null || !KingdeeLockConstant.STATUS_LOCKED.equals(lockRecord.getLockStatus())) {
|
||||
log.warn("金蝶解锁回调未匹配锁定中的锁记录(幂等跳过):targetBillType={},targetBillNo={}",
|
||||
param.getTargetBillType(), targetBillNo);
|
||||
continue;
|
||||
}
|
||||
lockRecord.setLockStatus(KingdeeLockConstant.STATUS_UNLOCKED);
|
||||
lockRecord.setOperationType(param.getOperationType());
|
||||
lockRecord.setUnlockTime(LocalDateTime.now());
|
||||
lockRecord.setNotifyOperator(param.getOperator());
|
||||
this.updateById(lockRecord);
|
||||
processed++;
|
||||
log.info("金蝶单据[{}:{}]解锁回调落库:operationType={},操作人={}",
|
||||
lockRecord.getSourceBillType(), lockRecord.getSourceBillNo(), param.getOperationType(), param.getOperator());
|
||||
if (KingdeeLockConstant.OPERATION_DELETE.equals(param.getOperationType())) {
|
||||
this.restoreOnKingdeeDelete(lockRecord, param.getOperator());
|
||||
}
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶删单(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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定补偿:扫 failed 且 try_count 未达重试上限(RETRY_MAX_COUNT)的锁记录,逐张重调 lockBusiData
|
||||
* (接口 1对1:单次一个源单号对一个金蝶单号,单张成败互不影响,由定时任务定时触发,亦可人工触发)。
|
||||
* <p>成功翻 locked(补锁时间、清失败原因);失败 try_count+1 并记失败原因,计数烧满上限后不再自动重试
|
||||
* (人工重推「同步金蝶」重置计数重新接管);成败落库均为批量更新(updateBatchById),避免逐张 update
|
||||
*/
|
||||
@Override
|
||||
public String retryFailedLocks() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
+1245
-71
File diff suppressed because it is too large
Load Diff
+1
@@ -236,6 +236,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));
|
||||
|
||||
+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
+4
-1
@@ -745,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");
|
||||
|
||||
+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; }
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user