Merge remote-tracking branch 'origin/master' into dev
This commit is contained in:
@@ -34,3 +34,7 @@ Thumbs.db
|
||||
.claude
|
||||
/outputs/
|
||||
/.codex-work/
|
||||
.serena/
|
||||
.workbuddy/
|
||||
/data/
|
||||
/output/
|
||||
|
||||
+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 回写的单据生命周期:已推送/审核通过/已收款)
|
||||
+8
-7
@@ -84,16 +84,11 @@ public class BladeAuthorizationHandler extends AbstractAuthorizationHandler {
|
||||
if (bladeProperties.isProd() && isWeakPassword(request.getPassword())) {
|
||||
return buildValidationFailure(ExceptionCode.INVALID_USER_PASSWORD);
|
||||
}
|
||||
// 判断账号是否锁定
|
||||
// 仅按账号锁定;不做 IP 锁定,避免同出口/网关 IP 下一人失败牵连全部未登录用户
|
||||
OAuth2Validation accountValidation = lockHandler.validateAccountLock(request.getTenantId(), request.getUsername());
|
||||
if (!accountValidation.isSuccess()) {
|
||||
return accountValidation;
|
||||
}
|
||||
// 判断IP是否锁定
|
||||
OAuth2Validation ipValidation = lockHandler.validateIpLock(request.getTenantId());
|
||||
if (!ipValidation.isSuccess()) {
|
||||
return ipValidation;
|
||||
}
|
||||
}
|
||||
return super.preValidation(request);
|
||||
}
|
||||
@@ -105,7 +100,13 @@ public class BladeAuthorizationHandler extends AbstractAuthorizationHandler {
|
||||
*/
|
||||
@Override
|
||||
public void preFailure(OAuth2Request request, OAuth2Validation validation) {
|
||||
// 处理认证失败,增加错误次数
|
||||
// 已因锁定失败时不再累加失败次数,避免其他账号重试时被误计并连带锁定
|
||||
if (validation != null && !validation.isSuccess()
|
||||
&& ExceptionCode.USER_TOO_MANY_FAILS.getCode() == validation.getCode()) {
|
||||
log.error("用户:{},认证失败,失败原因:{}", request.getUsername(), validation.getMessage());
|
||||
return;
|
||||
}
|
||||
// 处理认证失败,增加错误次数(仅账号维度)
|
||||
lockHandler.handleAuthFailure(request.getTenantId(), request.getUsername());
|
||||
|
||||
log.error("用户:{},认证失败,失败原因:{}", request.getUsername(), validation.getMessage());
|
||||
|
||||
@@ -90,7 +90,7 @@ public class BladeLockHandler {
|
||||
|
||||
/**
|
||||
* 处理认证失败
|
||||
* 同时增加账号和IP错误次数
|
||||
* 仅增加账号错误次数;不再累计 IP 失败次数,避免共享出口 IP 误伤其他用户。
|
||||
*
|
||||
* @param tenantId 租户id
|
||||
* @param account 账号
|
||||
@@ -102,7 +102,6 @@ public class BladeLockHandler {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
authLockClient.addAccountFailCount(tenantId, account, userId, ip, userAgent);
|
||||
authLockClient.addIpFailCount(tenantId, ip, userAgent);
|
||||
} catch (Exception e) {
|
||||
log.warn("认证失败计数异常: {}", e.getMessage());
|
||||
}
|
||||
|
||||
+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("获取数据失败");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+23
-18
@@ -4,29 +4,34 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 部门类型枚举
|
||||
* 组织类型枚举,与业务字典 organization_type 对齐
|
||||
*
|
||||
* @author bfhuange
|
||||
* @date 2024/8/26
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum DeptCategory {
|
||||
/**
|
||||
* 公司
|
||||
*/
|
||||
COMPANY(1, "公司"),
|
||||
/**
|
||||
* 部门
|
||||
*/
|
||||
DEPT(2, "部门"),
|
||||
;
|
||||
/**
|
||||
* 集团
|
||||
*/
|
||||
GROUP(1, "集团"),
|
||||
/**
|
||||
* 公司
|
||||
*/
|
||||
COMPANY(2, "公司"),
|
||||
/**
|
||||
* 部门
|
||||
*/
|
||||
DEPT(3, "部门"),
|
||||
;
|
||||
|
||||
/**
|
||||
* 部门类型编码
|
||||
*/
|
||||
private final Integer code;
|
||||
/**
|
||||
* 部门类型名称
|
||||
*/
|
||||
private final String name;
|
||||
/**
|
||||
* 组织类型编码
|
||||
*/
|
||||
private final Integer code;
|
||||
/**
|
||||
* 组织类型名称
|
||||
*/
|
||||
private final String name;
|
||||
}
|
||||
|
||||
+3
@@ -69,4 +69,7 @@ public class OaOrgSyncPageVO implements Serializable {
|
||||
|
||||
@Schema(description = "增量同步使用的 OA modified 起始时间,后续分页需原样回传")
|
||||
private String modifiedSince;
|
||||
|
||||
@Schema(description = "全量同步结束后原始数据层级 JSON 文件路径")
|
||||
private String dumpFilePath;
|
||||
}
|
||||
|
||||
+3
@@ -66,4 +66,7 @@ public class OaPersonSyncPageVO implements Serializable {
|
||||
|
||||
@Schema(description = "增量同步使用的 OA modified 起始时间,后续分页需原样回传")
|
||||
private String modifiedSince;
|
||||
|
||||
@Schema(description = "全量同步结束后原始数据层级 JSON 文件路径")
|
||||
private String dumpFilePath;
|
||||
}
|
||||
|
||||
+2
@@ -32,6 +32,8 @@ public class FormalSettlementSaveRequest implements Serializable {
|
||||
private String remark;
|
||||
private List<SummaryFee> summaryFees;
|
||||
private List<Invoice> invoices;
|
||||
/** 项目预付核销 */
|
||||
private List<SettlementProjectAdvanceWriteOffRequest> projectAdvanceWriteOffs;
|
||||
|
||||
@Data
|
||||
public static class DetailAdjustment implements Serializable {
|
||||
|
||||
+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;
|
||||
}
|
||||
+3
@@ -76,6 +76,9 @@ public class PreSettlementSaveRequest implements Serializable {
|
||||
@Schema(description = "结算合计费用")
|
||||
private List<SummaryFee> summaryFees;
|
||||
|
||||
@Schema(description = "项目预付核销")
|
||||
private List<SettlementProjectAdvanceWriteOffRequest> projectAdvanceWriteOffs;
|
||||
|
||||
@Data
|
||||
@Schema(description = "结算合计费用")
|
||||
public static class SummaryFee implements Serializable {
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 导入结算单请求(列表筛选 + 确认/草稿入参)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "导入结算单请求")
|
||||
public class SettlementImportBatchRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "主键")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "批次号")
|
||||
private String batchNo;
|
||||
|
||||
@Schema(description = "项目ID")
|
||||
private Long projectId;
|
||||
|
||||
@Schema(description = "项目名称")
|
||||
private String projectName;
|
||||
|
||||
@Schema(description = "结算类型 receivable/payable")
|
||||
private String settlementType;
|
||||
|
||||
@Schema(description = "结算单类型 formal/pre,应收仅 formal")
|
||||
private String settlementBillType;
|
||||
|
||||
@Schema(description = "客户合同ID")
|
||||
private Long customerContractId;
|
||||
|
||||
@Schema(description = "客户合同名称")
|
||||
private String customerContractName;
|
||||
|
||||
@Schema(description = "客户名称")
|
||||
private String customerName;
|
||||
|
||||
@Schema(description = "承运商合同ID")
|
||||
private Long carrierContractId;
|
||||
|
||||
@Schema(description = "承运商合同名称")
|
||||
private String carrierContractName;
|
||||
|
||||
@Schema(description = "承运商名称")
|
||||
private String carrierName;
|
||||
|
||||
@Schema(description = "运单模式:new=新运单,history=历史运单")
|
||||
private String waybillMode;
|
||||
|
||||
@Schema(description = "导入状态")
|
||||
private String importStatus;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "动态费用列名")
|
||||
private List<String> feeItemNames;
|
||||
|
||||
@Schema(description = "导入明细行")
|
||||
private List<Map<String, Object>> rows;
|
||||
|
||||
@Schema(description = "分批追加")
|
||||
private Boolean append;
|
||||
|
||||
@Schema(description = "列表筛选:项目名称")
|
||||
private String projectNameLike;
|
||||
|
||||
@Schema(description = "列表筛选:创建人")
|
||||
private Long createUser;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 导入结算单作废请求
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "导入结算单作废请求")
|
||||
public class SettlementImportVoidRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "批次主键")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "作废原因")
|
||||
private String reason;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 结算单项目预付核销保存行
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "结算单项目预付核销保存行")
|
||||
public class SettlementProjectAdvanceWriteOffRequest implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "项目预付付款申请ID")
|
||||
private Long paymentApplicationId;
|
||||
|
||||
@Schema(description = "本次核销金额")
|
||||
private BigDecimal writeOffAmount;
|
||||
|
||||
}
|
||||
+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;
|
||||
}
|
||||
|
||||
+3
-3
@@ -70,10 +70,10 @@ public class ProjectApply extends TenantEntity {
|
||||
@Schema(description = "业务部门")
|
||||
private String businessDeptName;
|
||||
|
||||
@Schema(description = "承办部门ID")
|
||||
private Long undertakeDeptId;
|
||||
@Schema(description = "平台公司ID,多个逗号分隔")
|
||||
private String undertakeDeptId;
|
||||
|
||||
@Schema(description = "承办部门")
|
||||
@Schema(description = "平台公司,多个顿号分隔")
|
||||
private String undertakeDeptName;
|
||||
|
||||
@Schema(description = "项目由来")
|
||||
|
||||
+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;
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 导入结算单批次
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("blade_settlement_import_batch")
|
||||
@Schema(description = "导入结算单批次")
|
||||
public class SettlementImportBatch extends TenantEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "批次号")
|
||||
private String batchNo;
|
||||
|
||||
@Schema(description = "项目ID")
|
||||
private Long projectId;
|
||||
|
||||
@Schema(description = "项目名称")
|
||||
private String projectName;
|
||||
|
||||
@Schema(description = "结算类型 receivable/payable")
|
||||
private String settlementType;
|
||||
|
||||
@Schema(description = "结算单类型 formal/pre")
|
||||
private String settlementBillType;
|
||||
|
||||
@Schema(description = "客户合同ID")
|
||||
private Long customerContractId;
|
||||
|
||||
@Schema(description = "客户合同名称")
|
||||
private String customerContractName;
|
||||
|
||||
@Schema(description = "客户名称")
|
||||
private String customerName;
|
||||
|
||||
@Schema(description = "承运商合同ID")
|
||||
private Long carrierContractId;
|
||||
|
||||
@Schema(description = "承运商合同名称")
|
||||
private String carrierContractName;
|
||||
|
||||
@Schema(description = "承运商名称")
|
||||
private String carrierName;
|
||||
|
||||
@Schema(description = "运单模式 new")
|
||||
private String waybillMode;
|
||||
|
||||
@Schema(description = "导入状态 draft/completed/voided")
|
||||
private String importStatus;
|
||||
|
||||
@Schema(description = "运单数量")
|
||||
private Integer waybillCount;
|
||||
|
||||
@Schema(description = "明细数量")
|
||||
private Integer detailCount;
|
||||
|
||||
@Schema(description = "正式结算单ID")
|
||||
private Long formalSettlementId;
|
||||
|
||||
@Schema(description = "正式结算单号")
|
||||
private String formalSettlementNo;
|
||||
|
||||
@Schema(description = "预结算单ID")
|
||||
private Long preSettlementId;
|
||||
|
||||
@Schema(description = "预结算单号")
|
||||
private String preSettlementNo;
|
||||
|
||||
@Schema(description = "动态费用列名JSON")
|
||||
private String feeItemNamesJson;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "关联运单导入批次ID")
|
||||
private Long waybillImportBatchId;
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.core.tenant.mp.TenantEntity;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 结算单项目预付核销实体类
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("blade_settlement_project_advance_write_off")
|
||||
@Schema(description = "结算单项目预付核销")
|
||||
public class SettlementProjectAdvanceWriteOff extends TenantEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "结算单类型:pre/formal")
|
||||
private String settlementType;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@Schema(description = "预结算/正式结算单ID")
|
||||
private Long settlementId;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@Schema(description = "项目预付付款申请ID")
|
||||
private Long paymentApplicationId;
|
||||
|
||||
@Schema(description = "预付单号")
|
||||
private String paymentNo;
|
||||
|
||||
@Schema(description = "项目预付金额")
|
||||
private BigDecimal projectAdvanceAmount;
|
||||
|
||||
@Schema(description = "本次核销金额")
|
||||
private BigDecimal writeOffAmount;
|
||||
|
||||
@Schema(description = "金蝶预付单号")
|
||||
private String kingdeeBillNo;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "可核销金额(动态计算)")
|
||||
private BigDecimal availableWriteOffAmount;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "预付单创建人姓名")
|
||||
private String createUserName;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "预付单创建时间(展示用)")
|
||||
private Date paymentCreateTime;
|
||||
|
||||
}
|
||||
+3
@@ -249,6 +249,9 @@ public class Waybill extends TenantEntity {
|
||||
@Schema(description = "导入批次ID")
|
||||
private Long importBatchId;
|
||||
|
||||
@Schema(description = "导入结算单批次ID")
|
||||
private Long settlementImportBatchId;
|
||||
|
||||
@Schema(description = "关联单号")
|
||||
private String relationNo;
|
||||
|
||||
|
||||
+24
-1
@@ -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,10 +19,13 @@ 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.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -31,7 +35,7 @@ import java.util.List;
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class FormalSettlementVO extends FormalSettlement {
|
||||
public class FormalSettlementVO extends FormalSettlement implements KingdeeUnlockMark {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
@TableField(exist = false) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate createStartDate;
|
||||
@TableField(exist = false) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate createEndDate;
|
||||
@@ -39,6 +43,21 @@ 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) @Schema(description = "金蝶解锁时间(非空代表该单曾被金蝶解锁/删单)")
|
||||
private LocalDateTime kingdeeUnlockTime;
|
||||
@TableField(exist = false) @Schema(description = "金蝶侧解锁操作人(回调报文 operator)")
|
||||
private String kingdeeUnlockOperator;
|
||||
@TableField(exist = false) @Schema(description = "解锁动作名称:金蝶删单/金蝶锁单")
|
||||
private String kingdeeUnlockOperationName;
|
||||
@TableField(exist = false) private String preSettlementNos;
|
||||
@TableField(exist = false) private String preSettlementNo;
|
||||
@TableField(exist = false) private List<FormalSettlementSource> sources;
|
||||
@@ -46,6 +65,10 @@ 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<SettlementProjectAdvanceWriteOff> projectAdvanceWriteOffs;
|
||||
@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;
|
||||
|
||||
+16
-1
@@ -30,10 +30,12 @@ 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;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -43,14 +45,27 @@ import java.util.List;
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class InvoiceApplicationVO extends InvoiceApplication {
|
||||
public class InvoiceApplicationVO extends InvoiceApplication implements KingdeeUnlockMark {
|
||||
@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 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;
|
||||
/** 金蝶解锁终态标识(unlocked 锁记录回填):解锁时间非空即代表该单曾被金蝶解锁/删单 */
|
||||
@TableField(exist = false) private LocalDateTime kingdeeUnlockTime;
|
||||
/** 金蝶侧解锁操作人(回调报文 operator) */
|
||||
@TableField(exist = false) private String kingdeeUnlockOperator;
|
||||
/** 解锁动作名称:金蝶删单/金蝶锁单 */
|
||||
@TableField(exist = false) private String kingdeeUnlockOperationName;
|
||||
@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;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.vo;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 金蝶解锁终态标识展示契约:需在页面展示「金蝶删单/解锁」标识的 VO 实现之,
|
||||
* 由 {@code KingdeeBillLockService#fillUnlockMarks} 统一回填(列表/详情共用)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface KingdeeUnlockMark {
|
||||
|
||||
/**
|
||||
* 金蝶解锁时间(unlocked 锁记录回填),非空即代表该单曾被金蝶解锁/删单
|
||||
*/
|
||||
void setKingdeeUnlockTime(LocalDateTime kingdeeUnlockTime);
|
||||
|
||||
/**
|
||||
* 金蝶侧解锁操作人(回调报文 operator)
|
||||
*/
|
||||
void setKingdeeUnlockOperator(String kingdeeUnlockOperator);
|
||||
|
||||
/**
|
||||
* 解锁动作名称:金蝶删单/金蝶锁单
|
||||
*/
|
||||
void setKingdeeUnlockOperationName(String kingdeeUnlockOperationName);
|
||||
}
|
||||
+14
-1
@@ -29,12 +29,13 @@ import org.springblade.transport.pojo.entity.PaymentApplicationSettlement;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 付款申请视图。 @author Chill */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PaymentApplicationVO extends PaymentApplication {
|
||||
public class PaymentApplicationVO extends PaymentApplication implements KingdeeUnlockMark {
|
||||
@Serial private static final long serialVersionUID = 1L;
|
||||
@TableField(exist = false) private LocalDate applyStartDate;
|
||||
@TableField(exist = false) private LocalDate applyEndDate;
|
||||
@@ -43,6 +44,18 @@ 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;
|
||||
/** 金蝶解锁终态标识(unlocked 锁记录回填):解锁时间非空即代表该单曾被金蝶解锁/删单,供页面展示明显标识 */
|
||||
@TableField(exist = false) private LocalDateTime kingdeeUnlockTime;
|
||||
/** 金蝶侧解锁操作人(回调报文 operator) */
|
||||
@TableField(exist = false) private String kingdeeUnlockOperator;
|
||||
/** 解锁动作名称:金蝶删单/金蝶锁单 */
|
||||
@TableField(exist = false) private String kingdeeUnlockOperationName;
|
||||
@TableField(exist = false) private List<PaymentApplicationInvoice> invoices;
|
||||
@TableField(exist = false) private List<PaymentApplicationRecord> paymentRecords;
|
||||
@TableField(exist = false) private List<PaymentApplicationSettlement> settlements;
|
||||
|
||||
+5
@@ -35,6 +35,7 @@ import org.springblade.transport.pojo.entity.PreSettlementChangeRecord;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementDetail;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementDetailFee;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementSummaryFee;
|
||||
import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serial;
|
||||
@@ -100,6 +101,10 @@ public class PreSettlementVO extends PreSettlement {
|
||||
@Schema(description = "预付信息")
|
||||
private List<PreSettlementAdvance> advances;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "项目预付核销")
|
||||
private List<SettlementProjectAdvanceWriteOff> projectAdvanceWriteOffs;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "变更记录")
|
||||
private List<PreSettlementChangeRecord> changeRecords;
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 可选项目预付核销选项
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "可选项目预付核销选项")
|
||||
public class ProjectAdvanceWriteOffOptionVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@Schema(description = "付款申请ID")
|
||||
private Long paymentApplicationId;
|
||||
|
||||
@Schema(description = "预付单号")
|
||||
private String paymentNo;
|
||||
|
||||
@Schema(description = "项目预付金额")
|
||||
private BigDecimal projectAdvanceAmount;
|
||||
|
||||
@Schema(description = "可核销金额")
|
||||
private BigDecimal availableWriteOffAmount;
|
||||
|
||||
@Schema(description = "金蝶预付单号")
|
||||
private String kingdeeBillNo;
|
||||
|
||||
@Schema(description = "创建人姓名")
|
||||
private String createUserName;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
+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<>();
|
||||
}
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springblade.transport.pojo.entity.SettlementImportBatch;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 导入结算单批次视图
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "导入结算单批次视图")
|
||||
public class SettlementImportBatchVO extends SettlementImportBatch {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "创建人姓名")
|
||||
private String createUserName;
|
||||
|
||||
@Schema(description = "更新人姓名")
|
||||
private String updateUserName;
|
||||
|
||||
@Schema(description = "导入状态显示名")
|
||||
private String importStatusName;
|
||||
|
||||
@Schema(description = "结算类型显示名")
|
||||
private String settlementTypeName;
|
||||
|
||||
@Schema(description = "结算单类型显示名")
|
||||
private String settlementBillTypeName;
|
||||
|
||||
@Schema(description = "运单模式显示名")
|
||||
private String waybillModeName;
|
||||
|
||||
@Schema(description = "动态费用列名")
|
||||
private List<String> feeItemNames;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.springblade.file.config;
|
||||
import com.aliyun.oss.OSSClientBuilder;
|
||||
import com.obs.services.ObsClient;
|
||||
import io.minio.MinioClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.file.service.IFileService;
|
||||
@@ -10,6 +11,7 @@ import org.springblade.file.service.impl.LocalFileServiceImpl;
|
||||
import org.springblade.file.service.impl.MinioFileServiceImpl;
|
||||
import org.springblade.file.service.impl.OssFileServiceImpl;
|
||||
import org.springblade.file.service.impl.ObsFileServiceImpl;
|
||||
import org.springblade.file.util.MinioClientFactory;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -20,6 +22,7 @@ import org.springframework.core.env.Environment;
|
||||
* @author bfhuange
|
||||
* @date 2024/10/21
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class FileConfig {
|
||||
|
||||
@@ -63,10 +66,22 @@ public class FileConfig {
|
||||
|
||||
private IFileService buildMinioFileService(FileStorageProperties.StoreProperties properties) {
|
||||
validateStoreProperties(STORAGE_MINIO, properties);
|
||||
MinioClient minioClient = MinioClient.builder()
|
||||
.endpoint(properties.getEndpoint())
|
||||
.credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
|
||||
.build();
|
||||
String endpointOrigin = MinioClientFactory.resolveOrigin(properties.getEndpoint());
|
||||
String endpointPathPrefix = MinioClientFactory.resolvePathPrefix(properties.getEndpoint());
|
||||
if (StringUtil.isNotBlank(endpointPathPrefix)) {
|
||||
log.warn("MinIO endpoint 含 path,将按 origin={} pathPrefix={} 访问;建议 endpoint 使用内网地址,transform-endpoint 使用外网 HTTPS",
|
||||
endpointOrigin, endpointPathPrefix);
|
||||
}
|
||||
if (StringUtil.isBlank(properties.getTransformEndpoint())
|
||||
&& properties.getEndpoint().trim().toLowerCase().startsWith("http://")) {
|
||||
log.warn("MinIO transform-endpoint 未配置,endpoint={}。HTTPS 页面分片直传会被浏览器 Mixed Content 拦截,将回退为服务端代传",
|
||||
properties.getEndpoint());
|
||||
} else {
|
||||
log.info("MinIO 文件存储已启用 endpoint={} pathPrefix={} transformEndpoint={}",
|
||||
endpointOrigin, endpointPathPrefix, properties.getTransformEndpoint());
|
||||
}
|
||||
MinioClient minioClient = MinioClientFactory.create(
|
||||
properties.getEndpoint(), properties.getAccessKeyId(), properties.getAccessKeySecret());
|
||||
return new MinioFileServiceImpl(properties, minioClient);
|
||||
}
|
||||
|
||||
@@ -95,7 +110,16 @@ public class FileConfig {
|
||||
FileStorageProperties.StoreProperties mergedProperties = new FileStorageProperties.StoreProperties();
|
||||
String fileStoragePrefix = "file.storage." + prefix;
|
||||
mergedProperties.setEndpoint(firstNotBlank(properties.getEndpoint(), environment.getProperty(fileStoragePrefix + ".endpoint"), environment.getProperty(prefix + ".endpoint")));
|
||||
mergedProperties.setTransformEndpoint(firstNotBlank(properties.getTransformEndpoint(), environment.getProperty(fileStoragePrefix + ".transform-endpoint"), environment.getProperty(prefix + ".transform-endpoint")));
|
||||
// 资源管理页 /resource/oss 的外网地址写在库表,不进 Spring Environment;凭证分片必须单独配 file.storage.minio.transform-endpoint
|
||||
mergedProperties.setTransformEndpoint(firstNotBlank(
|
||||
properties.getTransformEndpoint(),
|
||||
environment.getProperty(fileStoragePrefix + ".transform-endpoint"),
|
||||
environment.getProperty(fileStoragePrefix + ".transformEndpoint"),
|
||||
environment.getProperty(prefix + ".transform-endpoint"),
|
||||
environment.getProperty(prefix + ".transformEndpoint"),
|
||||
environment.getProperty("oss.transform-endpoint"),
|
||||
environment.getProperty("oss.transformEndpoint")
|
||||
));
|
||||
mergedProperties.setAccessKeyId(firstNotBlank(properties.getAccessKeyId(), environment.getProperty(fileStoragePrefix + ".access-key-id"), environment.getProperty(fileStoragePrefix + ".access-key"), environment.getProperty(prefix + ".access-key-id"), environment.getProperty(prefix + ".access-key")));
|
||||
mergedProperties.setAccessKeySecret(firstNotBlank(properties.getAccessKeySecret(), environment.getProperty(fileStoragePrefix + ".access-key-secret"), environment.getProperty(fileStoragePrefix + ".secret-key"), environment.getProperty(prefix + ".access-key-secret"), environment.getProperty(prefix + ".secret-key")));
|
||||
mergedProperties.setBucketName(firstNotBlank(properties.getBucketName(), environment.getProperty(fileStoragePrefix + ".bucket-name"), environment.getProperty(prefix + ".bucket-name")));
|
||||
@@ -107,7 +131,7 @@ public class FileConfig {
|
||||
private String firstNotBlank(String... values) {
|
||||
for (String value : values) {
|
||||
if (StringUtil.isNotBlank(value)) {
|
||||
return value;
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
+22
@@ -111,4 +111,26 @@ public class FileTaskController extends BladeController {
|
||||
String uploadUrl = fileTaskService.getUploadUrl(id, partNumber);
|
||||
return R.data(uploadUrl);
|
||||
}
|
||||
|
||||
@ApiLog("上传任务-服务端代传分片")
|
||||
@PostMapping(value = "/uploadPart", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Parameters({
|
||||
@Parameter(name = "id", description = "文件任务id", in = ParameterIn.QUERY, schema = @Schema(type = "string")),
|
||||
@Parameter(name = "partNumber", description = "分段编号", in = ParameterIn.QUERY, schema = @Schema(type = "int"))
|
||||
})
|
||||
@Operation(summary = "服务端代传分片", description = "HTTPS 页面无法直连 HTTP MinIO 时,由服务端转发分片到对象存储")
|
||||
public R<String> uploadPart(Long id, Integer partNumber, org.springframework.web.multipart.MultipartFile file) throws java.io.IOException {
|
||||
if (file == null || file.isEmpty()) {
|
||||
return R.fail("分片内容不能为空");
|
||||
}
|
||||
java.io.File tempFile = java.io.File.createTempFile("file-part-", ".bin");
|
||||
try {
|
||||
file.transferTo(tempFile);
|
||||
return R.data(fileTaskService.uploadPart(id, partNumber, tempFile));
|
||||
} finally {
|
||||
if (tempFile.exists() && !tempFile.delete()) {
|
||||
tempFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -74,6 +74,16 @@ public interface IFileTaskService extends IService<FileTask> {
|
||||
*/
|
||||
String getUploadUrl(Long id, Integer partNumber);
|
||||
|
||||
/**
|
||||
* 服务端代传分片(HTTPS 页面无法直连 HTTP MinIO 时使用)
|
||||
*
|
||||
* @param id 文件任务id
|
||||
* @param partNumber 分段编号
|
||||
* @param partFile 分片临时文件
|
||||
* @return ETag
|
||||
*/
|
||||
String uploadPart(Long id, Integer partNumber, java.io.File partFile);
|
||||
|
||||
/**
|
||||
* 取消文件任务
|
||||
* @param taskIds
|
||||
|
||||
+27
@@ -259,6 +259,33 @@ public class FileTaskServiceImpl extends ServiceImpl<FileTaskMapper, FileTask> i
|
||||
return fileService.getUploadPartUrl(fileTask.getObjectKey(), fileTask.getUploadId(), partNumber, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadPart(Long id, Integer partNumber, java.io.File partFile) {
|
||||
FileTask fileTask = this.getById(id);
|
||||
Func.requireNotNull(fileTask, "文件任务不存在");
|
||||
if (FileTaskStatus.COMPLETED.getCode().equals(fileTask.getStatus())) {
|
||||
throw new ServiceException("文件任务已完成");
|
||||
}
|
||||
if (FileTaskStatus.PAUSED.getCode().equals(fileTask.getStatus())) {
|
||||
throw new ServiceException("文件任务已暂停");
|
||||
}
|
||||
if (partFile == null || !partFile.exists()) {
|
||||
throw new ServiceException("分片文件不存在");
|
||||
}
|
||||
org.springblade.file.pojo.vo.PartEtagVO partEtag = fileService.uploadPart(
|
||||
fileTask.getObjectKey(),
|
||||
fileTask.getUploadId(),
|
||||
partFile,
|
||||
partNumber,
|
||||
0L,
|
||||
partFile.length()
|
||||
);
|
||||
if (partEtag == null || StringUtil.isBlank(partEtag.getEtag())) {
|
||||
throw new ServiceException("分片上传未返回 ETag");
|
||||
}
|
||||
return partEtag.getEtag().replace("\"", "");
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public void cancelBatchByIds(List<Long> taskIds) {
|
||||
|
||||
+106
-6
@@ -25,11 +25,12 @@
|
||||
*/
|
||||
package org.springblade.file.service.impl;
|
||||
|
||||
import io.minio.ComposeObjectArgs;
|
||||
import io.minio.ComposeSource;
|
||||
import io.minio.GetObjectArgs;
|
||||
import io.minio.GetPresignedObjectUrlArgs;
|
||||
import io.minio.MinioClient;
|
||||
import io.minio.ComposeObjectArgs;
|
||||
import io.minio.ComposeSource;
|
||||
import io.minio.ObjectWriteResponse;
|
||||
import io.minio.PutObjectArgs;
|
||||
import io.minio.RemoveObjectArgs;
|
||||
import io.minio.UploadObjectArgs;
|
||||
@@ -39,9 +40,13 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.file.config.FileStorageProperties;
|
||||
import org.springblade.file.util.MinioClientFactory;
|
||||
import org.springblade.file.pojo.vo.PartEtagVO;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
@@ -61,9 +66,11 @@ import java.util.UUID;
|
||||
public class MinioFileServiceImpl extends AbstractFileService {
|
||||
|
||||
private static final long PART_SIZE = 10 * 1024 * 1024L;
|
||||
private static final String DEFAULT_REGION = MinioClientFactory.DEFAULT_REGION;
|
||||
|
||||
private final FileStorageProperties.StoreProperties properties;
|
||||
private final MinioClient minioClient;
|
||||
private volatile MinioClient transformPresignClient;
|
||||
|
||||
@Override
|
||||
public String initiateMultipartUpload(String objectKey) {
|
||||
@@ -73,15 +80,50 @@ public class MinioFileServiceImpl extends AbstractFileService {
|
||||
@Override
|
||||
public String getUploadPartUrl(String objectKey, String uploadId, Integer partNumber, Long expires) {
|
||||
try {
|
||||
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.PUT).bucket(properties.getBucketName()).object(partObjectKey(objectKey, uploadId, partNumber))
|
||||
.expiry(Math.toIntExact(resolveExpires(expires))).build());
|
||||
String presignedUrl = resolvePresignClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.PUT)
|
||||
.bucket(properties.getBucketName())
|
||||
.object(partObjectKey(objectKey, uploadId, partNumber))
|
||||
.region(DEFAULT_REGION)
|
||||
.expiry(Math.toIntExact(resolveExpires(expires)))
|
||||
.build());
|
||||
return applyTransformPathPrefix(presignedUrl);
|
||||
} catch (Exception exception) {
|
||||
log.error("MinIO获取分片上传链接异常 objectKey:{}", objectKey, exception);
|
||||
throw new ServiceException("获取分片上传链接异常");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PartEtagVO uploadPart(String objectKey, String uploadId, File file, int partNumber, Long offset, Long partSize) {
|
||||
long start = offset == null ? 0L : offset;
|
||||
long size = partSize == null ? Math.max(0L, file.length() - start) : partSize;
|
||||
try (FileInputStream inputStream = new FileInputStream(file)) {
|
||||
long skipped = inputStream.skip(start);
|
||||
if (skipped < start) {
|
||||
throw new ServiceException("MinIO分段上传读取文件偏移失败");
|
||||
}
|
||||
ObjectWriteResponse response = minioClient.putObject(PutObjectArgs.builder()
|
||||
.bucket(properties.getBucketName())
|
||||
.object(partObjectKey(objectKey, uploadId, partNumber))
|
||||
.stream(inputStream, size, PART_SIZE)
|
||||
.build());
|
||||
String etag = response.etag() == null ? "" : response.etag().replace("\"", "");
|
||||
return new PartEtagVO(etag, partNumber);
|
||||
} catch (ServiceException serviceException) {
|
||||
throw serviceException;
|
||||
} catch (Exception exception) {
|
||||
log.error("MinIO分段上传异常 objectKey:{} partNumber:{}", objectKey, partNumber, exception);
|
||||
throw new ServiceException("MinIO分段上传异常");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelMultipartUpload(String objectKey, String uploadId) {
|
||||
// MinIO 分片以独立对象模拟,取消时由任务层清理业务状态即可
|
||||
log.debug("MinIO取消分段上传 objectKey:{} uploadId:{}", objectKey, uploadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeMultipartUpload(String objectKey, String uploadId, List<org.springblade.file.pojo.vo.PartEtagVO> partEtags) {
|
||||
try {
|
||||
@@ -143,12 +185,14 @@ public class MinioFileServiceImpl extends AbstractFileService {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
String presignedUrl = resolvePresignClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET)
|
||||
.bucket(properties.getBucketName())
|
||||
.object(resolveObjectKey(objectKey))
|
||||
.region(DEFAULT_REGION)
|
||||
.expiry(Math.toIntExact(resolveExpires(expires)))
|
||||
.build());
|
||||
return applyTransformPathPrefix(presignedUrl);
|
||||
} catch (Exception exception) {
|
||||
log.error("MinIO获取文件链接异常 objectKey:{}", objectKey, exception);
|
||||
throw new ServiceException("MinIO获取文件链接异常");
|
||||
@@ -170,6 +214,62 @@ public class MinioFileServiceImpl extends AbstractFileService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器侧预签名优先使用外网地址,避免 HTTPS 页面访问 HTTP MinIO 被 Mixed Content 拦截。
|
||||
* MinIO SDK 不允许 endpoint 带 path,因此外网地址中的 /oss 等前缀在签名后单独拼回。
|
||||
*/
|
||||
private MinioClient resolvePresignClient() {
|
||||
String transformEndpoint = properties.getTransformEndpoint();
|
||||
if (StringUtil.isBlank(transformEndpoint)) {
|
||||
return minioClient;
|
||||
}
|
||||
MinioClient cachedClient = transformPresignClient;
|
||||
if (cachedClient != null) {
|
||||
return cachedClient;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (transformPresignClient == null) {
|
||||
transformPresignClient = MinioClientFactory.create(
|
||||
transformEndpoint, properties.getAccessKeyId(), properties.getAccessKeySecret());
|
||||
}
|
||||
return transformPresignClient;
|
||||
}
|
||||
}
|
||||
|
||||
private String applyTransformPathPrefix(String presignedUrl) {
|
||||
String transformEndpoint = properties.getTransformEndpoint();
|
||||
if (StringUtil.isBlank(transformEndpoint) || StringUtil.isBlank(presignedUrl)) {
|
||||
return presignedUrl;
|
||||
}
|
||||
String pathPrefix = normalizePathPrefix(URI.create(transformEndpoint.trim()).getPath());
|
||||
if (StringUtil.isBlank(pathPrefix)) {
|
||||
return presignedUrl;
|
||||
}
|
||||
URI url = URI.create(presignedUrl);
|
||||
String path = url.getRawPath() == null ? "" : url.getRawPath();
|
||||
if (path.equals(pathPrefix) || path.startsWith(pathPrefix + "/")) {
|
||||
return presignedUrl;
|
||||
}
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append(url.getScheme()).append("://").append(url.getHost());
|
||||
if (url.getPort() > 0) {
|
||||
result.append(':').append(url.getPort());
|
||||
}
|
||||
result.append(pathPrefix).append(path);
|
||||
if (StringUtil.isNotBlank(url.getRawQuery())) {
|
||||
result.append('?').append(url.getRawQuery());
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private String normalizePathPrefix(String path) {
|
||||
if (StringUtil.isBlank(path) || "/".equals(path)) {
|
||||
return "";
|
||||
}
|
||||
String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
|
||||
return normalized.startsWith("/") ? normalized : "/" + normalized;
|
||||
}
|
||||
|
||||
private String resolveObjectKey(String objectKey) {
|
||||
if (StringUtil.isBlank(properties.getRootDirectory())) {
|
||||
return objectKey;
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.file.util;
|
||||
|
||||
import io.minio.MinioClient;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* MinIO 客户端构建。
|
||||
* <p>
|
||||
* SDK 不允许 endpoint 带 path;若配置为 {@code https://host/oss},则:
|
||||
* <ul>
|
||||
* <li>endpoint 使用 origin</li>
|
||||
* <li>通过 OkHttp 拦截器把 /oss 拼回每个 API 请求</li>
|
||||
* <li>固定 region,避免 GetBucketLocation 打到业务网关拿到 HTML</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class MinioClientFactory {
|
||||
|
||||
public static final String DEFAULT_REGION = "us-east-1";
|
||||
|
||||
private MinioClientFactory() {
|
||||
}
|
||||
|
||||
public static MinioClient create(String endpoint, String accessKey, String secretKey) {
|
||||
EndpointParts parts = parseEndpoint(endpoint);
|
||||
MinioClient.Builder builder = MinioClient.builder()
|
||||
.endpoint(parts.origin())
|
||||
.credentials(accessKey, secretKey)
|
||||
.region(DEFAULT_REGION);
|
||||
if (StringUtil.isNotBlank(parts.pathPrefix())) {
|
||||
builder.httpClient(buildHttpClient(parts.pathPrefix()), true);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static String resolveOrigin(String endpoint) {
|
||||
return parseEndpoint(endpoint).origin();
|
||||
}
|
||||
|
||||
public static String resolvePathPrefix(String endpoint) {
|
||||
return parseEndpoint(endpoint).pathPrefix();
|
||||
}
|
||||
|
||||
private static EndpointParts parseEndpoint(String endpoint) {
|
||||
if (StringUtil.isBlank(endpoint)) {
|
||||
throw new ServiceException("MinIO endpoint 不能为空");
|
||||
}
|
||||
URI uri = URI.create(endpoint.trim());
|
||||
if (StringUtil.isBlank(uri.getScheme()) || StringUtil.isBlank(uri.getHost())) {
|
||||
throw new ServiceException("MinIO endpoint 配置不正确: " + endpoint);
|
||||
}
|
||||
StringBuilder origin = new StringBuilder();
|
||||
origin.append(uri.getScheme()).append("://").append(uri.getHost());
|
||||
if (uri.getPort() > 0) {
|
||||
origin.append(':').append(uri.getPort());
|
||||
}
|
||||
return new EndpointParts(origin.toString(), normalizePathPrefix(uri.getPath()));
|
||||
}
|
||||
|
||||
private static String normalizePathPrefix(String path) {
|
||||
if (StringUtil.isBlank(path) || "/".equals(path)) {
|
||||
return "";
|
||||
}
|
||||
String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
|
||||
return normalized.startsWith("/") ? normalized : "/" + normalized;
|
||||
}
|
||||
|
||||
private static OkHttpClient buildHttpClient(String pathPrefix) {
|
||||
return new OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(chain -> {
|
||||
Request original = chain.request();
|
||||
HttpUrl originalUrl = original.url();
|
||||
String encodedPath = originalUrl.encodedPath();
|
||||
if (encodedPath.startsWith(pathPrefix + "/") || encodedPath.equals(pathPrefix)) {
|
||||
return chain.proceed(original);
|
||||
}
|
||||
HttpUrl newUrl = originalUrl.newBuilder()
|
||||
.encodedPath(pathPrefix + encodedPath)
|
||||
.build();
|
||||
return chain.proceed(original.newBuilder().url(newUrl).build());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private record EndpointParts(String origin, String pathPrefix) {
|
||||
}
|
||||
}
|
||||
+29
-44
@@ -44,94 +44,79 @@ public interface DeptConvert {
|
||||
|
||||
@Mapping(source = "subcompanyname", target = "deptName")
|
||||
@Mapping(source = "subcompanydesc", target = "fullName")
|
||||
// 父公司id转为父部门编码,要拼接前缀
|
||||
@Mapping(source = "supsubcomid", target = "parentCode")
|
||||
@Mapping(source = "created", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
// OA modified 单独落库,用于增量比对,避免覆盖本系统 updateTime
|
||||
@Mapping(source = "modified", target = "oaModified")
|
||||
Dept baseConvert(OACompanyResponse company);
|
||||
|
||||
@Mapping(source = "departmentname", target = "deptName")
|
||||
@Mapping(source = "departmentmark", target = "fullName")
|
||||
// 父部门id转为父部门编码,要拼接前缀
|
||||
@Mapping(source = "supdepid", target = "parentCode")
|
||||
// 所属子公司id转为所属公司编码,要拼接前缀
|
||||
@Mapping(source = "subcompanyid1", target = "belongCompanyCode")
|
||||
@Mapping(source = "created", target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
// OA modified 单独落库,用于增量比对,避免覆盖本系统 updateTime
|
||||
@Mapping(source = "modified", target = "oaModified")
|
||||
Dept baseConvert(OADepartmentResponse department);
|
||||
|
||||
/**
|
||||
* oa公司转本系统部门
|
||||
* @param company
|
||||
* @return
|
||||
* oa公司转本系统部门。组织编码由同步服务按 GWWL+三位数字 统一分配。
|
||||
*/
|
||||
default Dept company2dept(OACompanyResponse company) {
|
||||
Dept dept = this.baseConvert(company);
|
||||
// 部门编码添加前缀
|
||||
dept.setDeptCode(OAConvertConstant.COMPANY_OA_PREFIX + company.getId());
|
||||
// 编码稍后按规则生成;此处仅落 OA 主键,供按 oaId 匹配与重建层级
|
||||
dept.setDeptCode(null);
|
||||
String parentCode = dept.getParentCode();
|
||||
if (!OAConstant.ROOT_COMPANY_ID.equals(parentCode)) {
|
||||
// 父部门编码不是0,拼接前缀
|
||||
dept.setParentCode(OAConvertConstant.COMPANY_OA_PREFIX + parentCode);
|
||||
// 父部门id后续处理
|
||||
// 非顶级:parentCode 暂存上级 OA 主键,分配编码后再替换为正式组织编码
|
||||
dept.setParentCode(parentCode);
|
||||
dept.setParentId(null);
|
||||
} else {
|
||||
// 父部门编码是0,父id也设置为0
|
||||
dept.setParentId(OAConvertConstant.ROOT_PARENT_ID);
|
||||
dept.setParentCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
dept.setParentId(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
|
||||
}
|
||||
// 公司所属公司编码就是他自己的部门编码
|
||||
dept.setBelongCompanyCode(dept.getDeptCode());
|
||||
// OA主键与上级OA主键,分批同步结束后用于重建层级
|
||||
dept.setBelongCompanyCode(null);
|
||||
dept.setOaId(company.getId());
|
||||
dept.setOaSupSubComId(company.getSupsubcomid());
|
||||
// 显示顺序处理
|
||||
dept.setSort(OAUtils.parseInt(company.getShoworder()));
|
||||
// 部门类型,公司
|
||||
dept.setDeptCategory(DeptCategory.COMPANY.getCode());
|
||||
dept.setSyncTime(new Date());
|
||||
// 是oa的
|
||||
dept.setIsOa(CommonConstant.YES);
|
||||
return dept;
|
||||
}
|
||||
|
||||
/**
|
||||
* oa部门转本系统部门
|
||||
* @param oaDept
|
||||
* @param companyDeptMap 所有公司的部门编码与id map
|
||||
* @return
|
||||
* oa部门转本系统部门。组织编码由同步服务按「所属公司编码+三位数字」统一分配。
|
||||
*
|
||||
* @param oaDept OA部门
|
||||
* @param companyByOaId 已同步公司(oaId → 本地公司)
|
||||
*/
|
||||
default Dept dept2dept(OADepartmentResponse oaDept, Map<String, Long> companyDeptMap) {
|
||||
default Dept dept2dept(OADepartmentResponse oaDept, Map<String, Dept> companyByOaId) {
|
||||
Dept dept = this.baseConvert(oaDept);
|
||||
// 部门编码添加前缀
|
||||
dept.setDeptCode(OAConvertConstant.DEPARTMENT_OA_PREFIX + oaDept.getId());
|
||||
// OA主键;上级为根部门时记录所属公司OA主键,否则记录上级部门OA主键
|
||||
dept.setDeptCode(null);
|
||||
dept.setOaId(oaDept.getId());
|
||||
if (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid())) {
|
||||
dept.setOaSupSubComId(oaDept.getSubcompanyid1());
|
||||
} else {
|
||||
dept.setOaSupSubComId(oaDept.getSupdepid());
|
||||
}
|
||||
// 部门所属公司编码就是他所属公司编码加前缀
|
||||
dept.setBelongCompanyCode(OAConvertConstant.COMPANY_OA_PREFIX + dept.getBelongCompanyCode());
|
||||
String parentCode = dept.getParentCode();
|
||||
if (!OAConstant.ROOT_COMPANY_ID.equals(parentCode)) {
|
||||
// 父部门编码不是0,拼接前缀
|
||||
dept.setParentCode(OAConvertConstant.DEPARTMENT_OA_PREFIX + parentCode);
|
||||
// 父部门id后续处理
|
||||
Dept belongCompany = companyByOaId == null ? null : companyByOaId.get(oaDept.getSubcompanyid1());
|
||||
if (belongCompany != null) {
|
||||
dept.setBelongCompanyCode(belongCompany.getDeptCode());
|
||||
dept.setParentId(null);
|
||||
if (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid())) {
|
||||
dept.setParentCode(belongCompany.getDeptCode());
|
||||
dept.setParentId(belongCompany.getId());
|
||||
} else {
|
||||
// 上级为部门:parentCode 暂存上级部门 OA 主键,重建层级时再解析
|
||||
dept.setParentCode(oaDept.getSupdepid());
|
||||
}
|
||||
} else {
|
||||
// 父部门编码是0,设置为公司编码
|
||||
dept.setParentCode(dept.getBelongCompanyCode());
|
||||
// 根据父部门编码获取对应的id
|
||||
dept.setParentId(companyDeptMap.get(dept.getParentCode()));
|
||||
// 所属公司尚未解析时,暂存 OA 公司主键,后续补齐正式编码
|
||||
dept.setBelongCompanyCode(oaDept.getSubcompanyid1());
|
||||
dept.setParentCode(oaDept.getSupdepid());
|
||||
}
|
||||
|
||||
// 显示顺序处理
|
||||
dept.setSort(OAUtils.parseInt(oaDept.getShoworder()));
|
||||
// 部门类型,部门
|
||||
dept.setDeptCategory(DeptCategory.DEPT.getCode());
|
||||
dept.setSyncTime(new Date());
|
||||
// 是oa的
|
||||
dept.setIsOa(CommonConstant.YES);
|
||||
return dept;
|
||||
}
|
||||
|
||||
+19
@@ -88,6 +88,8 @@ public interface UserConvert {
|
||||
default User person2user(OAPersonResponse person, String defaultPassword) {
|
||||
User user = baseConvert(person);
|
||||
user.setAccount(resolveAccount(person));
|
||||
// OA 可能返回多号(如 133.../137...),只保留首个号码再入库加密
|
||||
user.setPhone(normalizeMobile(user.getPhone()));
|
||||
// 密码
|
||||
user.setPassword(defaultPassword);
|
||||
// 性别
|
||||
@@ -103,6 +105,23 @@ public interface UserConvert {
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化手机号:去空白,多号时取第一个
|
||||
*/
|
||||
default String normalizeMobile(String mobile) {
|
||||
if (StringUtils.isBlank(mobile)) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = mobile.trim();
|
||||
String[] parts = trimmed.split("[/|,;;,\\s]+");
|
||||
for (String part : parts) {
|
||||
if (StringUtils.isNotBlank(part)) {
|
||||
return part.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析本系统登录账号:优先 OA loginid,其次工号,最后手机号
|
||||
*
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
@@ -34,6 +34,26 @@ import java.util.List;
|
||||
*/
|
||||
public interface MeasurementUnitMapper extends BaseMapper<MeasurementUnit> {
|
||||
|
||||
/**
|
||||
* 按编码查询计量单位(包含逻辑删除记录,用于新增恢复)。
|
||||
*/
|
||||
MeasurementUnit selectByUnitCodeIncludingDeleted(@Param("unitCode") String unitCode);
|
||||
|
||||
/**
|
||||
* 按名称查询计量单位(包含逻辑删除记录,用于新增恢复)。
|
||||
*/
|
||||
MeasurementUnit selectByUnitNameIncludingDeleted(@Param("unitName") String unitName);
|
||||
|
||||
/**
|
||||
* 恢复逻辑删除计量单位。
|
||||
*/
|
||||
int restoreById(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 释放已软删除记录占用的单位名称,避免唯一索引冲突。
|
||||
*/
|
||||
int releaseUnitName(@Param("id") Long id, @Param("unitName") String unitName);
|
||||
|
||||
/**
|
||||
* 自定义分页
|
||||
*
|
||||
|
||||
+28
@@ -19,6 +19,34 @@
|
||||
<result column="remark" property="remark"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByUnitCodeIncludingDeleted" resultType="org.springblade.system.pojo.entity.MeasurementUnit">
|
||||
SELECT *
|
||||
FROM blade_measurement_unit
|
||||
WHERE unit_code = #{unitCode}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectByUnitNameIncludingDeleted" resultType="org.springblade.system.pojo.entity.MeasurementUnit">
|
||||
SELECT *
|
||||
FROM blade_measurement_unit
|
||||
WHERE unit_name = #{unitName}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<update id="restoreById">
|
||||
UPDATE blade_measurement_unit
|
||||
SET is_deleted = 0
|
||||
WHERE id = #{id}
|
||||
AND is_deleted = 1
|
||||
</update>
|
||||
|
||||
<update id="releaseUnitName">
|
||||
UPDATE blade_measurement_unit
|
||||
SET unit_name = #{unitName}
|
||||
WHERE id = #{id}
|
||||
AND is_deleted = 1
|
||||
</update>
|
||||
|
||||
<select id="selectMeasurementUnitPage" resultMap="measurementUnitResultMap">
|
||||
SELECT
|
||||
mmu.id,
|
||||
|
||||
+7
@@ -40,6 +40,13 @@ import java.util.List;
|
||||
*/
|
||||
public interface UserDeptMapper extends BaseMapper<UserDept> {
|
||||
|
||||
/**
|
||||
* 提高当前会话 group_concat 上限,需与 queryUserDeptIds 在同一事务/连接中调用
|
||||
*
|
||||
* @param maxLen 最大长度
|
||||
*/
|
||||
void setGroupConcatMaxLen(@Param("maxLen") int maxLen);
|
||||
|
||||
/**
|
||||
* 根据用户id列表查询对应的部门id列表拼接的字符串
|
||||
* @param userIds
|
||||
|
||||
+4
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.springblade.system.mapper.UserDeptMapper">
|
||||
|
||||
<update id="setGroupConcatMaxLen">
|
||||
SET SESSION group_concat_max_len = #{maxLen}
|
||||
</update>
|
||||
|
||||
<select id="queryUserDeptIds" resultType="org.springblade.system.pojo.vo.UserDeptIdsVO">
|
||||
select
|
||||
group_concat(distinct bd.id) deptIds,
|
||||
|
||||
+2
-1
@@ -30,6 +30,7 @@ import org.springblade.core.literule.core.RuleComponent;
|
||||
import org.springblade.core.tool.constant.BladeConstant;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.system.pojo.entity.Tenant;
|
||||
import org.springblade.system.pojo.enums.DeptCategory;
|
||||
import org.springblade.system.rule.context.TenantContext;
|
||||
|
||||
import static org.springblade.system.rule.constant.TenantRuleConstant.TENANT_DEPT_RULE;
|
||||
@@ -54,7 +55,7 @@ public class TenantDeptRule extends RuleComponent {
|
||||
dept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
dept.setDeptName(tenant.getTenantName());
|
||||
dept.setFullName(tenant.getTenantName());
|
||||
dept.setDeptCategory(1);
|
||||
dept.setDeptCategory(DeptCategory.GROUP.getCode());
|
||||
dept.setSort(2);
|
||||
dept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
|
||||
|
||||
|
||||
+8
-1
@@ -149,12 +149,19 @@ public interface IDeptService extends IService<Dept> {
|
||||
boolean removeDept(String ids);
|
||||
|
||||
/**
|
||||
* 删除非顶级组织,顶级组织(父级为 0)保留
|
||||
* 清除非保留顶级外的组织;保留 id=1 集团锚点,以及名称为「外部组织」的顶级
|
||||
*
|
||||
* @return 删除数量
|
||||
*/
|
||||
int clearNonTopDepts();
|
||||
|
||||
/**
|
||||
* 确保双顶级:id=1 集团(编码 GWWL)+ 独立「外部组织」顶级
|
||||
*
|
||||
* @return 集团顶级组织(id=1)
|
||||
*/
|
||||
Dept ensureOaSyncRootDept();
|
||||
|
||||
/**
|
||||
* 新增或修改部门
|
||||
* <p>
|
||||
|
||||
+343
-26
@@ -27,12 +27,14 @@ package org.springblade.system.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.common.constant.CommonConstant;
|
||||
import org.springblade.common.constant.DataStatusEnum;
|
||||
import org.springblade.core.cache.utils.CacheUtil;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
@@ -389,16 +391,256 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int clearNonTopDepts() {
|
||||
Long nonTopCount = this.count(Wrappers.<Dept>lambdaQuery()
|
||||
.isNotNull(Dept::getParentId)
|
||||
.ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID));
|
||||
if (nonTopCount == null || nonTopCount == 0L) {
|
||||
// 同步前清理:保留集团锚点 id=1,以及「外部组织」顶级;其余删除
|
||||
Dept groupRoot = this.ensureOaSyncRootDept();
|
||||
Dept externalRoot = this.ensureExternalOrgTopDept();
|
||||
Set<Long> keepIds = new HashSet<>();
|
||||
if (groupRoot != null && groupRoot.getId() != null) {
|
||||
keepIds.add(groupRoot.getId());
|
||||
}
|
||||
if (externalRoot != null && externalRoot.getId() != null) {
|
||||
keepIds.add(externalRoot.getId());
|
||||
}
|
||||
if (keepIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
this.remove(Wrappers.<Dept>lambdaQuery()
|
||||
.isNotNull(Dept::getParentId)
|
||||
.ne(Dept::getParentId, BladeConstant.TOP_PARENT_ID));
|
||||
return nonTopCount.intValue();
|
||||
Long clearCount = this.count(Wrappers.<Dept>lambdaQuery().notIn(Dept::getId, keepIds));
|
||||
if (clearCount == null || clearCount == 0L) {
|
||||
return 0;
|
||||
}
|
||||
this.remove(Wrappers.<Dept>lambdaQuery().notIn(Dept::getId, keepIds));
|
||||
return clearCount.intValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Dept ensureOaSyncRootDept() {
|
||||
this.migrateLegacyOaDeptCategory();
|
||||
this.removeLegacyOaSyncRoots();
|
||||
this.migrateLegacyGroupRootToIdOne();
|
||||
Dept rootDept = this.getById(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
|
||||
String tenantId = AuthUtil.getTenantId();
|
||||
if (StringUtil.isBlank(tenantId)) {
|
||||
tenantId = BladeConstant.ADMIN_TENANT_ID;
|
||||
}
|
||||
if (rootDept == null) {
|
||||
rootDept = new Dept();
|
||||
rootDept.setId(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
|
||||
rootDept.setTenantId(tenantId);
|
||||
rootDept.setParentId(BladeConstant.TOP_PARENT_ID);
|
||||
rootDept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
rootDept.setDeptName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
rootDept.setFullName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
rootDept.setShortName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
rootDept.setDeptCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
rootDept.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
rootDept.setBelongCompanyCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
rootDept.setDeptCategory(DeptCategory.GROUP.getCode());
|
||||
rootDept.setSort(0);
|
||||
rootDept.setStatus(DataStatusEnum.ENABLE.getCode());
|
||||
rootDept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
|
||||
rootDept.setIsOa(0);
|
||||
rootDept.setIsPlatformCompany(0);
|
||||
rootDept.setSyncTime(new Date());
|
||||
this.save(rootDept);
|
||||
log.info("已创建 OA 同步集团顶级 id={} name={} code={}",
|
||||
OAConvertConstant.OA_SYNC_ROOT_DEPT_ID,
|
||||
OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME,
|
||||
OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
} else {
|
||||
boolean wronglyNamedAsExternal = OAConvertConstant.EXTERNAL_ORG_NAME.equals(rootDept.getDeptName());
|
||||
boolean legacyNamed = OAConvertConstant.LEGACY_ROOT_DEPT_NAME.equals(rootDept.getDeptName());
|
||||
boolean needRename = wronglyNamedAsExternal || legacyNamed || StringUtil.isBlank(rootDept.getDeptName())
|
||||
|| !OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME.equals(rootDept.getDeptName());
|
||||
boolean needUpdate = needRename
|
||||
|| !OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE.equals(rootDept.getDeptCode())
|
||||
|| !DeptCategory.GROUP.getCode().equals(rootDept.getDeptCategory())
|
||||
|| !Objects.equals(BladeConstant.TOP_PARENT_ID, rootDept.getParentId());
|
||||
if (needUpdate) {
|
||||
Dept updateParam = new Dept();
|
||||
updateParam.setId(rootDept.getId());
|
||||
if (needRename) {
|
||||
updateParam.setDeptName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
updateParam.setFullName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
updateParam.setShortName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
rootDept.setDeptName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
rootDept.setFullName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
rootDept.setShortName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
}
|
||||
updateParam.setDeptCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
updateParam.setBelongCompanyCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
updateParam.setDeptCategory(DeptCategory.GROUP.getCode());
|
||||
updateParam.setParentId(BladeConstant.TOP_PARENT_ID);
|
||||
updateParam.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
updateParam.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
updateParam.setIsOa(0);
|
||||
this.updateById(updateParam);
|
||||
rootDept.setDeptCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
rootDept.setBelongCompanyCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
rootDept.setDeptCategory(DeptCategory.GROUP.getCode());
|
||||
rootDept.setParentId(BladeConstant.TOP_PARENT_ID);
|
||||
rootDept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
rootDept.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
rootDept.setIsOa(0);
|
||||
log.info("已校正 OA 同步集团顶级 id={} name={} code={} category={}",
|
||||
rootDept.getId(), rootDept.getDeptName(), rootDept.getDeptCode(), rootDept.getDeptCategory());
|
||||
}
|
||||
}
|
||||
this.ensureExternalOrgTopDept();
|
||||
return rootDept;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将历史集团伪顶级 id=135 迁移到 id=1(子节点改挂,再删除旧锚点)
|
||||
*/
|
||||
private void migrateLegacyGroupRootToIdOne() {
|
||||
Long legacyRootId = OAConvertConstant.LEGACY_OA_SYNC_ROOT_DEPT_ID;
|
||||
Long newRootId = OAConvertConstant.OA_SYNC_ROOT_DEPT_ID;
|
||||
if (Objects.equals(legacyRootId, newRootId)) {
|
||||
return;
|
||||
}
|
||||
Dept legacyRoot = this.getById(legacyRootId);
|
||||
if (legacyRoot == null) {
|
||||
return;
|
||||
}
|
||||
// 仅迁移历史伪顶级;若 id=135 已是真实 OA 公司记录则跳过删除
|
||||
boolean isOaCompanyRecord = StringUtil.isNotBlank(legacyRoot.getOaId())
|
||||
&& DeptCategory.COMPANY.getCode().equals(legacyRoot.getDeptCategory())
|
||||
&& (Objects.equals(legacyRoot.getIsOa(), CommonConstant.YES) || Objects.equals(legacyRoot.getIsOa(), 1));
|
||||
if (isOaCompanyRecord) {
|
||||
log.warn("历史 id={} 已是 OA 同步公司(oaId={}),跳过伪顶级迁移", legacyRootId, legacyRoot.getOaId());
|
||||
return;
|
||||
}
|
||||
Dept newRoot = this.getById(newRootId);
|
||||
String tenantId = AuthUtil.getTenantId();
|
||||
if (StringUtil.isBlank(tenantId)) {
|
||||
tenantId = BladeConstant.ADMIN_TENANT_ID;
|
||||
}
|
||||
if (newRoot == null) {
|
||||
newRoot = new Dept();
|
||||
newRoot.setId(newRootId);
|
||||
newRoot.setTenantId(StringUtil.isNotBlank(legacyRoot.getTenantId()) ? legacyRoot.getTenantId() : tenantId);
|
||||
newRoot.setParentId(BladeConstant.TOP_PARENT_ID);
|
||||
newRoot.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
newRoot.setDeptName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
newRoot.setFullName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
newRoot.setShortName(OAConvertConstant.OA_SYNC_ROOT_DEPT_NAME);
|
||||
newRoot.setDeptCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
newRoot.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
newRoot.setBelongCompanyCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
newRoot.setDeptCategory(DeptCategory.GROUP.getCode());
|
||||
newRoot.setSort(0);
|
||||
newRoot.setStatus(DataStatusEnum.ENABLE.getCode());
|
||||
newRoot.setIsDeleted(BladeConstant.DB_NOT_DELETED);
|
||||
newRoot.setIsOa(0);
|
||||
newRoot.setIsPlatformCompany(0);
|
||||
newRoot.setSyncTime(new Date());
|
||||
this.save(newRoot);
|
||||
log.info("迁移创建新集团顶级 id={}(来自历史 id={})", newRootId, legacyRootId);
|
||||
}
|
||||
this.update(Wrappers.<Dept>lambdaUpdate()
|
||||
.set(Dept::getParentId, newRootId)
|
||||
.set(Dept::getParentCode, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE)
|
||||
.eq(Dept::getParentId, legacyRootId));
|
||||
this.removeById(legacyRootId);
|
||||
log.info("已将历史集团顶级 id={} 迁移到 id={},并删除旧锚点", legacyRootId, newRootId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保存在独立的「外部组织」顶级(与 id=1 集团并存,仅保留一个同名顶级)
|
||||
*/
|
||||
private Dept ensureExternalOrgTopDept() {
|
||||
List<Dept> externalTops = this.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptName, OAConvertConstant.EXTERNAL_ORG_NAME)
|
||||
.and(wrapper -> wrapper
|
||||
.isNull(Dept::getParentId)
|
||||
.or()
|
||||
.eq(Dept::getParentId, BladeConstant.TOP_PARENT_ID))
|
||||
.ne(Dept::getId, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID)
|
||||
.orderByAsc(Dept::getId));
|
||||
if (CollectionUtil.isNotEmpty(externalTops)) {
|
||||
Dept keep = externalTops.get(0);
|
||||
if (externalTops.size() > 1) {
|
||||
List<Long> duplicateIds = externalTops.stream()
|
||||
.skip(1)
|
||||
.map(Dept::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(duplicateIds)) {
|
||||
this.update(Wrappers.<Dept>lambdaUpdate()
|
||||
.set(Dept::getParentId, keep.getId())
|
||||
.in(Dept::getParentId, duplicateIds));
|
||||
this.removeByIds(duplicateIds);
|
||||
log.info("已合并重复的外部组织顶级,保留 id={},删除{}", keep.getId(), duplicateIds);
|
||||
}
|
||||
}
|
||||
return keep;
|
||||
}
|
||||
String tenantId = AuthUtil.getTenantId();
|
||||
if (StringUtil.isBlank(tenantId)) {
|
||||
tenantId = BladeConstant.ADMIN_TENANT_ID;
|
||||
}
|
||||
Dept externalDept = new Dept();
|
||||
externalDept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
externalDept.setTenantId(tenantId);
|
||||
externalDept.setParentId(BladeConstant.TOP_PARENT_ID);
|
||||
externalDept.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
externalDept.setDeptName(OAConvertConstant.EXTERNAL_ORG_NAME);
|
||||
externalDept.setFullName(OAConvertConstant.EXTERNAL_ORG_NAME);
|
||||
externalDept.setShortName(OAConvertConstant.EXTERNAL_ORG_NAME);
|
||||
externalDept.setDeptCode(null);
|
||||
externalDept.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
externalDept.setDeptCategory(DeptCategory.GROUP.getCode());
|
||||
externalDept.setSort(0);
|
||||
externalDept.setStatus(DataStatusEnum.ENABLE.getCode());
|
||||
externalDept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
|
||||
externalDept.setIsOa(0);
|
||||
externalDept.setIsPlatformCompany(0);
|
||||
externalDept.setSyncTime(new Date());
|
||||
this.save(externalDept);
|
||||
log.info("已创建外部组织顶级 id={}", externalDept.getId());
|
||||
return externalDept;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理历史伪顶级(OACROOT);同名旧集团仅清理非 id=1 的顶级
|
||||
*/
|
||||
private void removeLegacyOaSyncRoots() {
|
||||
List<Dept> legacyRoots = this.list(Wrappers.<Dept>lambdaQuery()
|
||||
.ne(Dept::getId, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID)
|
||||
.ne(Dept::getId, OAConvertConstant.LEGACY_OA_SYNC_ROOT_DEPT_ID)
|
||||
.and(wrapper -> wrapper
|
||||
.eq(Dept::getDeptCode, OAConvertConstant.LEGACY_ROOT_DEPT_CODE)
|
||||
.or(nameWrapper -> nameWrapper
|
||||
.eq(Dept::getDeptName, OAConvertConstant.LEGACY_ROOT_DEPT_NAME)
|
||||
.eq(Dept::getDeptCode, OAConvertConstant.LEGACY_ROOT_DEPT_CODE)
|
||||
.and(parentWrapper -> parentWrapper
|
||||
.isNull(Dept::getParentId)
|
||||
.or()
|
||||
.eq(Dept::getParentId, BladeConstant.TOP_PARENT_ID)))));
|
||||
if (CollectionUtil.isEmpty(legacyRoots)) {
|
||||
return;
|
||||
}
|
||||
Set<Long> legacyIds = legacyRoots.stream().map(Dept::getId).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
this.update(Wrappers.<Dept>lambdaUpdate()
|
||||
.set(Dept::getParentId, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID)
|
||||
.set(Dept::getParentCode, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE)
|
||||
.in(Dept::getParentId, legacyIds));
|
||||
this.removeByIds(legacyIds);
|
||||
log.info("已清理历史伪顶级组织{}条:{}", legacyIds.size(), legacyIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按编码前缀对齐组织类型:OAC→公司、OAD→部门(覆盖历史 1/2 映射)
|
||||
*/
|
||||
private void migrateLegacyOaDeptCategory() {
|
||||
this.update(Wrappers.<Dept>lambdaUpdate()
|
||||
.set(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
|
||||
.likeRight(Dept::getDeptCode, OAConvertConstant.DEPARTMENT_OA_PREFIX));
|
||||
this.update(Wrappers.<Dept>lambdaUpdate()
|
||||
.set(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
.likeRight(Dept::getDeptCode, OAConvertConstant.COMPANY_OA_PREFIX)
|
||||
.ne(Dept::getDeptCode, OAConvertConstant.LEGACY_ROOT_DEPT_CODE));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -650,9 +892,16 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
if (StringUtil.isBlank(parent.getDeptCode())) {
|
||||
throw new ServiceException("请选择已配置组织编码的上级组织");
|
||||
}
|
||||
String pattern = Pattern.quote(parent.getDeptCode()) + "-\\d+";
|
||||
if (!deptCode.matches(pattern)) {
|
||||
throw new ServiceException("组织编码格式应为:上级编码-分段数字");
|
||||
// 公司:GWWL+三位数字;部门:所属公司编码+三位数字;其他:上级编码+三位数字
|
||||
String expectedParentCode = parent.getDeptCode();
|
||||
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
|
||||
expectedParentCode = OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE;
|
||||
} else if (DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())
|
||||
&& StringUtil.isNotBlank(dept.getBelongCompanyCode())) {
|
||||
expectedParentCode = dept.getBelongCompanyCode();
|
||||
}
|
||||
if (!org.springblade.system.util.DeptCodeUtil.matchesChildCode(expectedParentCode, deptCode)) {
|
||||
throw new ServiceException("组织编码格式应为:" + expectedParentCode + "三位数字(不足补零,中间不加-)");
|
||||
}
|
||||
this.ensureDeptCodeUnique(dept, deptCode);
|
||||
dept.setDeptCode(deptCode);
|
||||
@@ -724,8 +973,8 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
log.info("没有记录OA主键的组织,跳过层级重建");
|
||||
return;
|
||||
}
|
||||
Map<String, Dept> companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
|
||||
Map<String, Dept> departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
|
||||
Map<String, Dept> companyByOaId = this.indexOaCompanies(oaDeptList);
|
||||
Map<String, Dept> departmentByOaId = this.indexOaDepartments(oaDeptList);
|
||||
Map<Long, Long> desiredParentIdMap = new LinkedHashMap<>();
|
||||
Map<Long, String> desiredParentCodeMap = new HashMap<>();
|
||||
for (Dept dept : oaDeptList) {
|
||||
@@ -778,21 +1027,20 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
if (CollectionUtil.isEmpty(oaDeptList)) {
|
||||
return 0;
|
||||
}
|
||||
Map<String, Dept> companyByOaId = this.indexByOaId(oaDeptList, DeptCategory.COMPANY.getCode());
|
||||
Map<String, Dept> departmentByOaId = this.indexByOaId(oaDeptList, DeptCategory.DEPT.getCode());
|
||||
Map<String, Dept> companyByOaId = this.indexOaCompanies(oaDeptList);
|
||||
Map<String, Dept> departmentByOaId = this.indexOaDepartments(oaDeptList);
|
||||
Set<Long> deleteIds = new HashSet<>();
|
||||
for (Dept dept : oaDeptList) {
|
||||
String parentOaId = dept.getOaSupSubComId();
|
||||
// 上级为空或为 0:视为本地锚点/根级,不作为孤儿删除
|
||||
// 上级为空或为 0:OA 顶级公司挂本地锚点,不作为孤儿
|
||||
if (StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId)) {
|
||||
continue;
|
||||
}
|
||||
boolean parentExists;
|
||||
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
|
||||
parentExists = companyByOaId.containsKey(parentOaId);
|
||||
} else {
|
||||
parentExists = departmentByOaId.containsKey(parentOaId) || companyByOaId.containsKey(parentOaId);
|
||||
}
|
||||
boolean isCompany = this.isOaCompany(dept);
|
||||
boolean parentExists = isCompany
|
||||
? companyByOaId.containsKey(parentOaId)
|
||||
: departmentByOaId.containsKey(parentOaId) || companyByOaId.containsKey(parentOaId);
|
||||
// 全量数据整理后仍找不到上级:公司、部门一律删除,不改挂保留
|
||||
if (!parentExists) {
|
||||
deleteIds.add(dept.getId());
|
||||
}
|
||||
@@ -813,7 +1061,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
if (StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId)) {
|
||||
continue;
|
||||
}
|
||||
Dept parent = DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())
|
||||
Dept parent = this.isOaCompany(dept)
|
||||
? companyByOaId.get(parentOaId)
|
||||
: Optional.ofNullable(departmentByOaId.get(parentOaId)).orElse(companyByOaId.get(parentOaId));
|
||||
if (parent != null && deleteIds.contains(parent.getId())) {
|
||||
@@ -842,6 +1090,72 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
return deleteIds.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 索引 OA 公司:同时兼容类型迁移前后的公司记录
|
||||
*/
|
||||
private Map<String, Dept> indexOaCompanies(List<Dept> oaDeptList) {
|
||||
Map<String, Dept> oaIdMap = new HashMap<>();
|
||||
for (Dept dept : oaDeptList) {
|
||||
if (!this.isOaCompany(dept) || StringUtil.isBlank(dept.getOaId())) {
|
||||
continue;
|
||||
}
|
||||
Dept previous = oaIdMap.put(dept.getOaId(), dept);
|
||||
if (previous != null) {
|
||||
log.warn("OA公司主键重复,层级重建使用后一条 oaId={} id={} previousId={}",
|
||||
dept.getOaId(), dept.getId(), previous.getId());
|
||||
}
|
||||
}
|
||||
return oaIdMap;
|
||||
}
|
||||
|
||||
private boolean isOaCompany(Dept dept) {
|
||||
if (dept == null) {
|
||||
return false;
|
||||
}
|
||||
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
|
||||
return true;
|
||||
}
|
||||
if (org.springblade.system.util.DeptCodeUtil.isCompanyCode(dept.getDeptCode())) {
|
||||
return true;
|
||||
}
|
||||
return StringUtil.isNotBlank(dept.getDeptCode())
|
||||
&& dept.getDeptCode().startsWith(OAConvertConstant.COMPANY_OA_PREFIX)
|
||||
&& !OAConvertConstant.LEGACY_ROOT_DEPT_CODE.equals(dept.getDeptCode());
|
||||
}
|
||||
|
||||
private boolean isOaDepartment(Dept dept) {
|
||||
if (dept == null) {
|
||||
return false;
|
||||
}
|
||||
if (DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())) {
|
||||
return true;
|
||||
}
|
||||
if (StringUtil.isNotBlank(dept.getBelongCompanyCode())
|
||||
&& org.springblade.system.util.DeptCodeUtil.matchesChildCode(dept.getBelongCompanyCode(), dept.getDeptCode())) {
|
||||
return true;
|
||||
}
|
||||
return StringUtil.isNotBlank(dept.getDeptCode())
|
||||
&& dept.getDeptCode().startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 索引 OA 部门:兼容类型迁移前后
|
||||
*/
|
||||
private Map<String, Dept> indexOaDepartments(List<Dept> oaDeptList) {
|
||||
Map<String, Dept> oaIdMap = new HashMap<>();
|
||||
for (Dept dept : oaDeptList) {
|
||||
if (!this.isOaDepartment(dept) || StringUtil.isBlank(dept.getOaId())) {
|
||||
continue;
|
||||
}
|
||||
Dept previous = oaIdMap.put(dept.getOaId(), dept);
|
||||
if (previous != null) {
|
||||
log.warn("OA部门主键重复,层级重建使用后一条 oaId={} id={} previousId={}",
|
||||
dept.getOaId(), dept.getId(), previous.getId());
|
||||
}
|
||||
}
|
||||
return oaIdMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按机构类型建立 OA 主键索引
|
||||
*/
|
||||
@@ -866,19 +1180,22 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
private OaParentLink resolveOaParentLink(Dept dept, Map<String, Dept> companyByOaId, Map<String, Dept> departmentByOaId) {
|
||||
String parentOaId = dept.getOaSupSubComId();
|
||||
boolean rootParent = StringUtil.isBlank(parentOaId) || OAConstant.ROOT_COMPANY_ID.equals(parentOaId);
|
||||
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
|
||||
if (this.isOaCompany(dept)) {
|
||||
if (rootParent) {
|
||||
return new OaParentLink(OAConvertConstant.ROOT_PARENT_ID, OAConstant.ROOT_COMPANY_ID);
|
||||
// OA 顶级公司统一挂到本地顶级锚点下
|
||||
return new OaParentLink(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
}
|
||||
Dept parent = companyByOaId.get(parentOaId);
|
||||
if (parent == null || parent.getId() == null || Objects.equals(parent.getId(), dept.getId())) {
|
||||
// 全量整理后仍无上级:跳过重建,交由孤儿清理直接删除
|
||||
log.warn("未找到上级公司,跳过层级重建 oaId={} oaSupSubComId={} deptId={}",
|
||||
dept.getOaId(), parentOaId, dept.getId());
|
||||
return null;
|
||||
}
|
||||
return new OaParentLink(parent.getId(), this.resolveParentCode(parent, OAConvertConstant.COMPANY_OA_PREFIX, parentOaId));
|
||||
}
|
||||
if (!DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())) {
|
||||
if (!DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())
|
||||
&& !(StringUtil.isNotBlank(dept.getDeptCode()) && dept.getDeptCode().startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX))) {
|
||||
return null;
|
||||
}
|
||||
if (rootParent) {
|
||||
|
||||
+32
@@ -62,10 +62,42 @@ public class MeasurementUnitServiceImpl extends BaseServiceImpl<MeasurementUnitM
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submit(MeasurementUnit measurementUnit) {
|
||||
prepare(measurementUnit);
|
||||
prepareSubmitTarget(measurementUnit);
|
||||
validate(measurementUnit);
|
||||
return saveOrUpdate(measurementUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码或名称已存在且为软删除时:取消软删除并复用原主键,后续按更新落库;
|
||||
* 存在且未删除时仍由唯一校验提示「已存在」。
|
||||
*/
|
||||
private void prepareSubmitTarget(MeasurementUnit measurementUnit) {
|
||||
if (Func.isNotEmpty(measurementUnit.getId())) {
|
||||
return;
|
||||
}
|
||||
MeasurementUnit existingByCode = baseMapper.selectByUnitCodeIncludingDeleted(measurementUnit.getUnitCode());
|
||||
MeasurementUnit existingByName = baseMapper.selectByUnitNameIncludingDeleted(measurementUnit.getUnitName());
|
||||
MeasurementUnit toRestore = null;
|
||||
if (existingByCode != null && Objects.equals(existingByCode.getIsDeleted(), 1)) {
|
||||
toRestore = existingByCode;
|
||||
} else if (existingByName != null && Objects.equals(existingByName.getIsDeleted(), 1)
|
||||
&& (existingByCode == null || Objects.equals(existingByCode.getId(), existingByName.getId()))) {
|
||||
toRestore = existingByName;
|
||||
}
|
||||
if (toRestore == null) {
|
||||
return;
|
||||
}
|
||||
// 复用编码恢复时,若另一条软删除记录占用了目标名称,先释放名称唯一索引
|
||||
if (existingByName != null && Objects.equals(existingByName.getIsDeleted(), 1)
|
||||
&& !Objects.equals(existingByName.getId(), toRestore.getId())) {
|
||||
String releasedName = existingByName.getUnitName() + "_DEL_" + existingByName.getId();
|
||||
baseMapper.releaseUnitName(existingByName.getId(), releasedName);
|
||||
}
|
||||
baseMapper.restoreById(toRestore.getId());
|
||||
measurementUnit.setId(toRestore.getId());
|
||||
measurementUnit.setIsDeleted(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean changeStatus(Long id, Integer status) {
|
||||
|
||||
+408
-75
@@ -22,6 +22,7 @@ import org.springblade.system.pojo.vo.OaOrgSyncPageVO;
|
||||
import org.springblade.system.pojo.vo.OaPersonSyncPageVO;
|
||||
import org.springblade.system.service.*;
|
||||
import org.springblade.system.util.DataSyncRecordUtils;
|
||||
import org.springblade.system.util.DeptCodeUtil;
|
||||
import org.springblade.thirdparty.oa.constant.OAConstant;
|
||||
import org.springblade.thirdparty.oa.constant.OAConvertConstant;
|
||||
import org.springblade.thirdparty.oa.feign.IOAClient;
|
||||
@@ -65,6 +66,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
private final IMKPushService mkPushService;
|
||||
private final IDataSyncRecordService dataSyncRecordService;
|
||||
private final OaUserListSyncHelper oaUserListSyncHelper;
|
||||
private final OaSyncRawDumpHelper oaSyncRawDumpHelper;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
@@ -73,14 +75,18 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
ComposeLogUtil.addLog(log);
|
||||
// 1. 同步公司和部门并记录
|
||||
Date startTime = this.syncAndRecord(DataSyncRecordUtils::createOAOrgFetch, queryStartTime -> {
|
||||
// 0. 确保双顶级:id=1 集团(GWWL) + 独立「外部组织」
|
||||
deptService.ensureOaSyncRootDept();
|
||||
// 1. 同步公司
|
||||
this.syncCompany(queryStartTime);
|
||||
// 公司分批入库后,重建层级并清理无法挂载的公司
|
||||
// 公司分批入库后:清理白名单外公司、重建层级并清理无法挂载的公司
|
||||
this.removeOutsideWhitelistOrgs();
|
||||
deptService.rebuildOaHierarchy();
|
||||
deptService.removeOaOrphansWithoutParent();
|
||||
// 2. 同步部门
|
||||
this.syncDept(queryStartTime);
|
||||
// 3. 分批写入完成后,按 OA 主键重建组织、部门层级
|
||||
this.removeOutsideWhitelistOrgs();
|
||||
deptService.rebuildOaHierarchy();
|
||||
deptService.removeOaOrphansWithoutParent();
|
||||
// 4. 更新部门祖级列表
|
||||
@@ -290,6 +296,10 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
private OaPersonSyncPageVO syncPersonFromOaPage(int current, int size, boolean incremental, String modifiedSince) {
|
||||
int pageNo = current < 1 ? 1 : current;
|
||||
int pageSize = size < 1 ? 20 : Math.min(size, 200);
|
||||
// 全量同步首页开启原始数据收集,结束后按父级 id 写入 JSON
|
||||
if (!incremental && pageNo == 1) {
|
||||
oaSyncRawDumpHelper.beginFullDump();
|
||||
}
|
||||
IncrementalStart incrementalStart = resolvePersonIncrementalStart(incremental, modifiedSince);
|
||||
OAPersonSearch personSearch = buildPersonSearch(incrementalStart.getStartTime());
|
||||
personSearch.setCurPage(pageNo);
|
||||
@@ -303,6 +313,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
List<OAPersonResponse> oaPersons = responseData.getDataList() == null
|
||||
? Collections.emptyList() : responseData.getDataList();
|
||||
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
||||
if (!incremental) {
|
||||
oaSyncRawDumpHelper.collectPersons(oaPersons);
|
||||
}
|
||||
OaUserListSyncHelper.OaOrgIndex orgIndex = oaUserListSyncHelper.syncOrgsFromPersons(oaPersons);
|
||||
OaUserListSyncHelper.PersonSyncCount personSyncCount = oaUserListSyncHelper.handlePerson(oaPersons, orgIndex);
|
||||
CacheUtil.clear(USER_CACHE);
|
||||
@@ -320,6 +333,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
|| oaPersons.size() < pageSize
|
||||
|| (long) pageNo * pageSize >= totalSize;
|
||||
pageVO.setFinished(finished);
|
||||
if (!incremental && finished) {
|
||||
pageVO.setDumpFilePath(oaSyncRawDumpHelper.finishFullDump());
|
||||
}
|
||||
ComposeLogUtil.getLastLog().info("OA人员分页同步完成 mode={} {}/{},成功{},跳过{}",
|
||||
incremental ? "incremental" : "full", pageNo, totalSize,
|
||||
personSyncCount.getSyncedCount(), personSyncCount.getSkippedCount());
|
||||
@@ -338,6 +354,13 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
private OaOrgSyncPageVO syncCompanyFromOaPage(int current, int size, boolean incremental, String modifiedSince) {
|
||||
int pageNo = current < 1 ? 1 : current;
|
||||
int pageSize = size < 1 ? 20 : Math.min(size, 200);
|
||||
// 首屏确保双顶级:id=1 集团(GWWL) + 独立「外部组织」
|
||||
if (pageNo == 1) {
|
||||
deptService.ensureOaSyncRootDept();
|
||||
if (!incremental) {
|
||||
oaSyncRawDumpHelper.beginOrgFullDump();
|
||||
}
|
||||
}
|
||||
IncrementalStart incrementalStart = resolveIncrementalStart(incremental, modifiedSince, DeptCategory.COMPANY);
|
||||
OACompanySearch companySearch = buildCompanySearch(incrementalStart.getStartTime());
|
||||
companySearch.setCurPage(pageNo);
|
||||
@@ -351,6 +374,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
List<OACompanyResponse> oaCompanies = responseData.getDataList() == null
|
||||
? Collections.emptyList() : responseData.getDataList();
|
||||
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
||||
if (!incremental) {
|
||||
oaSyncRawDumpHelper.collectCompanies(oaCompanies);
|
||||
}
|
||||
OrgSyncCount syncCount = handleCompany(oaCompanies, true);
|
||||
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
|
||||
ComposeLogUtil.getLastLog().warn("同步公司,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
|
||||
@@ -358,7 +384,8 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("company", pageNo, pageSize, totalSize, oaCompanies.size(), syncCount);
|
||||
pageVO.setModifiedSince(incrementalStart.getModifiedSince());
|
||||
if (Boolean.TRUE.equals(pageVO.getFinished())) {
|
||||
// 公司全部分批入库后:重建层级,并删除上级不存在的数据
|
||||
// 公司全部分批入库后:清理白名单外公司、重建层级,并删除上级不存在的数据
|
||||
this.removeOutsideWhitelistOrgs();
|
||||
deptService.rebuildOaHierarchy();
|
||||
deptService.removeOaOrphansWithoutParent();
|
||||
}
|
||||
@@ -387,6 +414,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
ComposeLogUtil.getLastLog().warn("未查询到已同步的OA公司,跳过部门同步");
|
||||
OaOrgSyncPageVO emptyPageVO = buildOrgSyncPageVO("department", pageNo, pageSize, 0L, 0, OrgSyncCount.empty());
|
||||
emptyPageVO.setFinished(true);
|
||||
if (!incremental) {
|
||||
emptyPageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump());
|
||||
}
|
||||
return emptyPageVO;
|
||||
}
|
||||
IncrementalStart incrementalStart = resolveIncrementalStart(incremental, modifiedSince, DeptCategory.DEPT);
|
||||
@@ -402,6 +432,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
List<OADepartmentResponse> oaDepartments = responseData.getDataList() == null
|
||||
? Collections.emptyList() : responseData.getDataList();
|
||||
long totalSize = responseData.getTotalSize() == null ? 0L : responseData.getTotalSize();
|
||||
if (!incremental) {
|
||||
oaSyncRawDumpHelper.collectDepartments(oaDepartments);
|
||||
}
|
||||
OrgSyncCount syncCount = handleDept(oaDepartments, true);
|
||||
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
|
||||
ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
|
||||
@@ -409,10 +442,14 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("department", pageNo, pageSize, totalSize, oaDepartments.size(), syncCount);
|
||||
pageVO.setModifiedSince(incrementalStart.getModifiedSince());
|
||||
if (Boolean.TRUE.equals(pageVO.getFinished())) {
|
||||
// 部门分批同步完成后,重建层级、清理无法挂载数据并刷新祖级列表
|
||||
// 部门分批同步完成后,清理白名单外组织、重建层级、清理无法挂载数据并刷新祖级列表
|
||||
this.removeOutsideWhitelistOrgs();
|
||||
deptService.rebuildOaHierarchy();
|
||||
deptService.removeOaOrphansWithoutParent();
|
||||
deptService.updateAncestors(null);
|
||||
if (!incremental) {
|
||||
pageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump());
|
||||
}
|
||||
}
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||
@@ -437,10 +474,16 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
ComposeLogUtil.getLastLog().warn("解析回传 modifiedSince 失败:{},重新计算", modifiedSince);
|
||||
}
|
||||
}
|
||||
String codePrefix = DeptCategory.COMPANY.equals(deptCategory)
|
||||
? OAConvertConstant.COMPANY_OA_PREFIX
|
||||
: OAConvertConstant.DEPARTMENT_OA_PREFIX;
|
||||
Dept latest = deptService.getOne(Wrappers.<Dept>lambdaQuery()
|
||||
.select(Dept::getOaModified)
|
||||
.eq(Dept::getIsOa, YES)
|
||||
.eq(Dept::getDeptCategory, deptCategory.getCode())
|
||||
.and(wrapper -> wrapper
|
||||
.eq(Dept::getDeptCategory, deptCategory.getCode())
|
||||
.or()
|
||||
.likeRight(Dept::getDeptCode, codePrefix))
|
||||
.isNotNull(Dept::getOaModified)
|
||||
.ne(Dept::getOaModified, "")
|
||||
.orderByDesc(Dept::getOaModified)
|
||||
@@ -608,13 +651,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
if (CollectionUtil.isEmpty(oaCompanies)) {
|
||||
return OrgSyncCount.empty();
|
||||
}
|
||||
// 获取需要的公司名称
|
||||
Set<String> companyNames = syncAll ? Collections.emptySet() : getCompanyNames();
|
||||
// 转换数据
|
||||
List<Dept> allParam = oaCompanies.stream()
|
||||
// 强制跳过上级公司为 0 的数据
|
||||
.filter(company -> !OAConstant.ROOT_COMPANY_ID.equals(company.getSupsubcomid()))
|
||||
// 非全量同步时,筛选只要配置的公司
|
||||
.filter(this::isAllowedOaCompany)
|
||||
.filter(company -> syncAll || companyNames.contains(company.getSubcompanyname()))
|
||||
.map(deptConvert::company2dept)
|
||||
.toList();
|
||||
@@ -623,10 +662,41 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
|
||||
}
|
||||
|
||||
// 查询已存在公司(含 oaModified),按编码索引
|
||||
Map<String, Dept> existingDeptMap = loadExistingDeptMap(DeptCategory.COMPANY, null);
|
||||
Map<String, Long> deptIdByCode = toDeptIdMap(existingDeptMap);
|
||||
return persistSyncedDepts(allParam, existingDeptMap, deptIdByCode, DeptCategory.COMPANY, filteredSkipCount);
|
||||
Map<String, Dept> existingByOaId = loadExistingByOaId(DeptCategory.COMPANY);
|
||||
Map<String, Dept> codeOwnerMap = loadCompanyCodeOwnerMap();
|
||||
Set<String> usedCodes = new HashSet<>(codeOwnerMap.keySet());
|
||||
Map<String, Dept> batchByOaId = new LinkedHashMap<>();
|
||||
for (Dept dept : allParam) {
|
||||
this.assignCompanyCode(dept, existingByOaId, usedCodes);
|
||||
batchByOaId.put(dept.getOaId(), dept);
|
||||
}
|
||||
// 上级公司编码:OA 上级为 0 → GWWL;否则取上级公司已分配编码
|
||||
for (Dept dept : allParam) {
|
||||
if (OAConstant.ROOT_COMPANY_ID.equals(dept.getOaSupSubComId()) || StringUtils.isBlank(dept.getOaSupSubComId())) {
|
||||
dept.setParentCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE);
|
||||
dept.setParentId(OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
|
||||
continue;
|
||||
}
|
||||
Dept parent = batchByOaId.get(dept.getOaSupSubComId());
|
||||
if (parent == null) {
|
||||
parent = existingByOaId.get(dept.getOaSupSubComId());
|
||||
}
|
||||
if (parent != null && StringUtils.isNotBlank(parent.getDeptCode())) {
|
||||
dept.setParentCode(parent.getDeptCode());
|
||||
dept.setParentId(parent.getId());
|
||||
} else {
|
||||
dept.setParentCode(null);
|
||||
dept.setParentId(null);
|
||||
}
|
||||
}
|
||||
Map<String, Long> deptIdByCode = new HashMap<>();
|
||||
existingByOaId.values().forEach(dept -> {
|
||||
if (StringUtils.isNotBlank(dept.getDeptCode()) && dept.getId() != null) {
|
||||
deptIdByCode.put(dept.getDeptCode(), dept.getId());
|
||||
}
|
||||
});
|
||||
deptIdByCode.put(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, OAConvertConstant.OA_SYNC_ROOT_DEPT_ID);
|
||||
return persistSyncedDepts(allParam, existingByOaId, deptIdByCode, DeptCategory.COMPANY, filteredSkipCount);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -639,103 +709,241 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
if (CollectionUtil.isEmpty(oaDepts)) {
|
||||
return OrgSyncCount.empty();
|
||||
}
|
||||
// 查询所有公司的编码和id的map
|
||||
Map<String, Long> companyDeptMap = getAllCompanyDeptMap();
|
||||
// 根公司id
|
||||
Map<String, Dept> companyByOaId = loadCompanyByOaId();
|
||||
Set<String> allowedCompanyOaIds = resolveAllowedCompanyOaIds(companyByOaId);
|
||||
String rootCompanyId = getRootCompanyId();
|
||||
if (!syncAll && rootCompanyId == null) {
|
||||
return new OrgSyncCount(0, oaDepts.size(), Collections.emptyList());
|
||||
}
|
||||
// 根公司下要同步的部门名称
|
||||
Set<String> rootCompanyDeptNames = syncAll ? Collections.emptySet() : getRootCompanyDeptNames();
|
||||
// 转换数据
|
||||
List<Dept> allParam = oaDepts.stream()
|
||||
// 全量同步时不过滤;否则:部门的公司不是根公司,或者部门名称在根公司下需要同步的部门列表中,且是根部门
|
||||
.filter(oaDept -> allowedCompanyOaIds.contains(oaDept.getSubcompanyid1()))
|
||||
.filter(oaDept -> syncAll
|
||||
|| rootCompanyId == null
|
||||
|| !rootCompanyId.equals(oaDept.getSubcompanyid1())
|
||||
|| (OAConstant.ROOT_COMPANY_ID.equals(oaDept.getSupdepid()) && rootCompanyDeptNames.contains(oaDept.getDepartmentname())))
|
||||
.map(dept -> deptConvert.dept2dept(dept, companyDeptMap))
|
||||
.map(dept -> deptConvert.dept2dept(dept, companyByOaId))
|
||||
.toList();
|
||||
int filteredSkipCount = oaDepts.size() - allParam.size();
|
||||
if (CollectionUtil.isEmpty(allParam)) {
|
||||
return new OrgSyncCount(0, filteredSkipCount, Collections.emptyList());
|
||||
}
|
||||
Set<String> deptCodes = new HashSet<>();
|
||||
allParam.forEach(dept -> {
|
||||
if (StringUtils.isNotEmpty(dept.getDeptCode())) {
|
||||
deptCodes.add(dept.getDeptCode());
|
||||
|
||||
Map<String, Dept> existingByOaId = loadExistingByOaId(DeptCategory.DEPT);
|
||||
Map<String, Integer> nextSerialByCompany = new HashMap<>();
|
||||
Map<String, Set<String>> usedCodesByCompany = new HashMap<>();
|
||||
// 预载各公司下已有部门编码,保证分段连续
|
||||
for (Dept existing : existingByOaId.values()) {
|
||||
if (StringUtils.isBlank(existing.getBelongCompanyCode()) || StringUtils.isBlank(existing.getDeptCode())) {
|
||||
continue;
|
||||
}
|
||||
if (StringUtils.isNotEmpty(dept.getParentCode())) {
|
||||
deptCodes.add(dept.getParentCode());
|
||||
usedCodesByCompany
|
||||
.computeIfAbsent(existing.getBelongCompanyCode(), key -> new HashSet<>())
|
||||
.add(existing.getDeptCode());
|
||||
}
|
||||
Map<String, Dept> batchByOaId = new LinkedHashMap<>();
|
||||
for (Dept dept : allParam) {
|
||||
this.assignDepartmentCode(dept, existingByOaId, companyByOaId, usedCodesByCompany, nextSerialByCompany);
|
||||
batchByOaId.put(dept.getOaId(), dept);
|
||||
}
|
||||
// 解析上级:根部门挂公司;子部门挂上级部门(本批或已存在)
|
||||
Map<String, Long> deptIdByCode = new HashMap<>();
|
||||
companyByOaId.values().forEach(company -> {
|
||||
if (StringUtils.isNotBlank(company.getDeptCode()) && company.getId() != null) {
|
||||
deptIdByCode.put(company.getDeptCode(), company.getId());
|
||||
}
|
||||
});
|
||||
// 查询数据库已有部门(含 oaModified)
|
||||
Map<String, Dept> existingDeptMap = loadExistingDeptMap(DeptCategory.DEPT, deptCodes);
|
||||
Map<String, Long> deptIdByCode = toDeptIdMap(existingDeptMap);
|
||||
// 公司编码也放入 map,根部门挂到公司时才能解析父级
|
||||
companyDeptMap.forEach(deptIdByCode::putIfAbsent);
|
||||
return persistSyncedDepts(allParam, existingDeptMap, deptIdByCode, DeptCategory.DEPT, filteredSkipCount);
|
||||
existingByOaId.values().forEach(dept -> {
|
||||
if (StringUtils.isNotBlank(dept.getDeptCode()) && dept.getId() != null) {
|
||||
deptIdByCode.put(dept.getDeptCode(), dept.getId());
|
||||
}
|
||||
});
|
||||
for (Dept dept : allParam) {
|
||||
if (dept.getParentId() != null && StringUtils.isNotBlank(dept.getParentCode())
|
||||
&& DeptCodeUtil.matchesChildCode(dept.getBelongCompanyCode(), dept.getDeptCode())
|
||||
&& dept.getBelongCompanyCode().equals(dept.getParentCode())) {
|
||||
continue;
|
||||
}
|
||||
String parentOaId = dept.getOaSupSubComId();
|
||||
Dept belongCompany = companyByOaId.values().stream()
|
||||
.filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (belongCompany != null && (OAConstant.ROOT_COMPANY_ID.equals(parentOaId)
|
||||
|| StringUtils.equals(parentOaId, belongCompany.getOaId()))) {
|
||||
dept.setParentCode(belongCompany.getDeptCode());
|
||||
dept.setParentId(belongCompany.getId());
|
||||
continue;
|
||||
}
|
||||
Dept parentDept = batchByOaId.get(parentOaId);
|
||||
if (parentDept == null) {
|
||||
parentDept = existingByOaId.get(parentOaId);
|
||||
}
|
||||
if (parentDept != null && StringUtils.isNotBlank(parentDept.getDeptCode())) {
|
||||
dept.setParentCode(parentDept.getDeptCode());
|
||||
dept.setParentId(parentDept.getId());
|
||||
} else if (belongCompany != null) {
|
||||
// 上级部门尚未就绪时先挂所属公司,分批结束后按 OA 主键重建
|
||||
dept.setParentCode(belongCompany.getDeptCode());
|
||||
dept.setParentId(belongCompany.getId());
|
||||
} else {
|
||||
dept.setParentCode(null);
|
||||
dept.setParentId(null);
|
||||
}
|
||||
}
|
||||
return persistSyncedDepts(allParam, existingByOaId, deptIdByCode, DeptCategory.DEPT, filteredSkipCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载已存在组织,按部门编码索引
|
||||
*
|
||||
* @param deptCategory 组织类型
|
||||
* @param deptCodes 限定编码;为空则按类型全量加载
|
||||
* 公司编码:GWWL+三位数字;已有合规编码则复用
|
||||
*/
|
||||
private Map<String, Dept> loadExistingDeptMap(DeptCategory deptCategory, Set<String> deptCodes) {
|
||||
if (deptCodes != null && deptCodes.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
private void assignCompanyCode(Dept dept, Map<String, Dept> existingByOaId, Set<String> usedCodes) {
|
||||
Dept existing = existingByOaId.get(dept.getOaId());
|
||||
if (existing != null && DeptCodeUtil.isCompanyCode(existing.getDeptCode())) {
|
||||
dept.setDeptCode(existing.getDeptCode());
|
||||
dept.setBelongCompanyCode(existing.getDeptCode());
|
||||
usedCodes.add(existing.getDeptCode());
|
||||
return;
|
||||
}
|
||||
int serial = DeptCodeUtil.nextSerial(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, usedCodes);
|
||||
String deptCode = DeptCodeUtil.formatChildCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, serial);
|
||||
while (usedCodes.contains(deptCode)) {
|
||||
serial++;
|
||||
deptCode = DeptCodeUtil.formatChildCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, serial);
|
||||
}
|
||||
usedCodes.add(deptCode);
|
||||
dept.setDeptCode(deptCode);
|
||||
dept.setBelongCompanyCode(deptCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门编码:所属公司编码+三位数字;已有合规编码则复用
|
||||
*/
|
||||
private void assignDepartmentCode(Dept dept, Map<String, Dept> existingByOaId, Map<String, Dept> companyByOaId,
|
||||
Map<String, Set<String>> usedCodesByCompany, Map<String, Integer> nextSerialByCompany) {
|
||||
Dept existing = existingByOaId.get(dept.getOaId());
|
||||
Dept belongCompany = null;
|
||||
if (StringUtils.isNotBlank(dept.getBelongCompanyCode())) {
|
||||
belongCompany = companyByOaId.values().stream()
|
||||
.filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode())
|
||||
|| StringUtils.equals(company.getOaId(), dept.getBelongCompanyCode()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
if (belongCompany == null && existing != null && StringUtils.isNotBlank(existing.getBelongCompanyCode())) {
|
||||
String belongCode = existing.getBelongCompanyCode();
|
||||
belongCompany = companyByOaId.values().stream()
|
||||
.filter(company -> StringUtils.equals(company.getDeptCode(), belongCode))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
if (belongCompany == null) {
|
||||
return;
|
||||
}
|
||||
final Dept resolvedCompany = belongCompany;
|
||||
final String companyCode = resolvedCompany.getDeptCode();
|
||||
dept.setBelongCompanyCode(companyCode);
|
||||
if (existing != null && DeptCodeUtil.matchesChildCode(companyCode, existing.getDeptCode())) {
|
||||
dept.setDeptCode(existing.getDeptCode());
|
||||
usedCodesByCompany.computeIfAbsent(companyCode, key -> new HashSet<>()).add(existing.getDeptCode());
|
||||
return;
|
||||
}
|
||||
Set<String> usedCodes = usedCodesByCompany.computeIfAbsent(companyCode, key -> new HashSet<>());
|
||||
int serial = nextSerialByCompany.computeIfAbsent(companyCode,
|
||||
key -> DeptCodeUtil.nextSerial(companyCode, usedCodes));
|
||||
String deptCode = DeptCodeUtil.formatChildCode(companyCode, serial);
|
||||
while (usedCodes.contains(deptCode)) {
|
||||
serial++;
|
||||
deptCode = DeptCodeUtil.formatChildCode(companyCode, serial);
|
||||
}
|
||||
nextSerialByCompany.put(companyCode, serial + 1);
|
||||
usedCodes.add(deptCode);
|
||||
dept.setDeptCode(deptCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 oaId 加载已存在组织
|
||||
*/
|
||||
private Map<String, Dept> loadExistingByOaId(DeptCategory deptCategory) {
|
||||
List<Dept> deptList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptCategory, deptCategory.getCode())
|
||||
.in(deptCodes != null, Dept::getDeptCode, deptCodes)
|
||||
.select(Dept::getId, Dept::getDeptCode, Dept::getOaModified, Dept::getParentId));
|
||||
return deptList.stream()
|
||||
.filter(dept -> StringUtils.isNotEmpty(dept.getDeptCode()))
|
||||
.collect(Collectors.toMap(Dept::getDeptCode, dept -> dept, (a, b) -> b));
|
||||
}
|
||||
|
||||
private Map<String, Long> toDeptIdMap(Map<String, Dept> existingDeptMap) {
|
||||
Map<String, Long> deptIdByCode = new HashMap<>();
|
||||
existingDeptMap.forEach((code, dept) -> deptIdByCode.put(code, dept.getId()));
|
||||
return deptIdByCode;
|
||||
.isNotNull(Dept::getOaId)
|
||||
.ne(Dept::getOaId, "")
|
||||
.select(Dept::getId, Dept::getDeptCode, Dept::getOaModified, Dept::getParentId,
|
||||
Dept::getOaId, Dept::getBelongCompanyCode, Dept::getParentCode, Dept::getOaSupSubComId));
|
||||
Map<String, Dept> byOaId = new HashMap<>();
|
||||
for (Dept dept : deptList) {
|
||||
if (StringUtils.isNotBlank(dept.getOaId())) {
|
||||
byOaId.put(dept.getOaId(), dept);
|
||||
}
|
||||
}
|
||||
return byOaId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化同步数据:不存在则新增;存在且 oaModified 不一致才修改
|
||||
*
|
||||
* @param allParam 待处理数据
|
||||
* @param existingDeptMap 已存在组织(同类型)
|
||||
* @param deptIdByCode 编码→id(含上级公司等)
|
||||
* @param deptCategory 组织类型
|
||||
* @param filteredSkipCount 上游过滤跳过数
|
||||
* @return 同步统计
|
||||
* 已占用的公司编码(GWWLxxx)
|
||||
*/
|
||||
private OrgSyncCount persistSyncedDepts(List<Dept> allParam, Map<String, Dept> existingDeptMap,
|
||||
private Map<String, Dept> loadCompanyCodeOwnerMap() {
|
||||
List<Dept> companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
.likeRight(Dept::getDeptCode, OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE)
|
||||
.select(Dept::getId, Dept::getDeptCode, Dept::getOaId));
|
||||
Map<String, Dept> codeMap = new HashMap<>();
|
||||
for (Dept dept : companyList) {
|
||||
if (DeptCodeUtil.isCompanyCode(dept.getDeptCode())) {
|
||||
codeMap.put(dept.getDeptCode(), dept);
|
||||
}
|
||||
}
|
||||
return codeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 已同步公司:oaId → 公司
|
||||
*/
|
||||
private Map<String, Dept> loadCompanyByOaId() {
|
||||
List<Dept> companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
.isNotNull(Dept::getOaId)
|
||||
.ne(Dept::getOaId, ""));
|
||||
Map<String, Dept> byOaId = new HashMap<>();
|
||||
for (Dept dept : companyList) {
|
||||
if (StringUtils.isNotBlank(dept.getOaId())) {
|
||||
byOaId.put(dept.getOaId(), dept);
|
||||
}
|
||||
}
|
||||
return byOaId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化同步数据:按 oaId 匹配;不存在则新增;存在且 oaModified 不一致才修改
|
||||
*/
|
||||
private OrgSyncCount persistSyncedDepts(List<Dept> allParam, Map<String, Dept> existingByOaId,
|
||||
Map<String, Long> deptIdByCode, DeptCategory deptCategory,
|
||||
int filteredSkipCount) {
|
||||
Set<Long> existsDeptIds = existingDeptMap.values().stream()
|
||||
Set<Long> existsDeptIds = existingByOaId.values().stream()
|
||||
.map(Dept::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
allParam.forEach(dept -> {
|
||||
if (deptIdByCode.containsKey(dept.getDeptCode())) {
|
||||
Dept existing = existingByOaId.get(dept.getOaId());
|
||||
if (existing != null && existing.getId() != null) {
|
||||
dept.setId(existing.getId());
|
||||
} else if (StringUtils.isNotBlank(dept.getDeptCode()) && deptIdByCode.containsKey(dept.getDeptCode())) {
|
||||
dept.setId(deptIdByCode.get(dept.getDeptCode()));
|
||||
} else {
|
||||
dept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
}
|
||||
if (StringUtils.isNotBlank(dept.getDeptCode())) {
|
||||
deptIdByCode.put(dept.getDeptCode(), dept.getId());
|
||||
}
|
||||
});
|
||||
|
||||
// 重新把没有父id的数据设置一下父id;上级尚未入库时,新增记录先挂到根节点,全部分批完成后重建层级并清理无法挂载的数据
|
||||
int unresolvedParentCount = 0;
|
||||
for (Dept dept : allParam) {
|
||||
if (dept.getParentId() != null) {
|
||||
continue;
|
||||
}
|
||||
Long parentId = deptIdByCode.get(dept.getParentCode());
|
||||
Long parentId = StringUtils.isNotBlank(dept.getParentCode()) ? deptIdByCode.get(dept.getParentCode()) : null;
|
||||
if (parentId != null) {
|
||||
dept.setParentId(parentId);
|
||||
continue;
|
||||
@@ -750,8 +958,8 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
deptCategory.getName(), unresolvedParentCount);
|
||||
}
|
||||
|
||||
// 父id不为空才新增;已存在的记录即使父id未解析也允许更新(空父id不会覆盖原值)
|
||||
List<Dept> list = allParam.stream()
|
||||
.filter(dept -> StringUtils.isNotBlank(dept.getDeptCode()))
|
||||
.filter(dept -> dept.getParentId() != null || existsDeptIds.contains(dept.getId()))
|
||||
.toList();
|
||||
List<Dept> addList = list.stream()
|
||||
@@ -761,10 +969,9 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
ComposeLogUtil.getLastLog().info("批量新增{}:{}", deptCategory.getName(), addList.size());
|
||||
deptService.saveBatch(addList);
|
||||
}
|
||||
// 已存在且 oaModified 不一致(或不存在可比对值)才修改
|
||||
List<Dept> updateList = list.stream()
|
||||
.filter(dept -> existsDeptIds.contains(dept.getId()))
|
||||
.filter(dept -> !shouldSkipOaUpdate(existingDeptMap.get(dept.getDeptCode()), dept))
|
||||
.filter(dept -> !shouldSkipOaUpdate(existingByOaId.get(dept.getOaId()), dept))
|
||||
.toList();
|
||||
if (CollectionUtil.isNotEmpty(updateList)) {
|
||||
ComposeLogUtil.getLastLog().info("批量修改{}:{}", deptCategory.getName(), updateList.size());
|
||||
@@ -772,10 +979,11 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
int unchangedCount = (int) list.stream()
|
||||
.filter(dept -> existsDeptIds.contains(dept.getId()))
|
||||
.filter(dept -> shouldSkipOaUpdate(existingDeptMap.get(dept.getDeptCode()), dept))
|
||||
.filter(dept -> shouldSkipOaUpdate(existingByOaId.get(dept.getOaId()), dept))
|
||||
.count();
|
||||
List<Dept> notHandledList = allParam.stream()
|
||||
.filter(dept -> dept.getParentId() == null && !existsDeptIds.contains(dept.getId()))
|
||||
.filter(dept -> StringUtils.isBlank(dept.getDeptCode())
|
||||
|| (dept.getParentId() == null && !existsDeptIds.contains(dept.getId())))
|
||||
.toList();
|
||||
int syncedCount = addList.size() + updateList.size();
|
||||
int skippedCount = filteredSkipCount + unchangedCount + notHandledList.size();
|
||||
@@ -786,12 +994,17 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
|
||||
/**
|
||||
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段
|
||||
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。
|
||||
* 组织编码已按新规则重算且与库中不一致时,即使 modified 相同也要更新。
|
||||
*/
|
||||
private boolean shouldSkipOaUpdate(Dept existing, Dept incoming) {
|
||||
if (existing == null) {
|
||||
return false;
|
||||
}
|
||||
if (incoming != null && StringUtils.isNotBlank(incoming.getDeptCode())
|
||||
&& !StringUtils.equals(existing.getDeptCode(), incoming.getDeptCode())) {
|
||||
return false;
|
||||
}
|
||||
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
|
||||
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
|
||||
if (localModified.isEmpty() || remoteModified.isEmpty()) {
|
||||
@@ -831,6 +1044,106 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否允许同步的 OA 公司:二级白名单,或其上级为二级白名单(三级子公司)
|
||||
*/
|
||||
private boolean isAllowedOaCompany(OACompanyResponse company) {
|
||||
if (company == null || StringUtils.isBlank(company.getId())) {
|
||||
return false;
|
||||
}
|
||||
if (OAConvertConstant.OA_SYNC_KEEP_SECOND_LEVEL_COMPANY_IDS.contains(company.getId())) {
|
||||
return true;
|
||||
}
|
||||
return OAConvertConstant.OA_SYNC_KEEP_SECOND_LEVEL_COMPANY_IDS.contains(company.getSupsubcomid());
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析允许同步部门的所属公司 OA 主键:二级白名单 ∪ 已同步的三级子公司
|
||||
*/
|
||||
private Set<String> resolveAllowedCompanyOaIds(Map<String, Dept> companyByOaId) {
|
||||
Set<String> allowed = new HashSet<>(OAConvertConstant.OA_SYNC_KEEP_SECOND_LEVEL_COMPANY_IDS);
|
||||
if (companyByOaId == null || companyByOaId.isEmpty()) {
|
||||
return allowed;
|
||||
}
|
||||
for (Dept company : companyByOaId.values()) {
|
||||
if (company == null || StringUtils.isBlank(company.getOaId())) {
|
||||
continue;
|
||||
}
|
||||
if (OAConvertConstant.OA_SYNC_KEEP_SECOND_LEVEL_COMPANY_IDS.contains(company.getOaId())
|
||||
|| OAConvertConstant.OA_SYNC_KEEP_SECOND_LEVEL_COMPANY_IDS.contains(company.getOaSupSubComId())) {
|
||||
allowed.add(company.getOaId());
|
||||
}
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理不在白名单树内的 OA 公司及其子树(含部门)
|
||||
*/
|
||||
private int removeOutsideWhitelistOrgs() {
|
||||
List<Dept> oaCompanyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
.isNotNull(Dept::getOaId)
|
||||
.ne(Dept::getOaId, ""));
|
||||
Map<String, Dept> companyByOaId = new HashMap<>();
|
||||
for (Dept company : oaCompanyList) {
|
||||
if (StringUtils.isNotBlank(company.getOaId())) {
|
||||
companyByOaId.put(company.getOaId(), company);
|
||||
}
|
||||
}
|
||||
Set<String> allowedCompanyOaIds = resolveAllowedCompanyOaIds(companyByOaId);
|
||||
Set<Long> deleteIds = new HashSet<>();
|
||||
for (Dept company : oaCompanyList) {
|
||||
if (!allowedCompanyOaIds.contains(company.getOaId())) {
|
||||
deleteIds.add(company.getId());
|
||||
}
|
||||
}
|
||||
Map<String, String> companyCodeToOaId = new HashMap<>();
|
||||
for (Dept company : oaCompanyList) {
|
||||
if (StringUtils.isNotBlank(company.getDeptCode()) && StringUtils.isNotBlank(company.getOaId())) {
|
||||
companyCodeToOaId.put(company.getDeptCode(), company.getOaId());
|
||||
}
|
||||
}
|
||||
List<Dept> oaDeptList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
|
||||
.isNotNull(Dept::getOaId)
|
||||
.ne(Dept::getOaId, ""));
|
||||
for (Dept dept : oaDeptList) {
|
||||
String belongOaId = null;
|
||||
if (StringUtils.isNotBlank(dept.getBelongCompanyCode())) {
|
||||
belongOaId = companyCodeToOaId.get(dept.getBelongCompanyCode());
|
||||
}
|
||||
if (StringUtils.isBlank(belongOaId) && StringUtils.isNotBlank(dept.getOaSupSubComId())
|
||||
&& companyByOaId.containsKey(dept.getOaSupSubComId())) {
|
||||
// 根部门上级可能直接是公司 OA 主键
|
||||
belongOaId = dept.getOaSupSubComId();
|
||||
}
|
||||
if (StringUtils.isBlank(belongOaId) || !allowedCompanyOaIds.contains(belongOaId)) {
|
||||
deleteIds.add(dept.getId());
|
||||
}
|
||||
}
|
||||
if (CollectionUtil.isEmpty(deleteIds)) {
|
||||
return 0;
|
||||
}
|
||||
List<Dept> allDeptList = deptService.list();
|
||||
boolean cascadeChanged = true;
|
||||
while (cascadeChanged) {
|
||||
cascadeChanged = false;
|
||||
for (Dept dept : allDeptList) {
|
||||
if (deleteIds.contains(dept.getId())) {
|
||||
continue;
|
||||
}
|
||||
if (dept.getParentId() != null && deleteIds.contains(dept.getParentId())) {
|
||||
deleteIds.add(dept.getId());
|
||||
cascadeChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
deptService.removeByIds(deleteIds);
|
||||
ComposeLogUtil.getLastLog().info("清理白名单外 OA 组织{}条", deleteIds.size());
|
||||
return deleteIds.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OA 部门查询使用的子公司 id
|
||||
*
|
||||
@@ -842,6 +1155,8 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
if (syncAll) {
|
||||
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
.isNotNull(Dept::getOaId)
|
||||
.ne(Dept::getOaId, "")
|
||||
);
|
||||
} else {
|
||||
Set<String> companyNames = this.getCompanyNames();
|
||||
@@ -851,11 +1166,22 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.in(Dept::getDeptName, companyNames)
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
.isNotNull(Dept::getOaId)
|
||||
.ne(Dept::getOaId, "")
|
||||
);
|
||||
}
|
||||
Map<String, Dept> companyByOaId = new HashMap<>();
|
||||
for (Dept company : companyList) {
|
||||
String oaId = this.resolveOaCompanyId(company);
|
||||
if (StringUtils.isNotBlank(oaId)) {
|
||||
companyByOaId.put(oaId, company);
|
||||
}
|
||||
}
|
||||
Set<String> allowedCompanyOaIds = resolveAllowedCompanyOaIds(companyByOaId);
|
||||
return companyList.stream()
|
||||
.map(this::resolveOaCompanyId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.filter(allowedCompanyOaIds::contains)
|
||||
.distinct()
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
@@ -951,13 +1277,20 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
// 查询父部门编码是根公司的,部门类型的,根公司下部门名称的部门
|
||||
Dept rootCompany = deptService.getOne(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getOaId, rootCompanyId)
|
||||
.eq(Dept::getDeptCategory, DeptCategory.COMPANY.getCode())
|
||||
.last("limit 1"), false);
|
||||
if (rootCompany == null || StringUtils.isBlank(rootCompany.getDeptCode())) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.in(Dept::getDeptName, rootCompanyDeptNames)
|
||||
.eq(Dept::getDeptCategory, DeptCategory.DEPT.getCode())
|
||||
.eq(Dept::getParentCode, OAConvertConstant.COMPANY_OA_PREFIX + rootCompanyId)
|
||||
.eq(Dept::getParentCode, rootCompany.getDeptCode())
|
||||
).stream()
|
||||
.filter(dept -> StringUtils.isNotBlank(dept.getDeptCode()))
|
||||
.map(dept -> dept.getDeptCode().replace(OAConvertConstant.DEPARTMENT_OA_PREFIX, ""))
|
||||
.map(Dept::getOaId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@@ -980,11 +1313,11 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
ComposeLogUtil.getLastLog().warn("未查询到根公司 名称:{}", companyName);
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isBlank(dept.getDeptCode())) {
|
||||
if (StringUtils.isBlank(dept.getOaId()) && StringUtils.isBlank(dept.getDeptCode())) {
|
||||
ComposeLogUtil.getLastLog().warn("根公司编码为空:{}", companyName);
|
||||
return null;
|
||||
}
|
||||
// 部门编码去掉前缀,就是oa的id
|
||||
return dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, "");
|
||||
return StringUtils.isNotBlank(dept.getOaId()) ? dept.getOaId()
|
||||
: dept.getDeptCode().replace(OAConvertConstant.COMPANY_OA_PREFIX, "");
|
||||
}
|
||||
}
|
||||
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.system.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONWriter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.system.log.ComposeLogUtil;
|
||||
import org.springblade.thirdparty.oa.feign.IOAClient;
|
||||
import org.springblade.thirdparty.oa.pojo.response.OACompanyResponse;
|
||||
import org.springblade.thirdparty.oa.pojo.response.OADepartmentResponse;
|
||||
import org.springblade.thirdparty.oa.pojo.response.OAPersonResponse;
|
||||
import org.springblade.thirdparty.oa.pojo.search.OACompanySearch;
|
||||
import org.springblade.thirdparty.oa.pojo.search.OADepartmentSearch;
|
||||
import org.springblade.thirdparty.oa.pojo.search.OASearch;
|
||||
import org.springblade.thirdparty.oa.util.OAUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* OA 全量同步原始数据落盘:按父级 id 组装层级 JSON
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class OaSyncRawDumpHelper {
|
||||
|
||||
private static final String ORPHAN_COMPANY_ID = "_orphan_company";
|
||||
private static final String ORPHAN_DEPT_ID = "_orphan_dept";
|
||||
|
||||
private final IOAClient oaClient;
|
||||
|
||||
@Value("${third-party.oa.sync-dump-dir:./data/oa-sync}")
|
||||
private String syncDumpDir;
|
||||
|
||||
/**
|
||||
* 当前全量同步会话收集到的人员原始数据
|
||||
*/
|
||||
private final List<OAPersonResponse> collectingPersons = Collections.synchronizedList(new ArrayList<>());
|
||||
private volatile boolean collecting;
|
||||
|
||||
/**
|
||||
* 机构全量同步会话收集的公司/部门原始数据
|
||||
*/
|
||||
private final List<OACompanyResponse> collectingCompanies = Collections.synchronizedList(new ArrayList<>());
|
||||
private final List<OADepartmentResponse> collectingDepartments = Collections.synchronizedList(new ArrayList<>());
|
||||
private volatile boolean collectingOrg;
|
||||
|
||||
/**
|
||||
* 全量同步首页启动收集
|
||||
*/
|
||||
public synchronized void beginFullDump() {
|
||||
collectingPersons.clear();
|
||||
collecting = true;
|
||||
ComposeLogUtil.getLastLog().info("已开启 OA 全量同步原始数据收集,目录={}", resolveDumpDir());
|
||||
}
|
||||
|
||||
/**
|
||||
* 机构全量同步首页启动收集公司/部门原始数据
|
||||
*/
|
||||
public synchronized void beginOrgFullDump() {
|
||||
collectingCompanies.clear();
|
||||
collectingDepartments.clear();
|
||||
collectingOrg = true;
|
||||
ComposeLogUtil.getLastLog().info("已开启 OA 机构全量同步原始数据收集,目录={}", resolveDumpDir());
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集本页人员原始数据
|
||||
*/
|
||||
public void collectPersons(List<OAPersonResponse> oaPersons) {
|
||||
if (!collecting || oaPersons == null || oaPersons.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
collectingPersons.addAll(oaPersons);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集本页公司原始数据
|
||||
*/
|
||||
public void collectCompanies(List<OACompanyResponse> oaCompanies) {
|
||||
if (!collectingOrg || oaCompanies == null || oaCompanies.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
collectingCompanies.addAll(oaCompanies);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集本页部门原始数据
|
||||
*/
|
||||
public void collectDepartments(List<OADepartmentResponse> oaDepartments) {
|
||||
if (!collectingOrg || oaDepartments == null || oaDepartments.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
collectingDepartments.addAll(oaDepartments);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量同步结束:先写入人员层级 JSON,再异步补齐公司/部门接口原始层级数据
|
||||
*
|
||||
* @return 人员原始层级 JSON 文件路径,失败时返回 null
|
||||
*/
|
||||
public synchronized String finishFullDump() {
|
||||
if (!collecting) {
|
||||
return null;
|
||||
}
|
||||
List<OAPersonResponse> persons = new ArrayList<>(collectingPersons);
|
||||
collecting = false;
|
||||
collectingPersons.clear();
|
||||
try {
|
||||
String stamp = DateUtil.format(new Date(), "yyyyMMddHHmmss");
|
||||
Map<String, Object> personPayload = new LinkedHashMap<>();
|
||||
personPayload.put("exportTime", DateUtil.format(new Date(), DateUtil.PATTERN_DATETIME));
|
||||
personPayload.put("personCount", persons.size());
|
||||
personPayload.put("personTree", buildPersonTree(persons));
|
||||
personPayload.put("personsRaw", persons);
|
||||
Path personFile = writeDumpFile("oa-full-sync-person-raw-" + stamp + ".json", personPayload);
|
||||
ComposeLogUtil.getLastLog().info("OA 人员原始层级数据已写入 {}", personFile.toAbsolutePath());
|
||||
CompletableFuture.runAsync(() -> dumpOrgRawAsync(stamp));
|
||||
return personFile.toAbsolutePath().toString();
|
||||
} catch (Exception exception) {
|
||||
log.error("写入 OA 全量同步人员原始 JSON 失败", exception);
|
||||
ComposeLogUtil.getLastLog().error("写入 OA 全量同步人员原始 JSON 失败:{}", exception.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 机构全量同步结束:写入公司/部门接口原始数据(按父级 id 层级)
|
||||
*
|
||||
* @return JSON 文件路径,失败时返回 null
|
||||
*/
|
||||
public synchronized String finishOrgFullDump() {
|
||||
if (!collectingOrg) {
|
||||
return null;
|
||||
}
|
||||
List<OACompanyResponse> companies = new ArrayList<>(collectingCompanies);
|
||||
List<OADepartmentResponse> departments = new ArrayList<>(collectingDepartments);
|
||||
collectingOrg = false;
|
||||
collectingCompanies.clear();
|
||||
collectingDepartments.clear();
|
||||
try {
|
||||
String stamp = DateUtil.format(new Date(), "yyyyMMddHHmmss");
|
||||
Path orgFile = writeOrgDumpFile(stamp, companies, departments);
|
||||
ComposeLogUtil.getLastLog().info("OA 机构原始层级数据已写入 {}", orgFile.toAbsolutePath());
|
||||
return orgFile.toAbsolutePath().toString();
|
||||
} catch (Exception exception) {
|
||||
log.error("写入 OA 机构全量同步原始 JSON 失败", exception);
|
||||
ComposeLogUtil.getLastLog().error("写入 OA 机构全量同步原始 JSON 失败:{}", exception.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void dumpOrgRawAsync(String stamp) {
|
||||
try {
|
||||
List<OACompanyResponse> companies = fetchAllCompanies();
|
||||
List<OADepartmentResponse> departments = fetchAllDepartments(companies);
|
||||
Path orgFile = writeOrgDumpFile(stamp, companies, departments);
|
||||
log.info("OA 公司/部门原始层级数据已写入 {}", orgFile.toAbsolutePath());
|
||||
} catch (Exception exception) {
|
||||
log.error("异步写入 OA 公司/部门原始 JSON 失败", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Path writeOrgDumpFile(String stamp, List<OACompanyResponse> companies,
|
||||
List<OADepartmentResponse> departments) throws IOException {
|
||||
Map<String, Object> orgPayload = new LinkedHashMap<>();
|
||||
orgPayload.put("exportTime", DateUtil.format(new Date(), DateUtil.PATTERN_DATETIME));
|
||||
orgPayload.put("companyCount", companies.size());
|
||||
orgPayload.put("departmentCount", departments.size());
|
||||
orgPayload.put("companyTree", buildCompanyTree(companies));
|
||||
orgPayload.put("departmentTree", buildDepartmentTree(departments));
|
||||
orgPayload.put("companiesRaw", companies);
|
||||
orgPayload.put("departmentsRaw", departments);
|
||||
return writeDumpFile("oa-full-sync-org-raw-" + stamp + ".json", orgPayload);
|
||||
}
|
||||
|
||||
private List<OACompanyResponse> fetchAllCompanies() {
|
||||
List<OACompanyResponse> companies = new ArrayList<>();
|
||||
OACompanySearch companySearch = new OACompanySearch();
|
||||
companySearch.setCurPage(1);
|
||||
companySearch.setPageSize(100);
|
||||
OAUtils.pageSyncHandler(companySearch, param -> oaClient.queryCompanyPage(new OASearch<>(param)), response -> {
|
||||
log.error("导出公司原始数据失败 {}", JSON.toJSONString(response));
|
||||
return new ServiceException("导出公司原始数据失败");
|
||||
}, 10000, log::info).accept(companies::addAll);
|
||||
return companies;
|
||||
}
|
||||
|
||||
private List<OADepartmentResponse> fetchAllDepartments(List<OACompanyResponse> companies) {
|
||||
List<OADepartmentResponse> departments = new ArrayList<>();
|
||||
String subCompanyIds = companies.stream()
|
||||
.map(OACompanyResponse::getId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.reduce((left, right) -> left + "," + right)
|
||||
.orElse("");
|
||||
OADepartmentSearch departmentSearch = new OADepartmentSearch();
|
||||
departmentSearch.setCurPage(1);
|
||||
departmentSearch.setPageSize(100);
|
||||
departmentSearch.setSubcompanyid1(subCompanyIds);
|
||||
OAUtils.pageSyncHandler(departmentSearch, param -> oaClient.queryDepartmentPage(new OASearch<>(param)), response -> {
|
||||
log.error("导出部门原始数据失败 {}", JSON.toJSONString(response));
|
||||
return new ServiceException("导出部门原始数据失败");
|
||||
}, 10000, log::info).accept(departments::addAll);
|
||||
return departments;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildCompanyTree(List<OACompanyResponse> companies) {
|
||||
Map<String, Map<String, Object>> nodeMap = new LinkedHashMap<>();
|
||||
for (OACompanyResponse company : companies) {
|
||||
if (company == null || StringUtils.isBlank(company.getId())) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", company.getId());
|
||||
node.put("parentId", blankToRoot(company.getSupsubcomid()));
|
||||
node.put("type", "company");
|
||||
node.put("name", company.getSubcompanyname());
|
||||
node.put("raw", company);
|
||||
node.put("children", new ArrayList<Map<String, Object>>());
|
||||
nodeMap.put(company.getId(), node);
|
||||
}
|
||||
return linkChildren(nodeMap);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildDepartmentTree(List<OADepartmentResponse> departments) {
|
||||
Map<String, Map<String, Object>> nodeMap = new LinkedHashMap<>();
|
||||
for (OADepartmentResponse department : departments) {
|
||||
if (department == null || StringUtils.isBlank(department.getId())) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", department.getId());
|
||||
node.put("parentId", blankToRoot(department.getSupdepid()));
|
||||
node.put("companyId", department.getSubcompanyid1());
|
||||
node.put("type", "department");
|
||||
node.put("name", department.getDepartmentname());
|
||||
node.put("raw", department);
|
||||
node.put("children", new ArrayList<Map<String, Object>>());
|
||||
nodeMap.put(department.getId(), node);
|
||||
}
|
||||
return linkChildren(nodeMap);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildPersonTree(List<OAPersonResponse> persons) {
|
||||
Map<String, Map<String, Object>> companyMap = new LinkedHashMap<>();
|
||||
Map<String, Map<String, Object>> deptMap = new LinkedHashMap<>();
|
||||
for (OAPersonResponse person : persons) {
|
||||
if (person == null) {
|
||||
continue;
|
||||
}
|
||||
String companyId = StringUtils.defaultIfBlank(person.getSubcompanyid1(), ORPHAN_COMPANY_ID);
|
||||
String deptId = StringUtils.defaultIfBlank(person.getDepartmentid(), ORPHAN_DEPT_ID + "@" + companyId);
|
||||
Map<String, Object> companyNode = companyMap.computeIfAbsent(companyId, key -> {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", key);
|
||||
node.put("parentId", "0");
|
||||
node.put("type", "company");
|
||||
node.put("name", StringUtils.defaultIfBlank(person.getSubcompanyname(), key));
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
raw.put("subcompanyid1", person.getSubcompanyid1());
|
||||
raw.put("subcompanyname", person.getSubcompanyname());
|
||||
raw.put("subcompanycode", person.getSubcompanycode());
|
||||
node.put("raw", raw);
|
||||
node.put("children", new ArrayList<Map<String, Object>>());
|
||||
return node;
|
||||
});
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> companyChildren = (List<Map<String, Object>>) companyNode.get("children");
|
||||
Map<String, Object> deptNode = deptMap.computeIfAbsent(deptId, key -> {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", key);
|
||||
node.put("parentId", companyId);
|
||||
node.put("type", "department");
|
||||
node.put("name", StringUtils.defaultIfBlank(person.getDepartmentname(), key));
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
raw.put("departmentid", person.getDepartmentid());
|
||||
raw.put("departmentname", person.getDepartmentname());
|
||||
raw.put("departmentcode", person.getDepartmentcode());
|
||||
raw.put("subcompanyid1", person.getSubcompanyid1());
|
||||
node.put("raw", raw);
|
||||
node.put("children", new ArrayList<Map<String, Object>>());
|
||||
companyChildren.add(node);
|
||||
return node;
|
||||
});
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> deptChildren = (List<Map<String, Object>>) deptNode.get("children");
|
||||
Map<String, Object> personNode = new LinkedHashMap<>();
|
||||
personNode.put("id", StringUtils.defaultIfBlank(person.getId(), person.getLoginid()));
|
||||
personNode.put("parentId", deptId);
|
||||
personNode.put("type", "person");
|
||||
personNode.put("name", person.getLastname());
|
||||
personNode.put("raw", person);
|
||||
personNode.put("children", Collections.emptyList());
|
||||
deptChildren.add(personNode);
|
||||
}
|
||||
return new ArrayList<>(companyMap.values());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Map<String, Object>> linkChildren(Map<String, Map<String, Object>> nodeMap) {
|
||||
List<Map<String, Object>> roots = new ArrayList<>();
|
||||
for (Map<String, Object> node : nodeMap.values()) {
|
||||
String parentId = Objects.toString(node.get("parentId"), "0");
|
||||
Map<String, Object> parent = nodeMap.get(parentId);
|
||||
if (parent != null && !Objects.equals(parent.get("id"), node.get("id"))) {
|
||||
((List<Map<String, Object>>) parent.get("children")).add(node);
|
||||
} else {
|
||||
roots.add(node);
|
||||
}
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
private String blankToRoot(String parentId) {
|
||||
if (StringUtils.isBlank(parentId) || "0".equals(parentId) || "-1".equals(parentId)) {
|
||||
return "0";
|
||||
}
|
||||
return parentId;
|
||||
}
|
||||
|
||||
private Path writeDumpFile(String fileName, Map<String, Object> payload) throws IOException {
|
||||
Path dir = resolveDumpDir();
|
||||
Files.createDirectories(dir);
|
||||
Path dumpFile = dir.resolve(fileName);
|
||||
String json = JSON.toJSONString(payload, JSONWriter.Feature.PrettyFormat, JSONWriter.Feature.WriteMapNullValue);
|
||||
Files.writeString(dumpFile, json, StandardCharsets.UTF_8);
|
||||
return dumpFile;
|
||||
}
|
||||
|
||||
private Path resolveDumpDir() {
|
||||
return Paths.get(StringUtils.defaultIfBlank(syncDumpDir, "./data/oa-sync")).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
}
|
||||
+16
-29
@@ -86,7 +86,7 @@ public class OaUserListSyncHelper {
|
||||
private final IRoleService roleService;
|
||||
|
||||
/**
|
||||
* 从人员数据提取二级公司、三级部门,并挂到「桂物物流集团」下
|
||||
* 从人员数据提取二级公司、三级部门,并挂到集团顶级(id=1/GWWL) 下
|
||||
*
|
||||
* @param oaPersons OA人员
|
||||
* @return 组织索引
|
||||
@@ -350,7 +350,20 @@ public class OaUserListSyncHelper {
|
||||
String defaultRoleId = getDefaultRoleId();
|
||||
List<User> updateUserParams = list.stream()
|
||||
.filter(userDeptIds -> !unchangedUserIds.contains(userDeptIds.getUserId()))
|
||||
.filter(userDeptIds -> StringUtils.isNotBlank(userDeptIds.getDeptIds()) && userDeptIds.getDeptIds().length() <= MAX_DEPT_ID_LENGTH)
|
||||
.filter(userDeptIds -> {
|
||||
if (StringUtils.isBlank(userDeptIds.getDeptIds())) {
|
||||
return false;
|
||||
}
|
||||
int deptIdLength = userDeptIds.getDeptIds().length();
|
||||
int deptCodesLength = StringUtils.length(userDeptIds.getDeptCodes());
|
||||
if (deptIdLength > MAX_DEPT_ID_LENGTH || deptCodesLength > MAX_DEPT_ID_LENGTH) {
|
||||
ComposeLogUtil.getLastLog().warn(
|
||||
"用户部门字段超长已跳过回写 userId={} deptIdLen={} deptCodesLen={} max={}",
|
||||
userDeptIds.getUserId(), deptIdLength, deptCodesLength, MAX_DEPT_ID_LENGTH);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(userDeptIds -> {
|
||||
User user = new User();
|
||||
user.setId(userDeptIds.getUserId());
|
||||
@@ -383,33 +396,7 @@ public class OaUserListSyncHelper {
|
||||
}
|
||||
|
||||
private Dept getOrCreateRootCompany() {
|
||||
Dept rootCompany = deptService.getOne(Wrappers.<Dept>lambdaQuery()
|
||||
.eq(Dept::getDeptName, OAConvertConstant.ROOT_COMPANY_NAME)
|
||||
.last("limit 1"), false);
|
||||
if (rootCompany != null) {
|
||||
return rootCompany;
|
||||
}
|
||||
rootCompany = new Dept();
|
||||
rootCompany.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
|
||||
rootCompany.setTenantId(resolveTenantId());
|
||||
rootCompany.setParentId(BladeConstant.TOP_PARENT_ID);
|
||||
rootCompany.setAncestors(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
rootCompany.setDeptName(OAConvertConstant.ROOT_COMPANY_NAME);
|
||||
rootCompany.setFullName(OAConvertConstant.ROOT_COMPANY_NAME);
|
||||
rootCompany.setShortName(OAConvertConstant.ROOT_COMPANY_NAME);
|
||||
rootCompany.setDeptCode("OACROOT");
|
||||
rootCompany.setParentCode(String.valueOf(BladeConstant.TOP_PARENT_ID));
|
||||
rootCompany.setBelongCompanyCode("OACROOT");
|
||||
rootCompany.setDeptCategory(DeptCategory.COMPANY.getCode());
|
||||
rootCompany.setSort(0);
|
||||
rootCompany.setStatus(DataStatusEnum.ENABLE.getCode());
|
||||
rootCompany.setIsDeleted(BladeConstant.DB_NOT_DELETED);
|
||||
rootCompany.setIsOa(YES);
|
||||
rootCompany.setIsPlatformCompany(0);
|
||||
rootCompany.setSyncTime(new Date());
|
||||
deptService.save(rootCompany);
|
||||
ComposeLogUtil.getLastLog().info("已创建顶级组织:{}", OAConvertConstant.ROOT_COMPANY_NAME);
|
||||
return rootCompany;
|
||||
return deptService.ensureOaSyncRootDept();
|
||||
}
|
||||
|
||||
private Dept upsertOrg(String name, String oaCode, Dept existing, Dept parent, String tenantId,
|
||||
|
||||
+7
@@ -46,11 +46,18 @@ import java.util.List;
|
||||
@Service
|
||||
public class UserDeptServiceImpl extends ServiceImpl<UserDeptMapper, UserDept> implements IUserDeptService {
|
||||
|
||||
/**
|
||||
* 与 blade_user.dept_id / dept_codes 扩容后的字段上限一致
|
||||
*/
|
||||
private static final int GROUP_CONCAT_MAX_LEN = 2000;
|
||||
|
||||
@Override
|
||||
public List<UserDeptIdsVO> queryUserDeptIds(Collection<Long> userIds) {
|
||||
if (CollectionUtil.isEmpty(userIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// 需与后续查询在同一连接中生效(外层事务内调用)
|
||||
baseMapper.setGroupConcatMaxLen(GROUP_CONCAT_MAX_LEN);
|
||||
return baseMapper.queryUserDeptIds(userIds);
|
||||
}
|
||||
|
||||
|
||||
+29
-2
@@ -1206,9 +1206,36 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
if (user.getPhone() != null && user.getPhone().length() > 20) {
|
||||
user.setPhone(SM2Util.decrypt(user.getPhone(), properties.getPublicKey(), properties.getPrivateKey()));
|
||||
String phone = user.getPhone();
|
||||
// 密文 Hex 通常远长于普通手机号;含 / 等非 Hex 字符的明文(如双号)直接跳过
|
||||
if (StringUtil.isBlank(phone) || phone.length() <= 20 || !isSm2HexCipher(phone)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
user.setPhone(SM2Util.decrypt(phone, properties.getPublicKey(), properties.getPrivateKey()));
|
||||
} catch (Exception exception) {
|
||||
log.warn("手机号 SM2 解密失败,保留原值 length={}", phone.length(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 SM2 密文 Hex(仅 0-9a-f,且偶数长度)
|
||||
*/
|
||||
private boolean isSm2HexCipher(String value) {
|
||||
int length = value.length();
|
||||
if ((length & 1) != 0) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < length; index++) {
|
||||
char character = value.charAt(index);
|
||||
boolean isDigit = character >= '0' && character <= '9';
|
||||
boolean isLowerHex = character >= 'a' && character <= 'f';
|
||||
boolean isUpperHex = character >= 'A' && character <= 'F';
|
||||
if (!isDigit && !isLowerHex && !isUpperHex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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.system.util;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springblade.thirdparty.oa.constant.OAConvertConstant;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 组织编码工具:上级编码直接拼接固定 3 位分段数字(不足补零,中间不加 "-")
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class DeptCodeUtil {
|
||||
|
||||
/**
|
||||
* 分段数字位数
|
||||
*/
|
||||
public static final int SEGMENT_WIDTH = 3;
|
||||
|
||||
private DeptCodeUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成子级编码:上级编码 + 三位数字
|
||||
*/
|
||||
public static String formatChildCode(String parentCode, int serial) {
|
||||
return parentCode + String.format("%0" + SEGMENT_WIDTH + "d", serial);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否符合「上级编码 + 三位数字」格式(恰好多出 3 位数字)
|
||||
*/
|
||||
public static boolean matchesChildCode(String parentCode, String deptCode) {
|
||||
if (StringUtils.isBlank(parentCode) || StringUtils.isBlank(deptCode)) {
|
||||
return false;
|
||||
}
|
||||
return deptCode.matches("^" + Pattern.quote(parentCode) + "\\d{" + SEGMENT_WIDTH + "}$");
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为公司编码(GWWL + 三位数字)
|
||||
*/
|
||||
public static boolean isCompanyCode(String deptCode) {
|
||||
return matchesChildCode(OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE, deptCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在已有编码中取下一分段序号(从 1 起)
|
||||
*/
|
||||
public static int nextSerial(String parentCode, Collection<String> existingCodes) {
|
||||
if (StringUtils.isBlank(parentCode)) {
|
||||
return 1;
|
||||
}
|
||||
Pattern pattern = Pattern.compile("^" + Pattern.quote(parentCode) + "(\\d{" + SEGMENT_WIDTH + "})$");
|
||||
int maxSerial = 0;
|
||||
if (existingCodes != null) {
|
||||
for (String code : existingCodes) {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
continue;
|
||||
}
|
||||
Matcher matcher = pattern.matcher(code.trim());
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
}
|
||||
maxSerial = Math.max(maxSerial, Integer.parseInt(matcher.group(1)));
|
||||
}
|
||||
}
|
||||
return maxSerial + 1;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springblade.thirdparty.bfi.constant.BfiConstant;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 业财融合(BFI)推送金蝶配置属性,统一收口 Nacos 配置 thirdParty.bfi.*:
|
||||
* <p>
|
||||
* srcSys 为客商、付款申请等各业务推送金蝶的公共参数(外部系统编号);
|
||||
* customer、payApply 为各业务专属参数;未配置时使用字段默认值。
|
||||
* <p>
|
||||
* prefix 须为全小写(thirdparty.bfi),Spring 宽松绑定可匹配 Nacos 中的 camelCase 键(thirdParty.payApply.srcSys 等)。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "thirdparty.bfi")
|
||||
public class BfiProperties {
|
||||
|
||||
/**
|
||||
* 数据来源系统编码(公共):客商、付款申请等推送金蝶时统一使用,默认 WLTMS(运输TMS)
|
||||
*/
|
||||
private String srcSys = BfiConstant.DATA_SOURCE_WLTMS;
|
||||
|
||||
/**
|
||||
* 付款申请推送配置(A103-3 付款申请保存)
|
||||
*/
|
||||
private PayApply payApply = new PayApply();
|
||||
|
||||
/**
|
||||
* 开票申请推送配置(A303-1 开票申请单新增 / A303-5 开票申请发票查询)
|
||||
*/
|
||||
private InvoiceApply invoiceApply = new InvoiceApply();
|
||||
|
||||
/**
|
||||
* 收票单推送配置(A303-2 收票单批量保存 / A302-6 收票指定应付 / A801-3 收票单查询回写)
|
||||
*/
|
||||
private ReceivedInvoice receivedInvoice = new ReceivedInvoice();
|
||||
|
||||
/**
|
||||
* 收款认领公告查询配置(A301-2)
|
||||
*/
|
||||
private ClaimAnnounce claimAnnounce = new ClaimAnnounce();
|
||||
|
||||
/**
|
||||
* 付款结果定时回写配置(A103-2/A103-4,总开关同时控制应收勾稽、开票结果回写)
|
||||
*/
|
||||
private PayResult payResult = new PayResult();
|
||||
|
||||
/**
|
||||
* 收票单结果定时回写开关(A801-3)
|
||||
*/
|
||||
private ReceivedInvoiceResult receivedInvoiceResult = new ReceivedInvoiceResult();
|
||||
|
||||
/**
|
||||
* 收款认领推送配置(A301-3)
|
||||
*/
|
||||
private ReceiptClaimPush receiptClaimPush = new ReceiptClaimPush();
|
||||
|
||||
/**
|
||||
* 全票池发票定时同步配置(A801-1)
|
||||
*/
|
||||
private InvoiceReceipt invoiceReceipt = new InvoiceReceipt();
|
||||
|
||||
/**
|
||||
* 客商金蝶数据定时同步配置(A902-1/A902-2)
|
||||
*/
|
||||
private Customer customer = new Customer();
|
||||
|
||||
/**
|
||||
* 金蝶黑名单查询配置
|
||||
*/
|
||||
private Blacklist blacklist = new Blacklist();
|
||||
|
||||
/**
|
||||
* 开票结果回写锁配置(A303-5)
|
||||
*/
|
||||
private InvoiceResult invoiceResult = new InvoiceResult();
|
||||
|
||||
/**
|
||||
* 应收单勾稽回写锁配置(A302-4)
|
||||
*/
|
||||
private ArResult arResult = new ArResult();
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定补偿配置
|
||||
*/
|
||||
private BillLock billLock = new BillLock();
|
||||
|
||||
/**
|
||||
* 审批通过自动同步金蝶配置(结算单/付款申请/开票申请/收票登记)
|
||||
*/
|
||||
private AutoSync autoSync = new AutoSync();
|
||||
|
||||
/**
|
||||
* 应付/应收/收票单分录物料映射配置(A102-2/A302-2/A303-2 共用):
|
||||
* 运输类按项目业务模式映射——国内运输按运单运输方式映射、国际运输映射国际货代服务;
|
||||
* 装卸大类及手动添加的非运费杂费映射装卸物料
|
||||
*/
|
||||
private Material material = new Material();
|
||||
|
||||
/**
|
||||
* 付款申请推送配置项
|
||||
*/
|
||||
@Data
|
||||
public static class PayApply {
|
||||
|
||||
/**
|
||||
* 单据类型编码:ap_payapply_BT_S 付款申请单、ap_payapply_oth_BT_S 其他付款单,默认其他付款单
|
||||
*/
|
||||
private String billtype = "ap_payapply_oth_BT_S";
|
||||
|
||||
/**
|
||||
* 汇率表编码,默认 01
|
||||
*/
|
||||
private String exratetable = "01";
|
||||
|
||||
/**
|
||||
* 外部单据类型,默认 0KzXs
|
||||
*/
|
||||
private String srcType = "0KzXs";
|
||||
}
|
||||
|
||||
/**
|
||||
* 开票申请推送配置项(A303-1 / A303-5):
|
||||
* 销方名称取申请单开票单位快照、销方税号取客商档案统一社会信用代码、税收分类编码取商品行开票内容快照,均不走配置
|
||||
*/
|
||||
@Data
|
||||
public static class InvoiceApply {
|
||||
|
||||
/**
|
||||
* 分录外部单据类型,默认 0KzXs(与付款申请对齐)
|
||||
*/
|
||||
private String srcType = "0KzXs";
|
||||
}
|
||||
|
||||
/**
|
||||
* 收票单推送配置项(A303-2 / A302-6 / A801-3):
|
||||
* 发票类型编码、税收分类编码、购销方税号地址银行均取票池镜像与明细,不走配置
|
||||
*/
|
||||
@Data
|
||||
public static class ReceivedInvoice {
|
||||
|
||||
/**
|
||||
* A303-2 分录物料编码:联调期临时值(金蝶测试档案),发票明细无金蝶物料概念,
|
||||
* 真实物料口径与金蝶对齐后替换(与应收/应付联调临时值同源)
|
||||
*/
|
||||
private String materialNumber = "SP0324";
|
||||
|
||||
/**
|
||||
* A801-3 推送幂等预检回看窗口天数(金蝶修改时间窗口,契约约束不超过 1 个月)
|
||||
*/
|
||||
private int queryWindowDays = 30;
|
||||
|
||||
/**
|
||||
* A801-3 定时回写轮询窗口天数(同样受金蝶 1 个月窗口约束)
|
||||
*/
|
||||
private int resultWindowDays = 30;
|
||||
|
||||
/**
|
||||
* A801-3 回写 single-flight 锁租约秒数,须大于最坏一轮耗时(批量远程查询 + 逐单回写)
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收款认领公告查询配置项(A301-2)
|
||||
*/
|
||||
@Data
|
||||
public static class ClaimAnnounce {
|
||||
|
||||
/**
|
||||
* 查询时间窗口(月数),默认 6(近半年)
|
||||
*/
|
||||
private int months = 6;
|
||||
|
||||
/**
|
||||
* 认领人(claimusers):配置后公告查询固定使用该值,不再按当前登录用户姓名查询
|
||||
*/
|
||||
private String claimusers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分录物料映射配置项(thirdparty.bfi.material.*):默认值与金蝶测试档案对齐,
|
||||
* 金蝶正式档案确认后直接在 Nacos 覆盖
|
||||
*/
|
||||
@Data
|
||||
public static class Material {
|
||||
|
||||
/**
|
||||
* 国内运输:运单运输方式 -> 金蝶物料编码(公路运输/铁路运输/水路运输/航空运输四种),
|
||||
* 运输方式未配置映射时直接拦截报错
|
||||
*/
|
||||
private Map<String, String> domestic = new LinkedHashMap<>() {{
|
||||
put("公路运输", "XDWL11010001");
|
||||
put("铁路运输", "XDWL11010002");
|
||||
put("水路运输", "XDWL11010003");
|
||||
put("航空运输", "Item-00001082");
|
||||
}};
|
||||
|
||||
/**
|
||||
* 国际运输业务模式物料编码(国际货运代理服务)
|
||||
*/
|
||||
private String international = "Item-00001081";
|
||||
|
||||
/**
|
||||
* 装卸大类、手动添加非运费杂费物料编码(装卸)
|
||||
*/
|
||||
private String loading = "XDWL11002004";
|
||||
}
|
||||
|
||||
/**
|
||||
* 付款结果回写配置项(A103-2/A103-4)
|
||||
*/
|
||||
@Data
|
||||
public static class PayResult {
|
||||
|
||||
/**
|
||||
* 回写任务开关,代码默认关闭(安全兜底),由 Nacos 按环境显式开启
|
||||
*/
|
||||
private boolean syncEnabled = false;
|
||||
|
||||
/**
|
||||
* 回写 single-flight 锁租约秒数,须大于最坏一轮耗时(分批远程查询 + 逐单回写)
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收票单结果回写配置项(A801-3)
|
||||
*/
|
||||
@Data
|
||||
public static class ReceivedInvoiceResult {
|
||||
|
||||
/**
|
||||
* 回写任务开关,默认关闭(联调稳定后开启)
|
||||
*/
|
||||
private boolean syncEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收款认领推送配置项(A301-3)
|
||||
*/
|
||||
@Data
|
||||
public static class ReceiptClaimPush {
|
||||
|
||||
/**
|
||||
* 推送任务开关,默认关闭(联调稳定后开启)
|
||||
*/
|
||||
private boolean syncEnabled = false;
|
||||
|
||||
/**
|
||||
* 推送 single-flight 锁租约秒数,须大于最坏一轮耗时(逐认领人远程查公告 + 逐条推送)
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全票池发票同步配置项(A801-1)
|
||||
*/
|
||||
@Data
|
||||
public static class InvoiceReceipt {
|
||||
|
||||
/**
|
||||
* 同步任务开关,默认关闭(上线先手动跑一次全窗口铺底)
|
||||
*/
|
||||
private boolean syncEnabled = false;
|
||||
|
||||
/**
|
||||
* 增量窗口天数(按票池更新时间滚动)
|
||||
*/
|
||||
private int incrementDays = 30;
|
||||
|
||||
/**
|
||||
* 同步发票种类白名单(逗号分隔票易通编码,如 26,27 数电普票/专票),为空不限制
|
||||
*/
|
||||
private String invoiceTypes = "";
|
||||
|
||||
/**
|
||||
* 同步 single-flight 锁租约秒数,须大于最坏一轮耗时;进程异常退出时锁在租约到期后自动释放
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客商金蝶数据同步配置项(A902-1/A902-2)
|
||||
*/
|
||||
@Data
|
||||
public static class Customer {
|
||||
|
||||
/**
|
||||
* 同步任务开关,默认关闭
|
||||
*/
|
||||
private boolean syncEnabled = false;
|
||||
|
||||
/**
|
||||
* 同步 single-flight 锁租约秒数,须大于最坏一轮耗时(复查 + 批量推送 + 停用联动)
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶黑名单查询配置项
|
||||
*/
|
||||
@Data
|
||||
public static class Blacklist {
|
||||
|
||||
/**
|
||||
* 客商提审命中风险警示类(B)时是否一并拦截;默认 false 仅放行告警,禁止交易类(A)始终拦截
|
||||
*/
|
||||
private boolean blockRiskWarning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开票结果回写锁配置项(A303-5)
|
||||
*/
|
||||
@Data
|
||||
public static class InvoiceResult {
|
||||
|
||||
/**
|
||||
* 回写 single-flight 锁租约秒数
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应收单勾稽回写锁配置项(A302-4)
|
||||
*/
|
||||
@Data
|
||||
public static class ArResult {
|
||||
|
||||
/**
|
||||
* 勾稽 single-flight 锁租约秒数
|
||||
*/
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定补偿配置项
|
||||
*/
|
||||
@Data
|
||||
public static class BillLock {
|
||||
|
||||
/**
|
||||
* 锁定/解锁总开关(thirdParty.bfi.billLock.enabled):推送后锁定 lockBusiData、
|
||||
* 金蝶解锁回调 unLockNotifyBusiData、锁定补偿重锁三处共用;
|
||||
* 默认开启保持既有锁定行为,Nacos 置 false 一键停用
|
||||
* (推送后不再锁单、解锁回调只应答不处理、补偿跳过),存量锁记录守卫(不可编辑校验)不受影响
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 锁定补偿开关,默认关闭(安全兜底)
|
||||
*/
|
||||
private boolean retryEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过自动同步金蝶配置项(结算单 A102-2/A302-2、付款申请 A103-1/A103-3、开票申请 A303-1、收票 A303-2+A302-6)
|
||||
*/
|
||||
@Data
|
||||
public static class AutoSync {
|
||||
|
||||
/**
|
||||
* 结算单审批后自动推送开关
|
||||
*/
|
||||
private boolean settlement = false;
|
||||
|
||||
/**
|
||||
* 付款申请审批后自动推送开关
|
||||
*/
|
||||
private boolean payment = false;
|
||||
|
||||
/**
|
||||
* 开票申请审批后自动推送开关
|
||||
*/
|
||||
private boolean invoiceApply = false;
|
||||
|
||||
/**
|
||||
* 收票登记审批后自动推送开关
|
||||
*/
|
||||
private boolean receipt = false;
|
||||
|
||||
/**
|
||||
* failed 单据定时重推开关(仅重推生成失败,不扫 unsynced 存量)
|
||||
*/
|
||||
private boolean retryEnabled = false;
|
||||
}
|
||||
|
||||
}
|
||||
+85
-4
@@ -5,28 +5,109 @@
|
||||
package org.springblade.transport.config;
|
||||
|
||||
import io.minio.MinioClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 凭证图片 MinIO 客户端配置。
|
||||
* <p>
|
||||
* 连接参数由 Nacos 的 file.storage.minio 配置提供。
|
||||
* 未配置 {@code file.storage.minio.endpoint} 时不注册该客户端,
|
||||
* 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动;
|
||||
* 此时凭证相关接口会在调用时给出明确提示,而非启动即失败。
|
||||
* <ul>
|
||||
* <li>{@code endpoint}:建议内网地址;若误配为 https://host/oss,会自动剥离 path 并用拦截器补回</li>
|
||||
* <li>{@code transform-endpoint}:外网地址(可带 /oss),用于浏览器预签名</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "file.storage.minio", name = "endpoint")
|
||||
public class VoucherMinioConfig {
|
||||
|
||||
private static final String DEFAULT_REGION = "us-east-1";
|
||||
|
||||
@Bean
|
||||
public MinioClient voucherMinioClient(
|
||||
@Value("${file.storage.minio.endpoint:${minio.endpoint:}}") String endpoint,
|
||||
@Value("${file.storage.minio.access-key-id:${minio.access-key:}}") String accessKey,
|
||||
@Value("${file.storage.minio.access-key-secret:${minio.secret-key:}}") String secretKey) {
|
||||
return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();
|
||||
String origin = resolveClientEndpoint(endpoint);
|
||||
String pathPrefix = resolvePathPrefix(endpoint);
|
||||
if (StringUtil.isNotBlank(pathPrefix)) {
|
||||
log.warn("MinIO endpoint 含 path,将按 origin={} pathPrefix={} 访问;建议 endpoint 使用内网地址",
|
||||
origin, pathPrefix);
|
||||
}
|
||||
return create(endpoint, accessKey, secretKey);
|
||||
}
|
||||
|
||||
public static MinioClient create(String endpoint, String accessKey, String secretKey) {
|
||||
String origin = resolveClientEndpoint(endpoint);
|
||||
String pathPrefix = resolvePathPrefix(endpoint);
|
||||
MinioClient.Builder builder = MinioClient.builder()
|
||||
.endpoint(origin)
|
||||
.credentials(accessKey, secretKey)
|
||||
.region(DEFAULT_REGION);
|
||||
if (StringUtil.isNotBlank(pathPrefix)) {
|
||||
builder.httpClient(buildHttpClient(pathPrefix), true);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* MinIO SDK 不允许 endpoint 带 path(例如 https://host/oss)。
|
||||
*/
|
||||
public static String resolveClientEndpoint(String endpoint) {
|
||||
if (StringUtil.isBlank(endpoint)) {
|
||||
throw new ServiceException("MinIO endpoint 不能为空");
|
||||
}
|
||||
URI uri = URI.create(endpoint.trim());
|
||||
if (StringUtil.isBlank(uri.getScheme()) || StringUtil.isBlank(uri.getHost())) {
|
||||
throw new ServiceException("MinIO endpoint 配置不正确: " + endpoint);
|
||||
}
|
||||
StringBuilder origin = new StringBuilder();
|
||||
origin.append(uri.getScheme()).append("://").append(uri.getHost());
|
||||
if (uri.getPort() > 0) {
|
||||
origin.append(':').append(uri.getPort());
|
||||
}
|
||||
return origin.toString();
|
||||
}
|
||||
|
||||
private static String resolvePathPrefix(String endpoint) {
|
||||
URI uri = URI.create(endpoint.trim());
|
||||
String path = uri.getPath();
|
||||
if (StringUtil.isBlank(path) || "/".equals(path)) {
|
||||
return "";
|
||||
}
|
||||
String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
|
||||
return normalized.startsWith("/") ? normalized : "/" + normalized;
|
||||
}
|
||||
|
||||
private static OkHttpClient buildHttpClient(String pathPrefix) {
|
||||
return new OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(chain -> {
|
||||
Request original = chain.request();
|
||||
HttpUrl originalUrl = original.url();
|
||||
String encodedPath = originalUrl.encodedPath();
|
||||
if (encodedPath.startsWith(pathPrefix + "/") || encodedPath.equals(pathPrefix)) {
|
||||
return chain.proceed(original);
|
||||
}
|
||||
HttpUrl newUrl = originalUrl.newBuilder()
|
||||
.encodedPath(pathPrefix + encodedPath)
|
||||
.build();
|
||||
return chain.proceed(original.newBuilder().url(newUrl).build());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.constant;
|
||||
|
||||
/**
|
||||
* 客商档案金蝶(星瀚)协同常量
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class CustomerBfiConstants {
|
||||
|
||||
/**
|
||||
* 推送金蝶固定值:业务职能全选(1销售,2结算,3付款,4收货)
|
||||
*/
|
||||
public static final String BIZ_FUNCTION_ALL = "1,2,3,4";
|
||||
|
||||
/**
|
||||
* 客商类型:内部组织(customer_kind 字段值)
|
||||
*/
|
||||
public static final String CUSTOMER_KIND_INTERNAL = "internal";
|
||||
|
||||
/**
|
||||
* 客商类型:外部客商(customer_kind 字段值)
|
||||
*/
|
||||
public static final String CUSTOMER_KIND_EXTERNAL = "external";
|
||||
|
||||
/**
|
||||
* 金蝶伙伴类型编码有效范围上界(BfiCustomerSaveDTO.type 全集 1~17)
|
||||
*/
|
||||
public static final int PARTNER_TYPE_MAX = 17;
|
||||
|
||||
/**
|
||||
* 营业期限类型:长期(TMS 表单文本)
|
||||
*/
|
||||
public static final String BUSINESS_TERM_LONG = "长期";
|
||||
|
||||
private CustomerBfiConstants() {
|
||||
}
|
||||
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.constant;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
|
||||
/**
|
||||
* 金蝶附件同步(A902)常量
|
||||
* <p>entityNumber/controlKey 映射以常量维护,二期客商(资质附件)等金蝶确认后补充条目即可,不改架构</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class KingdeeAttachmentConstant {
|
||||
|
||||
// ==================== 业务单据类型(biz_type) ====================
|
||||
|
||||
/**
|
||||
* 付款申请单
|
||||
*/
|
||||
public static final String BIZ_PAY_APPLICATION = "pay_application";
|
||||
/**
|
||||
* 正式结算-财务应付单面板
|
||||
*/
|
||||
public static final String BIZ_FORMAL_SETTLEMENT_AP = "formal_settlement_ap";
|
||||
/**
|
||||
* 正式结算-财务应收单面板
|
||||
*/
|
||||
public static final String BIZ_FORMAL_SETTLEMENT_AR = "formal_settlement_ar";
|
||||
/**
|
||||
* 收票单
|
||||
*/
|
||||
public static final String BIZ_INVOICE_RECEIPT = "invoice_receipt";
|
||||
/**
|
||||
* 开票申请单
|
||||
*/
|
||||
public static final String BIZ_INVOICE_APPLICATION = "invoice_application";
|
||||
|
||||
// ==================== 星瀚 entityNumber(A902 定位单据类型) ====================
|
||||
|
||||
/**
|
||||
* 付款申请单
|
||||
*/
|
||||
public static final String ENTITY_PAY_APPLICATION = "ap_payapply";
|
||||
/**
|
||||
* 财务应付单(正式结算应付面板)
|
||||
*/
|
||||
public static final String ENTITY_AP_BILL = "ap_finapbill";
|
||||
/**
|
||||
* 财务应收单(正式结算应收面板)
|
||||
*/
|
||||
public static final String ENTITY_AR_BILL = "ar_finarbill";
|
||||
/**
|
||||
* 收票单
|
||||
*/
|
||||
public static final String ENTITY_INVOICE = "ap_invoice";
|
||||
/**
|
||||
* 开票申请单
|
||||
*/
|
||||
public static final String ENTITY_INVOICE_APPLICATION = "sim_original_bill";
|
||||
|
||||
/**
|
||||
* 业务单据类型 -> 星瀚 entityNumber 映射;
|
||||
* 客商(资质附件)二期接入:待金蝶确认后补充专用 biz_type 与 entityNumber 条目
|
||||
*/
|
||||
public static final Map<String, String> ENTITY_NUMBERS = Map.of(
|
||||
BIZ_PAY_APPLICATION, ENTITY_PAY_APPLICATION,
|
||||
BIZ_FORMAL_SETTLEMENT_AP, ENTITY_AP_BILL,
|
||||
BIZ_FORMAL_SETTLEMENT_AR, ENTITY_AR_BILL,
|
||||
BIZ_INVOICE_RECEIPT, ENTITY_INVOICE,
|
||||
BIZ_INVOICE_APPLICATION, ENTITY_INVOICE_APPLICATION
|
||||
);
|
||||
|
||||
/**
|
||||
* 星瀚附件面板/附件字段标识(各单据统一)
|
||||
*/
|
||||
public static final String CONTROL_KEY = "attachmentpanel";
|
||||
|
||||
// ==================== 限制 ====================
|
||||
|
||||
/**
|
||||
* 单文件软限制 20MB(待金蝶确认后调整);超限记失败原因跳过,不阻塞同单其余附件
|
||||
*/
|
||||
public static final long MAX_FILE_SIZE_BYTES = 20L * 1024 * 1024;
|
||||
|
||||
// ==================== 同步行状态(sync_status) ====================
|
||||
|
||||
/**
|
||||
* 同步失败
|
||||
*/
|
||||
public static final int SYNC_STATUS_FAILED = 0;
|
||||
/**
|
||||
* 同步成功(终态,幂等跳过)
|
||||
*/
|
||||
public static final int SYNC_STATUS_SUCCESS = 1;
|
||||
|
||||
// ==================== 附件同步聚合态(列表展示,运行时聚合非存储字段) ====================
|
||||
|
||||
/**
|
||||
* 无附件:单据 attachmentsJson 无可上传附件
|
||||
*/
|
||||
public static final String AGGREGATE_NONE = "none";
|
||||
/**
|
||||
* 未上传:有附件且成功数为 0
|
||||
*/
|
||||
public static final String AGGREGATE_PENDING = "pending";
|
||||
/**
|
||||
* 部分未传:有附件且部分上传成功
|
||||
*/
|
||||
public static final String AGGREGATE_PARTIAL = "partial";
|
||||
/**
|
||||
* 已上传:有附件且全部上传成功
|
||||
*/
|
||||
public static final String AGGREGATE_DONE = "done";
|
||||
|
||||
/**
|
||||
* 聚合态 -> 展示名称
|
||||
*/
|
||||
public static final Map<String, String> AGGREGATE_NAMES = Map.of(
|
||||
AGGREGATE_NONE, "无附件",
|
||||
AGGREGATE_PENDING, "未上传",
|
||||
AGGREGATE_PARTIAL, "部分未传",
|
||||
AGGREGATE_DONE, "已上传"
|
||||
);
|
||||
|
||||
/**
|
||||
* 业务单据类型对应星瀚 entityNumber
|
||||
*
|
||||
* @param bizType 业务单据类型
|
||||
* @return entityNumber,未配置时抛业务异常(entityNumber 是硬约束,猜错即传不上)
|
||||
*/
|
||||
public static String entityNumber(String bizType) {
|
||||
String entityNumber = ENTITY_NUMBERS.get(bizType);
|
||||
if (entityNumber == null) {
|
||||
throw new ServiceException("金蝶附件上传失败:业务单据类型[" + bizType + "]未配置 entityNumber 映射");
|
||||
}
|
||||
return entityNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚合态展示名称(未知态兜底返回原文)
|
||||
*
|
||||
* @param aggregateStatus 聚合态
|
||||
* @return 展示名称
|
||||
*/
|
||||
public static String aggregateName(String aggregateStatus) {
|
||||
return AGGREGATE_NAMES.getOrDefault(aggregateStatus, aggregateStatus);
|
||||
}
|
||||
|
||||
private KingdeeAttachmentConstant() {
|
||||
}
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.constant;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 付款同步金蝶(星瀚)固定值。
|
||||
* 金蝶侧档案编码暂以常量维护,待金蝶侧确认取值后直接修改常量重新发包。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class KingdeeBfiConstant {
|
||||
|
||||
/** 往来类型:供应商(付款申请与财务应付单共用)。 */
|
||||
public static final String BFI_SUPPLIER_ASSTACT_TYPE = "bd_supplier";
|
||||
|
||||
/** 往来类型:客户(财务应付单按合同类别为客户合同时使用)。 */
|
||||
public static final String BFI_CUSTOMER_ASSTACT_TYPE = "bd_customer";
|
||||
|
||||
/** 来源单据类型:上游合同(有合同时传 awrp_src_bt)。 */
|
||||
public static final String BFI_CONTRACT_SRC_BT = "awrp_contract_info";
|
||||
|
||||
/** 无项目时传的项目编码占位值。 */
|
||||
public static final String BFI_PROJECT_NUMBER_NONE = "999";
|
||||
|
||||
/** 联调默认物料编码:金蝶测试档案值,真实货物类型档案编码与金蝶对齐后替换。 */
|
||||
public static final String BFI_AP_BILL_MATERIAL_NUMBER_TEST = "SP0324";
|
||||
|
||||
/** 联调默认计量单位编码:金蝶测试档案值,档案维护计量单位编码后替换。 */
|
||||
public static final String BFI_AP_BILL_MEASUREUNIT_NUMBER_TEST = "TON";
|
||||
/** 联调默认银行行号:金蝶测试档案值,档案维护银行行号后替换。 */
|
||||
public static final String BFI_AP_BILL_BANKACCOUNT_NUMBER_TEST = "001110012009";
|
||||
|
||||
/** 联调默认项目编码:金蝶测试档案值,项目档案与金蝶对齐后替换。 */
|
||||
public static final String BFI_AP_BILL_PROJECT_NUMBER_TEST = "999";
|
||||
|
||||
/** 付款类型编码:正式付款(结算付款)。 */
|
||||
public static final String BFI_PAYMENT_TYPE_FORMAL = "201";
|
||||
|
||||
/** 付款类型编码:预付款。 */
|
||||
public static final String BFI_PAYMENT_TYPE_PREPAY = "202";
|
||||
|
||||
/** A102-2 默认单据类型名称:服务采购应付(经 AP_BILL_BILLTYPE_MAP 转换为编码)。待确认:具体类型需金蝶侧最终确认,确认后修改此名称即可。 */
|
||||
public static final String BFI_AP_BILL_BILLTYPE_NAME = "服务采购应付";
|
||||
|
||||
/** A102-2 款项性质编码(暂按 2001 材料采购)。待确认:编码需金蝶侧最终确认,确认后如有调整直接改此常量。 */
|
||||
public static final String BFI_AP_BILL_PAY_PROPERTY = "2001";
|
||||
|
||||
/**
|
||||
* A102-2 付款方式(赊购 CREDIT,允许金蝶侧后续调整付款金额;
|
||||
*/
|
||||
public static final String BFI_AP_BILL_PUR_MODE_CREDIT = "CREDIT";
|
||||
/**
|
||||
* A102-2 付款方式 现购 CASH 会由金蝶自动 100% 推付款)。
|
||||
*/
|
||||
public static final String BFI_AP_BILL_PUR_MODE_CASH = "CASH";
|
||||
|
||||
/** A102-2 必填:外部单据类型(应付单来源标识)。待确认:取值需金蝶侧确认,确认后如有调整直接改此常量。 */
|
||||
public static final String BFI_AP_BILL_SRC_TYPE = "TMS财务应付单";
|
||||
|
||||
/** 财务应付单(ap_finapbill)单据类型映射:中文名称 -> 星瀚编码。 */
|
||||
public static final Map<String, String> AP_BILL_BILLTYPE_MAP = Map.ofEntries(
|
||||
Map.entry("借贷项调整", "ApFin_borr_BT_S"),
|
||||
Map.entry("费用采购应付", "ApFin_fee_BT_S"),
|
||||
Map.entry("其他应付", "ApFin_other_BT_S"),
|
||||
Map.entry("产品委外应付", "ApFin_product_BT_S"),
|
||||
Map.entry("标准采购应付", "ApFin_pur_BT_S"),
|
||||
Map.entry("采购费用应付", "ApFin_purfee_BT_S"),
|
||||
Map.entry("销售费用应付", "ApFin_salefee_BT_S"),
|
||||
Map.entry("服务采购应付", "ApFin_service_BT_S"),
|
||||
Map.entry("资产采购应付", "ap_finapbill_asset_BT_S")
|
||||
);
|
||||
|
||||
/** 财务应收单(ar_finarbill)单据类型映射:中文名称 -> 星瀚编码(当前付款链路未启用,应收结算单推送规则待业务确认)。 */
|
||||
public static final Map<String, String> AR_BILL_BILLTYPE_MAP = Map.ofEntries(
|
||||
Map.entry("借贷项调整", "arfin_borrowar_BT_S"),
|
||||
Map.entry("其他应收", "arfin_other_BT_S"),
|
||||
Map.entry("销售费用应收", "arfin_salefee_BT_S"),
|
||||
Map.entry("服务销售应收", "arfin_sersal_BT_S"),
|
||||
Map.entry("标准销售应收", "arfin_standard_BT_S")
|
||||
);
|
||||
|
||||
/** A302-2 默认单据类型名称:服务销售应收(经 AR_BILL_BILLTYPE_MAP 转换为编码)。待确认:具体类型需金蝶侧最终确认,确认后修改此名称即可。 */
|
||||
public static final String BFI_AR_BILL_BILLTYPE_NAME = "服务销售应收";
|
||||
|
||||
/** A302-2 款项性质编码(暂按 1001 一般销售,与应付 2001 材料采购区分)。待确认:编码需金蝶侧最终确认,确认后如有调整直接改此常量。 */
|
||||
public static final String BFI_AR_BILL_PAY_PROPERTY = "1001";
|
||||
|
||||
/** A302-2 付款方式:现销 CASH(与应付现购对称,由金蝶自动推收款)。待确认:取值需金蝶侧确认。 */
|
||||
public static final String BFI_AR_BILL_PAY_MODE_CASH = "CASH";
|
||||
|
||||
/** A302-2 必填:外部单据类型(应收单来源标识)。待确认:取值需金蝶侧确认,确认后如有调整直接改此常量。 */
|
||||
public static final String BFI_AR_BILL_SRC_TYPE = "TMS财务应收单";
|
||||
|
||||
/** A303-1 上游来源单据类型:财务应收单(awrp_src_bt)。 */
|
||||
public static final String BFI_AR_BILL_SRC_BT = "ar_finarbill";
|
||||
|
||||
/** A301-3 认领推送核心单据类型:其他应付(e_corebilltype)。 */
|
||||
public static final String BFI_CLAIM_CORE_BILL_TYPE = "ar_finarbill";
|
||||
|
||||
/** A303-1 发票种类:数电发票(普通发票)。 */
|
||||
public static final String BFI_INVOICE_TYPE_DIGITAL_GENERAL = "10xdp";
|
||||
|
||||
/** A303-1 发票种类:数电发票(增值税专用发票)。 */
|
||||
public static final String BFI_INVOICE_TYPE_DIGITAL_SPECIAL = "08xdp";
|
||||
|
||||
/** A303-1 特殊票种:货物运输(运费/货物运输类发票)。 */
|
||||
public static final String BFI_INVOICE_SPECIAL_TYPE_FREIGHT = "E04";
|
||||
|
||||
/** A303-1 特殊票种:非特殊票种(其余费用类发票,如其他、非货物运输的运输类)。 */
|
||||
public static final String BFI_INVOICE_SPECIAL_TYPE_NONE = "00";
|
||||
|
||||
/** A303-1 特殊票种货物运输判定关键字:命中商品名称或商品和服务分类简称即视为货物运输类。 */
|
||||
public static final List<String> BFI_INVOICE_FREIGHT_KEYWORDS = List.of("货物运输", "运费");
|
||||
|
||||
/** A303-1 货物运输单据体运输工具种类合法值:字典 transport_type 翻译名称命中则直送,否则兜底其它运输工具。 */
|
||||
public static final List<String> BFI_FREIGHT_TRANSPORT_TYPES = List.of("铁路运输", "公路运输", "水路运输", "航空运输", "管道运输", "其它运输工具");
|
||||
|
||||
/** A303-1 货物运输单据体运输工具种类兜底值。 */
|
||||
public static final String BFI_FREIGHT_TRANSPORT_TYPE_OTHER = "其它运输工具";
|
||||
|
||||
/** 金蝶同步状态:开票完成(A303-5 查到发票,终态,开票申请专用)。 */
|
||||
public static final String KINGDEE_STATUS_INVOICED = "invoiced";
|
||||
|
||||
/** 付款申请单(ap_payapply)单据类型映射:中文名称 -> 星瀚编码(使用规则待业务确认)。 */
|
||||
public static final Map<String, String> PAY_APPLY_BILLTYPE_MAP = Map.of(
|
||||
"采购付款申请", "ap_payapply_BT_S",
|
||||
"其他付款申请", "ap_payapply_oth_BT_S"
|
||||
);
|
||||
|
||||
/** 金蝶同步状态:未生成(新建单据默认)。 */
|
||||
public static final String KINGDEE_STATUS_UNSYNCED = "unsynced";
|
||||
|
||||
/** 金蝶同步状态:已生成(推送成功,等待付款)。 */
|
||||
public static final String KINGDEE_STATUS_SYNCED = "synced";
|
||||
|
||||
/** 金蝶同步状态:付款中(A103-2 paystatus = Inpayment 部分付款)。 */
|
||||
public static final String KINGDEE_STATUS_PAYING = "paying";
|
||||
|
||||
/** 金蝶同步状态:已付款(A103-2 paystatus = Alreadypay,终态)。 */
|
||||
public static final String KINGDEE_STATUS_PAID = "paid";
|
||||
|
||||
/** 金蝶同步状态:已关闭(A103-2 billstatus = D,终态)。 */
|
||||
public static final String KINGDEE_STATUS_CLOSED = "closed";
|
||||
|
||||
/** 金蝶同步状态:生成失败(推送异常/金蝶返回失败,可重推)。 */
|
||||
public static final String KINGDEE_STATUS_FAILED = "failed";
|
||||
|
||||
/** 收票单推送状态:未推送(新建单据默认,票池绑定层的 kingdee_status 与此无关)。 */
|
||||
public static final String RECEIVED_PUSH_UNSYNCED = "unsynced";
|
||||
|
||||
/** 收票单推送状态:金蝶收票单已保存(A303-2 成功,等待/已完成指定应付)。 */
|
||||
public static final String RECEIVED_PUSH_PUSHED = "pushed";
|
||||
|
||||
/** 收票单推送状态:已指定应付(A302-6 成功,一票一应付,终态;金蝶侧异常回收获占)。 */
|
||||
public static final String RECEIVED_PUSH_ASSIGNED = "assigned";
|
||||
|
||||
/** 收票单推送状态:推送失败(保存/指定异常或金蝶侧删除红冲,人工处理后重推)。 */
|
||||
public static final String RECEIVED_PUSH_FAILED = "failed";
|
||||
|
||||
/** A303-2 业务类型(PUR:采购 / FEE:费用,TMS 服务费进项票暂按采购)。待确认:取值需金蝶侧最终确认。 */
|
||||
public static final String BFI_RECEIVED_BIZ_TYPE = "PUR";
|
||||
|
||||
/** A303-2 必填:外部单据类型(收票单来源标识)。待确认:取值需金蝶侧确认,确认后如有调整直接改此常量。 */
|
||||
public static final String BFI_RECEIVED_INVOICE_SRC_TYPE = "TMS收票登记";
|
||||
|
||||
/** A103-2 付款状态:已付款。 */
|
||||
public static final String BFI_PAY_STATUS_ALREADY_PAY = "Alreadypay";
|
||||
|
||||
/** A103-2 付款状态:部分付款。 */
|
||||
public static final String BFI_PAY_STATUS_IN_PAYMENT = "Inpayment";
|
||||
|
||||
/** A103-2 付款状态:未付款。 */
|
||||
public static final String BFI_PAY_STATUS_UNPAID = "Unpaid";
|
||||
|
||||
/** A103-2 单据状态:已关闭。 */
|
||||
public static final String BFI_BILL_STATUS_CLOSED = "D";
|
||||
|
||||
/** A801-1 查询限定发票状态:0 正常(票易通字典),作废/异常票不进镜像、不可补查 */
|
||||
public static final String POOL_INVOICE_STATUS_NORMAL = "0";
|
||||
|
||||
/** A301-3 认领推送:收款类型.编码(示例 101,待金蝶确认后如有调整直接改此常量)。 */
|
||||
public static final String BFI_CLAIM_REC_PAY_TYPE = "999";
|
||||
|
||||
/** A301-3 认领推送:付款人类型(默认客户 bd_customer,待金蝶确认后如有调整直接改此常量)。 */
|
||||
public static final String BFI_CLAIM_PAYMENT_TYPE = "bd_customer";
|
||||
|
||||
/** 收款流水推送状态:未推送(未满足推送条件或尚未轮到推送)。 */
|
||||
public static final String RECEIPT_CLAIM_PUSH_NONE = "none";
|
||||
|
||||
/** 收款流水推送状态:已推送(终态闸门,不允许二次推送)。 */
|
||||
public static final String RECEIPT_CLAIM_PUSH_PUSHED = "pushed";
|
||||
|
||||
/** 收款流水推送状态:推送失败(本地认领保留,可重新推送,重试复用同一来源编码)。 */
|
||||
public static final String RECEIPT_CLAIM_PUSH_FAILED = "failed";
|
||||
|
||||
/** 收款流水金蝶回写状态:未推送(认领完成推送前的初始状态)。 */
|
||||
public static final String RECEIPT_CLAIM_BILL_NONE = "none";
|
||||
|
||||
/** 金蝶同步状态:认领结果已推送(A301-3 成功,等待金蝶收款单审核/收款,可继续回写)。 */
|
||||
public static final String KINGDEE_STATUS_PUSHED = "pushed";
|
||||
|
||||
/** 金蝶同步状态:收款单已收款(A301-1 billstatus = D,终态)。 */
|
||||
public static final String KINGDEE_STATUS_RECEIVED = "received";
|
||||
|
||||
/** 选票金额使用状态可选值:仅保留剩余可用金额>0 且金额使用状态为 0 的可用票,2-全部使用 */
|
||||
public static final String POOL_AMOUNT_USE_STATUS_SELECTABLE = "0";
|
||||
|
||||
/** 单请求远程补页调用上限:金额可用过滤在拉取后进行,过滤率过低时以此封顶防请求无限拉长 */
|
||||
public static final int POOL_MAX_REMOTE_CALLS = 3;
|
||||
|
||||
/** 选票单据使用状态固定为未用(1,票易通字典):仅未使用的票可被认领/登记,前端不提供该筛选条件 */
|
||||
public static final String POOL_EXPENSE_STATUS_UNUSED = "1";
|
||||
|
||||
/**
|
||||
* 按中文名称从映射字典转换单据类型编码。
|
||||
*
|
||||
* @param billtypeMap 单据类型映射字典
|
||||
* @param name 单据类型中文名称
|
||||
* @return 星瀚单据类型编码,未维护时返回 null
|
||||
*/
|
||||
public static String billtypeBy(Map<String, String> billtypeMap, String name) {
|
||||
return billtypeMap.get(name);
|
||||
}
|
||||
|
||||
private KingdeeBfiConstant() {
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.constant;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定/解锁(lockBusiData / unLockNotifyBusiData)固定值。
|
||||
* <p>lockBusiData 由 WLTMS 调金蝶(源系统 WLTMS、目标系统 XH),
|
||||
* unLockNotifyBusiData 由金蝶调 WLTMS(源系统 XH、目标系统 WLTMS);
|
||||
* WLTMS 单据类型自定义,金蝶目标单据类型为固定值</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class KingdeeLockConstant {
|
||||
|
||||
/** 锁动作:锁定 */
|
||||
public static final String ACTION_LOCK = "LOCK";
|
||||
|
||||
/** 锁动作:解锁 */
|
||||
public static final String ACTION_UNLOCK = "UNLOCK";
|
||||
|
||||
/** 来源操作类型:单据新增 */
|
||||
public static final String OPERATION_SAVE = "SAVE";
|
||||
|
||||
/** 来源操作类型:单据删除(解锁回调携带 DELETE 时 WLTMS 废除源单并还原关联数据) */
|
||||
public static final String OPERATION_DELETE = "DELETE";
|
||||
|
||||
/** 系统标识:WLTMS(运输管理系统,即本系统) */
|
||||
public static final String SYSTEM_WLTMS = "WLTMS";
|
||||
|
||||
/** 系统标识:金蝶星瀚 XH */
|
||||
public static final String SYSTEM_XH = "XH";
|
||||
|
||||
/** 锁状态:锁定失败(补偿定时任务的重试对象) */
|
||||
public static final String STATUS_FAILED = "failed";
|
||||
|
||||
/** 锁状态:金蝶锁定中(源单冻结,不可编辑/作废) */
|
||||
public static final String STATUS_LOCKED = "locked";
|
||||
|
||||
/** 锁状态:已解锁(终态;同单重推生成新锁记录) */
|
||||
public static final String STATUS_UNLOCKED = "unlocked";
|
||||
|
||||
/** WLTMS 单据类型:正式结算单(应付/应收共用,按结算单 settlementType 区分金蝶目标类型) */
|
||||
public static final String SOURCE_SETTLEMENT = "SETTLEMENT";
|
||||
|
||||
/** WLTMS 单据类型:开票申请单 */
|
||||
public static final String SOURCE_INVOICE_APPLY = "INVOICE_APPLY";
|
||||
|
||||
/** WLTMS 单据类型:付款申请单(映射预留,本期不锁) */
|
||||
public static final String SOURCE_PAY_APPLY = "PAY_APPLY";
|
||||
|
||||
/** WLTMS 单据类型:收票单(映射预留,本期不锁) */
|
||||
public static final String SOURCE_RECEIVED_INVOICE = "RECEIVED_INVOICE";
|
||||
|
||||
/** 金蝶目标单据类型:财务应付单(A102-2,应付结算单推送后锁定) */
|
||||
public static final String TARGET_AP_BILL = "ApFin_other_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:财务应收单(A302-2,应收结算单推送后锁定) */
|
||||
public static final String TARGET_AR_BILL = "arfin_other_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:开票单(A303-1 开票申请单,按发票张次逐张一行锁记录) */
|
||||
public static final String TARGET_INVOICE_BILL = "cdm_paybill_bank_elect_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:付款申请单(映射预留,本期不锁) */
|
||||
public static final String TARGET_PAY_APPLY = "ap_payapply_oth_BT_S";
|
||||
|
||||
/** 金蝶目标单据类型:收票单(映射预留,本期不锁) */
|
||||
public static final String TARGET_RECEIVED_INVOICE = "cdm_recbill_bank_BT_S";
|
||||
|
||||
/** 锁定重试上限:补偿任务超过该次数不再自动重试,人工排查(失败原因见 fail_reason) */
|
||||
public static final int RETRY_MAX_COUNT = 5;
|
||||
|
||||
/** 金蝶目标单据类型 -> WLTMS 单据类型反查映射(解锁回调按 targetBillType 分发废除还原用) */
|
||||
public static final Map<String, String> TARGET_TO_SOURCE_MAP = Map.ofEntries(
|
||||
Map.entry(TARGET_AP_BILL, SOURCE_SETTLEMENT),
|
||||
Map.entry(TARGET_AR_BILL, SOURCE_SETTLEMENT),
|
||||
Map.entry(TARGET_INVOICE_BILL, SOURCE_INVOICE_APPLY),
|
||||
Map.entry(TARGET_PAY_APPLY, SOURCE_PAY_APPLY),
|
||||
Map.entry(TARGET_RECEIVED_INVOICE, SOURCE_RECEIVED_INVOICE)
|
||||
);
|
||||
|
||||
private KingdeeLockConstant() {
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.constant;
|
||||
|
||||
/**
|
||||
* 运输业务固定值。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class TransportBusiConstant {
|
||||
|
||||
/** 付款类型:项目预付。 */
|
||||
public static final String PAYMENT_TYPE_PROJECT_ADVANCE = "project_advance";
|
||||
|
||||
/** 付款类型:进度预付。 */
|
||||
public static final String PAYMENT_TYPE_PROGRESS_ADVANCE = "progress_advance";
|
||||
|
||||
/** 付款类型:结算付款(关联正式结算单的正式付款)。 */
|
||||
public static final String PAYMENT_TYPE_SETTLEMENT = "settlement_payment";
|
||||
|
||||
/** 结算单类型:预结算。 */
|
||||
public static final String SETTLEMENT_BILL_PRE = "pre";
|
||||
|
||||
/** 结算单类型:正式结算。 */
|
||||
public static final String SETTLEMENT_BILL_FORMAL = "formal";
|
||||
|
||||
/** 审批状态:草稿(允许编辑、删除)。 */
|
||||
public static final String DRAFT = "draft";
|
||||
|
||||
/** 审批状态:审批中(审核节点,允许通过/驳回)。 */
|
||||
public static final String REVIEWING = "reviewing";
|
||||
|
||||
/** 审批状态:审批通过。 */
|
||||
public static final String APPROVED = "approved";
|
||||
|
||||
/** 审批状态:已驳回(允许再编辑后重新提交)。 */
|
||||
public static final String RETURNED = "returned";
|
||||
|
||||
/** 审批状态:已作废(终态)。 */
|
||||
public static final String VOIDED = "voided";
|
||||
|
||||
private TransportBusiConstant() {
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiCustomerVO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 业财融合(BFI)客商查询控制器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("/bfi")
|
||||
@Tag(name = "BFI客商", description = "业财融合客商查询接口")
|
||||
public class BfiCustomerController extends BladeController {
|
||||
|
||||
private final IBfiService bfiService;
|
||||
|
||||
@GetMapping("/customer/query-by-credit-code")
|
||||
@Operation(summary = "按统一社会信用代码查询金蝶客商", description = "新增客商时查询金蝶是否已有该客商,已审核则回填工商信息")
|
||||
public R<BfiCustomerVO> queryByCreditCode(
|
||||
@Parameter(description = "统一社会信用代码", required = true) @RequestParam String creditCode) {
|
||||
if (StringUtil.isBlank(creditCode)) {
|
||||
return R.fail("统一社会信用代码不能为空");
|
||||
}
|
||||
BfiCustomerQueryDTO param = new BfiCustomerQueryDTO();
|
||||
param.setPageNo(1);
|
||||
param.setPageSize(1);
|
||||
param.setSocietycreditcode(List.of(creditCode.trim().toUpperCase()));
|
||||
BfiPageVO<BfiCustomerVO> page = bfiService.queryCustomer(param);
|
||||
List<BfiCustomerVO> rows = page == null ? null : page.getRows();
|
||||
return R.data(rows == null || rows.isEmpty() ? null : rows.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiExchangeRateVO;
|
||||
import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 业财融合(BFI)汇率查询控制器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("/bfi")
|
||||
@Tag(name = "BFI汇率", description = "业财融合汇率查询接口")
|
||||
public class BfiExchangeRateController extends BladeController {
|
||||
|
||||
private final IBfiService bfiService;
|
||||
|
||||
@GetMapping("/exchange-rate")
|
||||
@Operation(summary = "BFI汇率查询", description = "根据结算币种编码和汇率日期查询BFI汇率")
|
||||
public R<BfiExchangeRateVO> getBfiExchangeRate(
|
||||
@RequestParam String currencyCode,
|
||||
@RequestParam(required = false) String effectdate) {
|
||||
LocalDate date = StringUtil.isBlank(effectdate) ? LocalDate.now() : LocalDate.parse(effectdate);
|
||||
List<BfiExchangeRateVO> rates = bfiService.queryExchangeRate(date, date);
|
||||
if (rates == null || rates.isEmpty()) {
|
||||
return R.fail("未查询到该日期的BFI汇率数据");
|
||||
}
|
||||
BfiExchangeRateVO matched = rates.stream()
|
||||
.filter(r -> currencyCode.equalsIgnoreCase(r.getOrgcurNumber()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (matched == null) {
|
||||
return R.fail("未查询到币种 " + currencyCode + " 的汇率数据");
|
||||
}
|
||||
return R.data(matched);
|
||||
}
|
||||
}
|
||||
+26
-2
@@ -31,6 +31,7 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.excel.util.ExcelUtil;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
@@ -61,6 +62,7 @@ import java.util.List;
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@PreAuth(menu = "customer_archive")
|
||||
@@ -134,13 +136,25 @@ public class CustomerArchiveController extends BladeController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过
|
||||
* 审核通过(通过后自动推送金蝶,推送失败不影响审批结果,失败原因以警告文案返回)
|
||||
*/
|
||||
@PostMapping("/approve")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "审核通过", description = "传入id")
|
||||
public R approve(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.status(customerArchiveService.approve(id));
|
||||
boolean result = customerArchiveService.approve(id);
|
||||
if (!result) {
|
||||
return R.status(false);
|
||||
}
|
||||
try {
|
||||
customerArchiveService.syncKingdee(id);
|
||||
return R.success("审核通过");
|
||||
} catch (Exception exception) {
|
||||
log.error("客商审批通过后推送金蝶异常,id={}", id, exception);
|
||||
String reason = exception.getMessage() == null ? "金蝶接口调用异常" : exception.getMessage();
|
||||
// 与结算/付款/开票审批口径一致:审批已生效,推送失败原因以 data=false 警告文案返回
|
||||
return R.data(Boolean.FALSE, "审核通过,但推送金蝶失败:" + reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,6 +188,16 @@ public class CustomerArchiveController extends BladeController {
|
||||
return R.status(customerArchiveService.removeDraft(ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送金蝶(审批通过后失败重推入口)
|
||||
*/
|
||||
@PostMapping("/sync-kingdee")
|
||||
@ApiOperationSupport(order = 12)
|
||||
@Operation(summary = "推送金蝶", description = "传入id,仅审批通过的客商可推送,已推送/金蝶已有自动幂等跳过")
|
||||
public R<CustomerArchive> syncKingdee(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.data(customerArchiveService.syncKingdee(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 评分明细模板
|
||||
*/
|
||||
|
||||
+41
-3
@@ -22,17 +22,22 @@ import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementBatchPaymentRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementInvoiceClaimRequest;
|
||||
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
|
||||
import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetailFee;
|
||||
import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
|
||||
import org.springblade.transport.pojo.vo.FormalSettlementVO;
|
||||
import org.springblade.transport.excel.FormalSettlementExcel;
|
||||
import org.springblade.transport.pojo.vo.PreSettlementVO;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IInvoiceReceiptService;
|
||||
import org.springblade.transport.service.IKingdeeBillLockService;
|
||||
import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.IReceiptFlowService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -59,8 +64,10 @@ import java.math.RoundingMode;
|
||||
@Tag(name = "正式结算单", description = "正式结算单管理")
|
||||
public class FormalSettlementController extends BladeController {
|
||||
private final IFormalSettlementService formalSettlementService;
|
||||
private final IKingdeeBillLockService kingdeeBillLockService;
|
||||
private final IPreSettlementService preSettlementService;
|
||||
private final IReceiptFlowService receiptFlowService;
|
||||
private final IInvoiceReceiptService invoiceReceiptService;
|
||||
|
||||
@GetMapping("/list")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@@ -190,7 +197,19 @@ public class FormalSettlementController extends BladeController {
|
||||
@PostMapping("/approve")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "审批通过")
|
||||
public R approve(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.approve(request); return R.success("审批通过"); }
|
||||
public R approve(@RequestBody FormalSettlementStatusRequest request) {
|
||||
formalSettlementService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
|
||||
@PostMapping("/return")
|
||||
@ApiOperationSupport(order = 12)
|
||||
@@ -204,11 +223,23 @@ public class FormalSettlementController extends BladeController {
|
||||
|
||||
@PostMapping("/sync-kingdee")
|
||||
@ApiOperationSupport(order = 14)
|
||||
@Operation(summary = "推送金蝶应付单")
|
||||
@Operation(summary = "推送金蝶(应付单/应收单)")
|
||||
public R<String> syncKingdee(@RequestParam Long id) { return R.data(formalSettlementService.syncKingdee(id)); }
|
||||
|
||||
@GetMapping("/detail-fees")
|
||||
@PostMapping("/sync-kingdee-ar-result")
|
||||
@ApiOperationSupport(order = 15)
|
||||
@Operation(summary = "手动勾稽金蝶应收单(A302-4 回写纠偏)")
|
||||
public R<String> syncKingdeeArResult() { return R.data(formalSettlementService.syncKingdeeArResult()); }
|
||||
|
||||
@PostMapping("/retry-kingdee-bill-lock")
|
||||
@ApiOperationSupport(order = 17)
|
||||
@Operation(summary = "手动补偿金蝶单据锁定(扫锁定失败锁记录重调lockBusiData,应付/应收/开票通用)")
|
||||
public R<String> retryKingdeeBillLock() {
|
||||
return R.data(kingdeeBillLockService.retryFailedLocks());
|
||||
}
|
||||
|
||||
@GetMapping("/detail-fees")
|
||||
@ApiOperationSupport(order = 16)
|
||||
@Operation(summary = "正式结算货物费用快照")
|
||||
public R<List<FormalSettlementDetailFee>> detailFees(@RequestParam Long detailId) {
|
||||
return R.data(formalSettlementService.detailFees(detailId));
|
||||
@@ -250,4 +281,11 @@ public class FormalSettlementController extends BladeController {
|
||||
public R<List<Map<String, Object>>> receiptClaims(@RequestParam Long formalSettlementId) {
|
||||
return R.data(receiptFlowService.settlementClaims(formalSettlementId));
|
||||
}
|
||||
|
||||
@GetMapping("/invoice-pool")
|
||||
@ApiOperationSupport(order = 21)
|
||||
@Operation(summary = "金蝶票据池镜像分页查询(认领查询)")
|
||||
public R<IPage<KingdeeInvoicePoolVO>> invoicePool(KingdeeInvoicePoolQueryDTO query) {
|
||||
return R.data(invoiceReceiptService.invoicePoolPage(query));
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -35,6 +35,7 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
import org.springblade.transport.pojo.dto.InvoiceApplicationSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
|
||||
@@ -131,6 +132,15 @@ public class InvoiceApplicationController extends BladeController {
|
||||
@Operation(summary = "审批通过开票申请")
|
||||
public R approve(@RequestBody InvoiceApplicationStatusRequest request) {
|
||||
invoiceApplicationService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
|
||||
@@ -156,4 +166,11 @@ public class InvoiceApplicationController extends BladeController {
|
||||
public R<String> syncKingdee(@RequestParam Long id) {
|
||||
return R.data(invoiceApplicationService.syncKingdee(id));
|
||||
}
|
||||
|
||||
@PostMapping("/sync-kingdee-invoice-result")
|
||||
@ApiOperationSupport(order = 13)
|
||||
@Operation(summary = "手动回写金蝶开票结果(A303-5)")
|
||||
public R<String> syncKingdeeInvoiceResult() {
|
||||
return R.data(invoiceApplicationService.syncKingdeeInvoiceResult());
|
||||
}
|
||||
}
|
||||
|
||||
+43
-9
@@ -29,16 +29,19 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
|
||||
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
|
||||
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
|
||||
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
|
||||
import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
|
||||
import org.springblade.transport.service.IInvoiceReceiptService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -80,9 +83,24 @@ public class InvoiceReceiptController extends BladeController {
|
||||
|
||||
@GetMapping("/invoice-pool")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "查询金蝶进项发票票据池")
|
||||
public R<List<KingdeeInvoicePool>> invoicePool(@RequestParam(required = false) String keyword) {
|
||||
return R.data(invoiceReceiptService.invoicePool(keyword));
|
||||
@Operation(summary = "金蝶票据池镜像分页查询(含认领/登记标记)")
|
||||
public R<IPage<KingdeeInvoicePoolVO>> invoicePool(KingdeeInvoicePoolQueryDTO query) {
|
||||
return R.data(invoiceReceiptService.invoicePoolPage(query));
|
||||
}
|
||||
|
||||
@PostMapping("/sync-invoice-pool")
|
||||
@ApiOperationSupport(order = 13)
|
||||
@Operation(summary = "手动触发金蝶全票池发票同步(窗口拉取+镜像全列upsert)")
|
||||
public R<String> syncInvoicePool() {
|
||||
return R.data(invoiceReceiptService.syncKingdeeInvoicePool());
|
||||
}
|
||||
|
||||
@GetMapping("/invoice-pool/attachment/download")
|
||||
@ApiOperationSupport(order = 17)
|
||||
@Operation(summary = "金蝶票池发票文件下载(A801-2 流式回源)",
|
||||
description = "按发票号定位金蝶侧文件:本地镜像优先,镜像未落库实时回源 A801-1;成功返回文件流,失败返回 JSON 错误")
|
||||
public void downloadPoolAttachment(@RequestParam String invoiceNo, HttpServletResponse response) {
|
||||
invoiceReceiptService.downloadPoolAttachment(invoiceNo, response);
|
||||
}
|
||||
|
||||
@GetMapping("/settlement-candidates")
|
||||
@@ -129,6 +147,15 @@ public class InvoiceReceiptController extends BladeController {
|
||||
@Operation(summary = "审批通过收票登记")
|
||||
public R approve(@RequestBody InvoiceReceiptStatusRequest request) {
|
||||
invoiceReceiptService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
|
||||
@@ -148,10 +175,17 @@ public class InvoiceReceiptController extends BladeController {
|
||||
return R.success("作废成功");
|
||||
}
|
||||
|
||||
@PostMapping("/sync-kingdee")
|
||||
@ApiOperationSupport(order = 12)
|
||||
@Operation(summary = "同步金蝶发票状态")
|
||||
public R<String> syncKingdee(@RequestParam Long id) {
|
||||
return R.data(invoiceReceiptService.syncKingdee(id));
|
||||
@PostMapping("/push-kingdee")
|
||||
@ApiOperationSupport(order = 15)
|
||||
@Operation(summary = "批量推送金蝶收票单(A303-2 收票单保存 + A302-6 收票指定应付)")
|
||||
public R<String> pushKingdee(@RequestBody List<Long> ids) {
|
||||
return R.data(invoiceReceiptService.pushKingdeeReceivedInvoice(ids));
|
||||
}
|
||||
|
||||
@PostMapping("/sync-kingdee-result")
|
||||
@ApiOperationSupport(order = 16)
|
||||
@Operation(summary = "手动触发金蝶收票单结果回写(A801-3 异常回收/指定补偿)")
|
||||
public R<String> syncKingdeeResult() {
|
||||
return R.data(invoiceReceiptService.syncKingdeeReceivedInvoiceResult());
|
||||
}
|
||||
}
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillLockResultVO;
|
||||
import org.springblade.transport.service.IKingdeeBillLockService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定回调 控制器
|
||||
* <p>
|
||||
* unLockNotifyBusiData:金蝶(星瀚)侧解锁/删单后回调 WLTMS 送结果,
|
||||
* 无登录态公开端点(模式同 CustomerArchivePublicController);
|
||||
* operationType=DELETE 时 WLTMS 废除源单并还原关联数据。
|
||||
* 应答与 lockBusiData 出参同构:{updateCount, updateResultList},逐单处理(1-成功,0-未匹配/失败)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@TenantIgnore
|
||||
// @PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@RequestMapping("kingdee-bfi")
|
||||
@Tag(name = "金蝶单据锁定回调", description = "金蝶星瀚调用:单据解锁/删单通知")
|
||||
public class KingdeeBfiPublicController {
|
||||
|
||||
private final IKingdeeBillLockService kingdeeBillLockService;
|
||||
|
||||
/**
|
||||
* 单据解锁通知(unLockNotifyBusiData):多张单据编号逗号拼接批量回调,逐张处理
|
||||
*
|
||||
* @param param 金蝶回调参数
|
||||
* @return 处理结果,与 lockBusiData 出参同构
|
||||
*/
|
||||
@PostMapping("unLockNotifyBusiData")
|
||||
@Operation(summary = "单据解锁通知", description = "金蝶侧解锁/删单后回调;operationType=DELETE 时废除源单并还原关联数据")
|
||||
public BfiBillLockResultVO unLockNotifyBusiData(@RequestBody BfiBillLockDTO param) {
|
||||
log.info("金蝶单据解锁回调:{}", JSON.toJSONString(param));
|
||||
try {
|
||||
return BfiBillLockResultVO.of(kingdeeBillLockService.onUnlockNotify(param));
|
||||
} catch (Exception exception) {
|
||||
log.error("金蝶单据解锁回调处理失败:lockAction={},operationType={},targetBillType={},targetBillNo={}",
|
||||
param == null ? null : param.getLockAction(), param == null ? null : param.getOperationType(),
|
||||
param == null ? null : param.getTargetBillType(), param == null ? null : param.getTargetBillNo(), exception);
|
||||
return BfiBillLockResultVO.of(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-1
@@ -29,10 +29,12 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
|
||||
import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO;
|
||||
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
|
||||
import org.springblade.transport.pojo.vo.ProjectAdvanceWriteOffOptionVO;
|
||||
import org.springblade.transport.service.IPaymentApplicationService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -66,6 +68,17 @@ public class PaymentApplicationController extends BladeController {
|
||||
@RequestParam Long referenceId, @RequestParam(required = false) Long excludeId) {
|
||||
return R.data(paymentApplicationService.referenceAmount(paymentType, referenceId, excludeId));
|
||||
}
|
||||
|
||||
@GetMapping("/project-advance-write-off-options")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "按合同查询可选项目预付核销列表")
|
||||
public R<List<ProjectAdvanceWriteOffOptionVO>> projectAdvanceWriteOffOptions(
|
||||
@RequestParam Long contractId,
|
||||
@RequestParam(required = false) String excludeSettlementType,
|
||||
@RequestParam(required = false) Long excludeSettlementId) {
|
||||
return R.data(paymentApplicationService.projectAdvanceWriteOffOptions(
|
||||
contractId, excludeSettlementType, excludeSettlementId));
|
||||
}
|
||||
@PostMapping("/save")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "保存付款申请")
|
||||
@@ -81,7 +94,19 @@ public class PaymentApplicationController extends BladeController {
|
||||
@PostMapping("/approve")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "审批通过付款申请")
|
||||
public R approve(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.approve(request); return R.success("审批通过"); }
|
||||
public R approve(@RequestBody PaymentApplicationStatusRequest request) {
|
||||
paymentApplicationService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
@PostMapping("/return")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "驳回付款申请")
|
||||
@@ -100,4 +125,8 @@ public class PaymentApplicationController extends BladeController {
|
||||
public R<List<String>> syncKingdeeBatch(@RequestBody List<Long> ids) {
|
||||
return R.data(paymentApplicationService.syncKingdeeBatch(ids));
|
||||
}
|
||||
@PostMapping("/sync-kingdee-result")
|
||||
@ApiOperationSupport(order = 12)
|
||||
@Operation(summary = "手动回写金蝶付款结果(A103-2/A103-4)")
|
||||
public R<String> syncKingdeeResult() { return R.data(paymentApplicationService.syncKingdeePaymentResult()); }
|
||||
}
|
||||
|
||||
+10
-2
@@ -84,7 +84,15 @@ public class ReceiptClaimRecordController extends BladeController {
|
||||
@PostMapping("/void")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "作废认领记录并生成金蝶认领冲单")
|
||||
public R<String> voidClaim(@RequestParam Long id) {
|
||||
return R.data(receiptClaimRecordService.voidClaim(id));
|
||||
public R<Void> voidClaim(@RequestParam Long id) {
|
||||
receiptClaimRecordService.voidClaim(id);
|
||||
return R.success("作废成功");
|
||||
}
|
||||
|
||||
@PostMapping("/query-kingdee-result")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "查询金蝶认领结果(A301-1 收款单查询回写)")
|
||||
public R<Integer> queryKingdeeResult() {
|
||||
return R.data(receiptClaimRecordService.refreshKingdeeResult());
|
||||
}
|
||||
}
|
||||
|
||||
+12
-4
@@ -35,8 +35,8 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.dto.ReceiptClaimPushResult;
|
||||
import org.springblade.transport.pojo.dto.ReceiptClaimRequest;
|
||||
import org.springblade.transport.pojo.dto.ReceiptFlowSyncRequest;
|
||||
import org.springblade.transport.pojo.vo.ReceiptFlowVO;
|
||||
import org.springblade.transport.service.IReceiptFlowService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -95,8 +95,16 @@ public class ReceiptFlowController extends BladeController {
|
||||
|
||||
@PostMapping("/sync")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "手动同步金蝶收款流水")
|
||||
public R<Integer> sync(@RequestBody(required = false) ReceiptFlowSyncRequest request) {
|
||||
return R.data(receiptFlowService.sync(request));
|
||||
@Operation(summary = "手动同步金蝶收款流水(A301-2 收款认领公告查询)")
|
||||
public R<Integer> sync() {
|
||||
return R.data(receiptFlowService.sync());
|
||||
}
|
||||
|
||||
@PostMapping("/push-kingdee")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "推送金蝶收款认领(A301-3 收款认领结果推送,仅认领完成流水可推送)")
|
||||
public R<ReceiptClaimPushResult> pushKingdee(@RequestParam Long flowId) {
|
||||
ReceiptClaimPushResult result = receiptFlowService.pushClaimToKingdee(flowId);
|
||||
return result.isSuccess() ? R.data(result) : R.fail(result.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -133,13 +133,15 @@ public class ReceivablePayableDetailController extends BladeController {
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "转结算候选明细")
|
||||
public R<IPage<Map<String, Object>>> transferCandidates(Query query,
|
||||
@RequestParam(required = false) String documentNo,
|
||||
@RequestParam(required = false) String contractNo,
|
||||
@RequestParam(required = false) String contractName,
|
||||
@RequestParam(required = false) String batchNo,
|
||||
@RequestParam(required = false) String generateStartDate,
|
||||
@RequestParam(required = false) String generateEndDate,
|
||||
@RequestParam(required = false) String settlementBillType,
|
||||
@RequestParam(required = false) String settlementType) {
|
||||
return R.data(detailService.transferCandidates(Condition.getPage(query), contractName, batchNo,
|
||||
return R.data(detailService.transferCandidates(Condition.getPage(query), documentNo, contractNo, contractName, batchNo,
|
||||
generateStartDate, generateEndDate, settlementBillType, settlementType));
|
||||
}
|
||||
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportBatchRequest;
|
||||
import org.springblade.transport.pojo.dto.SettlementImportVoidRequest;
|
||||
import org.springblade.transport.pojo.vo.SettlementImportBatchVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||
import org.springblade.transport.service.ISettlementImportBatchService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 导入结算单控制器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@PreAuth(menu = "settlement_import")
|
||||
@RequestMapping("settlement-import")
|
||||
@Tag(name = "导入结算单", description = "导入结算单管理")
|
||||
public class SettlementImportController extends BladeController {
|
||||
|
||||
private final ISettlementImportBatchService settlementImportBatchService;
|
||||
|
||||
@GetMapping("/options")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "筛选项与表单选项")
|
||||
public R<Map<String, List<Map<String, Object>>>> options() {
|
||||
return R.data(settlementImportBatchService.options());
|
||||
}
|
||||
|
||||
@GetMapping("/next-code")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "下一个批次号")
|
||||
public R<String> nextCode() {
|
||||
return R.data(settlementImportBatchService.nextBatchNo());
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "导入结算单批次分页")
|
||||
public R<IPage<SettlementImportBatchVO>> list(SettlementImportBatchRequest request, Query query) {
|
||||
return R.data(settlementImportBatchService.page(Condition.getPage(query), request));
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "导入结算单批次详情")
|
||||
public R<SettlementImportBatchVO> detail(@RequestParam Long id) {
|
||||
return R.data(settlementImportBatchService.detail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/export-template")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导出导入模板")
|
||||
public void exportTemplate(HttpServletResponse response) {
|
||||
settlementImportBatchService.exportTemplate(response);
|
||||
}
|
||||
|
||||
@PostMapping("/parse")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "解析导入 Excel")
|
||||
public R<Map<String, Object>> parse(@RequestPart("file") MultipartFile file) {
|
||||
return R.data(settlementImportBatchService.parse(file));
|
||||
}
|
||||
|
||||
@PostMapping("/validate")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "校验导入数据")
|
||||
public R<WaybillImportValidateResultVO> validate(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.validate(request));
|
||||
}
|
||||
|
||||
@PostMapping("/export-failures")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "导出失败明细")
|
||||
public void exportFailures(@RequestBody SettlementImportBatchRequest request, HttpServletResponse response) {
|
||||
settlementImportBatchService.exportFailures(request, null, response);
|
||||
}
|
||||
|
||||
@PostMapping("/draft")
|
||||
@ApiOperationSupport(order = 9)
|
||||
@Operation(summary = "保存草稿")
|
||||
public R<SettlementImportBatchVO> draft(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.saveDraft(request));
|
||||
}
|
||||
|
||||
@PostMapping("/confirm")
|
||||
@ApiOperationSupport(order = 10)
|
||||
@Operation(summary = "确认导入结算单")
|
||||
public R<SettlementImportBatchVO> confirm(@RequestBody SettlementImportBatchRequest request) {
|
||||
return R.data(settlementImportBatchService.confirm(request));
|
||||
}
|
||||
|
||||
@PostMapping("/void")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "作废导入结算单批次")
|
||||
public R voidBatch(@RequestBody SettlementImportVoidRequest request) {
|
||||
settlementImportBatchService.voidBatch(request);
|
||||
return R.success("作废成功");
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -29,10 +29,16 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 对外路径:{@code /blade-transport/voucher-manage/**}
|
||||
* <p>
|
||||
* 网关 StripPrefix 后落入 {@code /voucher-manage/**};
|
||||
* 同时兼容未去前缀直连 {@code /blade-transport/voucher-manage/**},避免 404 落到静态资源。
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@PreAuth(menu = "voucher_manage")
|
||||
@RequestMapping("/voucher-manage")
|
||||
@RequestMapping({"/voucher-manage", "/blade-transport/voucher-manage"})
|
||||
@Tag(name = "凭证管理", description = "执行凭证批次管理")
|
||||
public class VoucherManageController extends BladeController {
|
||||
private final IVoucherManageService voucherManageService;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user