Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
2026-10-17 01:12:05 +08:00
403 changed files with 44738 additions and 845 deletions
+4
View File
@@ -34,3 +34,7 @@ Thumbs.db
.claude
/outputs/
/.codex-work/
.serena/
.workbuddy/
/data/
/output/
+82
View File
@@ -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 回写的单据生命周期:已推送/审核通过/已收款)
@@ -84,16 +84,11 @@ public class BladeAuthorizationHandler extends AbstractAuthorizationHandler {
if (bladeProperties.isProd() && isWeakPassword(request.getPassword())) {
return buildValidationFailure(ExceptionCode.INVALID_USER_PASSWORD);
}
// 判断账号是否锁定
// 仅按账号锁定;不做 IP 锁定,避免同出口/网关 IP 下一人失败牵连全部未登录用户
OAuth2Validation accountValidation = lockHandler.validateAccountLock(request.getTenantId(), request.getUsername());
if (!accountValidation.isSuccess()) {
return accountValidation;
}
// 判断IP是否锁定
OAuth2Validation ipValidation = lockHandler.validateIpLock(request.getTenantId());
if (!ipValidation.isSuccess()) {
return ipValidation;
}
}
return super.preValidation(request);
}
@@ -105,7 +100,13 @@ public class BladeAuthorizationHandler extends AbstractAuthorizationHandler {
*/
@Override
public void preFailure(OAuth2Request request, OAuth2Validation validation) {
// 处理认证失败,增加错误次数
// 已因锁定失败时不再累加失败次数,避免其他账号重试时被误计并连带锁定
if (validation != null && !validation.isSuccess()
&& ExceptionCode.USER_TOO_MANY_FAILS.getCode() == validation.getCode()) {
log.error("用户:{},认证失败,失败原因:{}", request.getUsername(), validation.getMessage());
return;
}
// 处理认证失败,增加错误次数(仅账号维度)
lockHandler.handleAuthFailure(request.getTenantId(), request.getUsername());
log.error("用户:{},认证失败,失败原因:{}", request.getUsername(), validation.getMessage());
@@ -90,7 +90,7 @@ public class BladeLockHandler {
/**
* 处理认证失败
* 同时增加账号和IP错误次数
* 仅增加账号错误次数;不再累计 IP 失败次数,避免共享出口 IP 误伤其他用户。
*
* @param tenantId 租户id
* @param account 账号
@@ -102,7 +102,6 @@ public class BladeLockHandler {
CompletableFuture.runAsync(() -> {
try {
authLockClient.addAccountFailCount(tenantId, account, userId, ip, userAgent);
authLockClient.addIpFailCount(tenantId, ip, userAgent);
} catch (Exception e) {
log.warn("认证失败计数异常: {}", e.getMessage());
}
@@ -32,6 +32,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;
}
}
@@ -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();
}
@@ -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("获取数据失败");
}
}
@@ -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;
}
@@ -69,4 +69,7 @@ public class OaOrgSyncPageVO implements Serializable {
@Schema(description = "增量同步使用的 OA modified 起始时间,后续分页需原样回传")
private String modifiedSince;
@Schema(description = "全量同步结束后原始数据层级 JSON 文件路径")
private String dumpFilePath;
}
@@ -66,4 +66,7 @@ public class OaPersonSyncPageVO implements Serializable {
@Schema(description = "增量同步使用的 OA modified 起始时间,后续分页需原样回传")
private String modifiedSince;
@Schema(description = "全量同步结束后原始数据层级 JSON 文件路径")
private String dumpFilePath;
}
@@ -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 {
@@ -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;
@@ -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;
@@ -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;
}
@@ -76,6 +76,9 @@ public class PreSettlementSaveRequest implements Serializable {
@Schema(description = "结算合计费用")
private List<SummaryFee> summaryFees;
@Schema(description = "项目预付核销")
private List<SettlementProjectAdvanceWriteOffRequest> projectAdvanceWriteOffs;
@Data
@Schema(description = "结算合计费用")
public static class SummaryFee implements Serializable {
@@ -0,0 +1,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;
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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 = "项目由来")
@@ -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;
@@ -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;
}
@@ -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;
}
@@ -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;
@@ -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;
@@ -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;
}
@@ -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;
@@ -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;
}
@@ -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);
}
@@ -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;
@@ -35,6 +35,7 @@ import org.springblade.transport.pojo.entity.PreSettlementChangeRecord;
import org.springblade.transport.pojo.entity.PreSettlementDetail;
import org.springblade.transport.pojo.entity.PreSettlementDetailFee;
import org.springblade.transport.pojo.entity.PreSettlementSummaryFee;
import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
@@ -100,6 +101,10 @@ public class PreSettlementVO extends PreSettlement {
@Schema(description = "预付信息")
private List<PreSettlementAdvance> advances;
@TableField(exist = false)
@Schema(description = "项目预付核销")
private List<SettlementProjectAdvanceWriteOff> projectAdvanceWriteOffs;
@TableField(exist = false)
@Schema(description = "变更记录")
private List<PreSettlementChangeRecord> changeRecords;
@@ -0,0 +1,72 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 可选项目预付核销选项
*
* @author Chill
*/
@Data
@Schema(description = "可选项目预付核销选项")
public class ProjectAdvanceWriteOffOptionVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "付款申请ID")
private Long paymentApplicationId;
@Schema(description = "预付单号")
private String paymentNo;
@Schema(description = "项目预付金额")
private BigDecimal projectAdvanceAmount;
@Schema(description = "可核销金额")
private BigDecimal availableWriteOffAmount;
@Schema(description = "金蝶预付单号")
private String kingdeeBillNo;
@Schema(description = "创建人姓名")
private String createUserName;
@Schema(description = "创建时间")
private Date createTime;
}
@@ -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)
@@ -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<>();
}
@@ -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;
@@ -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();
}
}
}
}
@@ -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
@@ -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) {
@@ -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;
@@ -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) {
}
}
@@ -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;
}
@@ -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)));
}
}
@@ -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);
/**
* 自定义分页
*
@@ -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,
@@ -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
@@ -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,
@@ -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);
@@ -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>
@@ -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) {
@@ -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) {
@@ -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, "");
}
}
@@ -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();
}
}
@@ -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,
@@ -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);
}
@@ -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
@@ -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;
}
}
+9
View File
@@ -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>
@@ -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 {
@@ -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;
}
}
@@ -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();
}
}
@@ -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() {
}
}
@@ -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() {
}
}
@@ -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() {
}
}
@@ -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() {
}
}
@@ -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() {
}
}
@@ -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));
}
}
@@ -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);
}
}
@@ -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));
}
/**
* 评分明细模板
*/
@@ -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));
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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);
}
}
}
@@ -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()); }
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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));
}
@@ -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("作废成功");
}
}
@@ -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