Merge branch 'feature/payment-apply-bfi-sync' into feature/hcs-api-p0

# Conflicts:
#	blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerKingdeeSyncTask.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java
#	doc/nacos/blade.yaml
#	doc/nacos/third-party-api.yaml
This commit is contained in:
刘泉佳
2026-10-15 04:07:13 +08:00
556 changed files with 35208 additions and 6751 deletions
@@ -31,6 +31,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 业财融合(BFI)推送金蝶配置属性,统一收口 Nacos 配置 thirdParty.bfi.*:
* <p>
@@ -67,6 +70,68 @@ public class BfiProperties {
*/
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();
/**
* 付款申请推送配置项
*/
@@ -114,6 +179,245 @@ public class BfiProperties {
* 真实物料口径与金蝶对齐后替换(与应收/应付联调临时值同源)
*/
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;
}
}
@@ -0,0 +1,58 @@
/**
* 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 org.springblade.core.tool.utils.Func;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;
import java.beans.PropertyEditorSupport;
/**
* 列表查询 deptId 绑定:用户可能绑定多个组织,前端会传入逗号分隔字符串。
* 组织筛选项为单选,这里取第一个有效 ID,避免 Long 类型转换失败。
*
* @author Chill
*/
@ControllerAdvice(basePackages = "org.springblade.transport.controller")
public class DeptIdBindingAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Long.class, "deptId", new PropertyEditorSupport() {
@Override
public void setAsText(String text) {
if (Func.isBlank(text)) {
setValue(null);
return;
}
setValue(Func.firstLong(text));
}
});
}
}
@@ -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,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() {
}
}
@@ -25,6 +25,7 @@
*/
package org.springblade.transport.constant;
import java.util.List;
import java.util.Map;
/**
@@ -58,27 +59,6 @@ public final class KingdeeBfiConstant {
/** 联调默认项目编码:金蝶测试档案值,项目档案与金蝶对齐后替换。 */
public static final String BFI_AP_BILL_PROJECT_NUMBER_TEST = "999";
/** 应付/应收单物料编码:装卸大类费用映射物料。 */
public static final String BFI_MATERIAL_LOADING = "装卸";
/** 应付/应收单物料编码:国际运输业务模式、国际货物运输代理服务项目业务类型映射物料。 */
public static final String BFI_MATERIAL_INTERNATIONAL = "国际货运代理服务费";
/** 应付/应收单物料编码:国内道路货物运输服务前缀,拼接运单运输方式(如 国内道路货物运输服务-公路运输)。 */
public static final String BFI_MATERIAL_DOMESTIC_PREFIX = "国内道路货物运输服务-";
/** 金蝶物料编码映射:物料名称 -> 金蝶物料编码 */
public static final Map<String, String> BFI_MATERIAL_NUMBER_MAP = Map.ofEntries(
Map.entry("国际货运代理服务费", "Item-00001081"),
Map.entry("物流配送", "Item-00001080"),
Map.entry("国内道路货物运输服务-公路运输", "XDWL11010001"),
Map.entry("国内道路货物运输服务-水路运输", "XDWL11010001"),
Map.entry("国内道路货物运输服务-铁路运输", "XDWL11010001"),
Map.entry("国内道路货物运输服务-航空运输", "XDWL11010001"),
Map.entry("国内道路货物运输服务-多式联运", "XDWL11010001"),
Map.entry("装卸", "XDWL11010002")
);
/** 付款类型编码:正式付款(结算付款)。 */
public static final String BFI_PAYMENT_TYPE_FORMAL = "201";
@@ -140,12 +120,30 @@ public final class KingdeeBfiConstant {
/** 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";
@@ -207,13 +205,22 @@ public final class KingdeeBfiConstant {
public static final String POOL_INVOICE_STATUS_NORMAL = "0";
/** A301-3 认领推送:收款类型.编码(示例 101,待金蝶确认后如有调整直接改此常量)。 */
public static final String BFI_CLAIM_REC_PAY_TYPE = "101";
public static final String BFI_CLAIM_REC_PAY_TYPE = "999";
/** A301-3 认领推送:付款人类型(默认客户 bd_customer,待金蝶确认后如有调整直接改此常量)。 */
public static final String BFI_CLAIM_PAYMENT_TYPE = BFI_CUSTOMER_ASSTACT_TYPE;
public static final String BFI_CLAIM_PAYMENT_TYPE = "bd_customer";
/** A301-3 认领推送来源单据编码前缀:SKCL + yyyyMMddHHmmss + 认领id尾号,作 A301-1 回写匹配键。 */
public static final String BFI_CLAIM_SRC_CODE_PREFIX = "SKCL";
/** 收款流水推送状态:未推送(未满足推送条件或尚未轮到推送)。 */
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";
@@ -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() {
}
}
@@ -128,10 +128,28 @@ public class AccidentRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入事故记录", description = "传入excel")
public R importAccidentRecord(MultipartFile file, HttpServletResponse response) {
List<AccidentRecordExcel> failureList = accidentRecordService.importAccidentRecord(ExcelUtil.read(file, AccidentRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AccidentRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
accidentRecordService.importAccidentRecord(ExcelUtil.read(file, AccidentRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AccidentRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", file, AccidentRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -125,10 +125,28 @@ public class AnnualInspectionRecordController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入年检记录", description = "传入excel")
public R importAnnualInspectionRecord(MultipartFile file, HttpServletResponse response) {
List<AnnualInspectionRecordExcel> failureList = annualInspectionRecordService.importAnnualInspectionRecord(ExcelUtil.read(file, AnnualInspectionRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AnnualInspectionRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
annualInspectionRecordService.importAnnualInspectionRecord(ExcelUtil.read(file, AnnualInspectionRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AnnualInspectionRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", file, AnnualInspectionRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -0,0 +1,103 @@
/**
* 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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
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.vo.CertAuditDetailVO;
import org.springblade.transport.pojo.vo.TransportCertRecordVO;
import org.springblade.transport.service.ICertAuditService;
import org.springblade.transport.support.CertAuditPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 认证审核(管理端)
* <p>
* 审核对象是**一条认证记录**,不是「一辆车」或「一个人」。
* 这与旧的车辆认证审核接口有本质区别:旧接口作用于车辆行并原地覆盖,
* 在「生效值 / 待审值分离」之后语义已不成立,故已弃用。
*/
@RestController
@AllArgsConstructor
@RequestMapping({"/certification-audit", "/blade-transport/certification-audit"})
@Tag(name = "认证审核", description = "管理端司机认证与车辆认证审核")
public class CertificationAuditController extends BladeController {
private final ICertAuditService certAuditService;
@GetMapping("/pending-page")
@ApiOperationSupport(order = 1)
@Operation(summary = "认证记录列表", description = "按认证对象类型与审核状态分页查询,按提交时间倒序;状态不传时查审核中的记录。列表同时下发该记录司机的姓名与手机号(司机档案当前值)")
@PreAuth(CertAuditPermissions.AUDIT_VIEW)
public R<IPage<TransportCertRecordVO>> pendingPage(
@Parameter(description = "认证对象类型:driver-司机,vehicle-车辆", required = true)
@RequestParam String bizType,
@Parameter(description = "展示键模糊匹配:提交时的司机姓名 / 车牌号")
@RequestParam(required = false) String bizKey,
@Parameter(description = "司机姓名模糊匹配:当前姓名,同时兼顾提交时的旧名")
@RequestParam(required = false) String driverName,
@Parameter(description = "司机手机号模糊匹配:支持只输后四位")
@RequestParam(required = false) String driverMobile,
@Parameter(description = "审核状态:0审核中 1已通过 2已驳回 3已作废;不传按0(审核中)处理")
@RequestParam(required = false) Integer status,
Query query) {
return R.data(certAuditService.pendingPage(bizType, bizKey, driverName, driverMobile, status, Condition.getPage(query)));
}
@GetMapping("/detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "审核详情", description = "同时返回待审快照与当前生效值,供前端做字段级对比")
@PreAuth(CertAuditPermissions.AUDIT_VIEW)
public R<CertAuditDetailVO> detail(
@Parameter(description = "认证记录主键", required = true) @RequestParam Long id) {
return R.data(certAuditService.detail(id));
}
@PostMapping("/audit")
@ApiOperationSupport(order = 3)
@Operation(summary = "审核认证记录", description = "通过使该记录成为生效快照;驳回只终结该记录,生效快照不变")
@PreAuth(CertAuditPermissions.AUDIT_ACTION)
public R<Boolean> audit(
@Parameter(description = "认证记录主键", required = true) @RequestParam Long id,
@Parameter(description = "是否通过", required = true) @RequestParam Boolean pass,
@Parameter(description = "驳回原因,驳回时必填") @RequestParam(required = false) String rejectReason) {
return R.status(certAuditService.audit(id, Boolean.TRUE.equals(pass), rejectReason));
}
}
@@ -33,11 +33,9 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.log.exception.ServiceException;
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.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
@@ -46,6 +44,7 @@ import org.springblade.transport.pojo.entity.CommonAddress;
import org.springblade.transport.pojo.vo.CommonAddressRemoveResultVO;
import org.springblade.transport.pojo.vo.CommonAddressVO;
import org.springblade.transport.service.ICommonAddressService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -54,7 +53,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Objects;
/**
* 常用地址 控制器
@@ -164,15 +162,9 @@ public class CommonAddressController extends BladeController {
if (Func.isNotEmpty(ids)) {
queryWrapper.in(CommonAddress::getId, Func.toLongList(ids));
}
if (Objects.equals(commonAddress.getAllDept(), 1)) {
if (!AuthUtil.isAdministrator()) {
throw new ServiceException("无权导出全部组织常用地址");
}
} else {
queryWrapper.eq(CommonAddress::getDeptId, currentDeptId());
}
if (Objects.equals(commonAddress.getAllDept(), 1) && Func.isNotEmpty(commonAddress.getDeptId())) {
queryWrapper.eq(CommonAddress::getDeptId, commonAddress.getDeptId());
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(commonAddress.getAllDept(), commonAddress.getDeptId(), "常用地址");
if (Func.isNotEmpty(deptIdList)) {
queryWrapper.eq(CommonAddress::getDeptId, deptIdList.get(0));
}
if (Func.isNotEmpty(commonAddress.getAddressName())) {
queryWrapper.like(CommonAddress::getAddressName, commonAddress.getAddressName().trim());
@@ -198,12 +190,4 @@ public class CommonAddressController extends BladeController {
return queryWrapper;
}
private Long currentDeptId() {
Long deptId = Func.firstLong(AuthUtil.getDeptId());
if (Func.isEmpty(deptId) || deptId <= 0) {
throw new ServiceException("当前用户所属组织为空,无法导出常用地址");
}
return deptId;
}
}
@@ -38,9 +38,9 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.transport.excel.CommonCargoExcel;
import org.springblade.transport.excel.CommonCargoExportExcel;
import org.springblade.transport.excel.CommonCargoImportFailureExcel;
import org.springframework.web.multipart.MultipartFile;
import org.springblade.transport.pojo.entity.CommonCargo;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
@@ -110,14 +110,41 @@ public class CommonCargoController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入常用货物", description = "传入excel")
public R importCommonCargo(MultipartFile file, HttpServletResponse response) {
List<CommonCargoImportFailureExcel> failureList = commonCargoService.importCommonCargo(ExcelUtil.read(file, CommonCargoExcel.class));
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CommonCargoImportFailureExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
commonCargoService.importCommonCargo(ExcelUtil.read(file, CommonCargoExcel.class));
} catch (ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
exportFailure(response, exception.getFailureList());
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", file, CommonCargoExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
/**
* 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
* <p>
* 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
*/
private void exportFailure(HttpServletResponse response, List<?> failureList) {
ImportFailureExcelUtil.exportFailureReasonOnly(response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CommonCargoExcel.class);
}
@GetMapping("/export-template")
@ApiOperationSupport(order = 7)
@Operation(summary = "导出模板")
@@ -38,6 +38,7 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.transport.excel.CommonRouteExportExcel;
import org.springblade.transport.excel.CommonRouteImportExcel;
import org.springblade.transport.pojo.entity.CommonRoute;
@@ -109,18 +110,34 @@ public class CommonRouteController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入常用线路", description = "传入excel")
public R importCommonRoute(MultipartFile file, HttpServletResponse response) {
List<CommonRouteImportExcel> failureList = commonRouteService.importCommonRoute(
ExcelUtil.read(file, CommonRouteImportExcel.class)
);
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
commonRouteService.importCommonRoute(ExcelUtil.read(file, CommonRouteImportExcel.class));
} catch (ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
ImportFailureExcelUtil.exportFailureReasonOnly(
response,
"常用线路导入失败明细" + DateUtil.time(),
"导入失败明细",
failureList,
exception.getFailureList(),
CommonRouteImportExcel.class
);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "常用线路导入失败明细" + DateUtil.time(), "导入失败明细", file, CommonRouteImportExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
@@ -177,8 +177,16 @@ public class ContractManageController extends BladeController {
return R.data(contractManageService.expireStats(contractManage));
}
@GetMapping("/export-contract-manage")
@GetMapping("/next-contract-no")
@ApiOperationSupport(order = 16)
@Operation(summary = "预览下一合同编号", description = "按合同负责部门编码前缀+年份+三位流水预览")
public R<String> nextContractNo(
@Parameter(description = "合同负责部门ID", required = true) @RequestParam Long organizationId) {
return R.data(contractManageService.nextContractNo(organizationId));
}
@GetMapping("/export-contract-manage")
@ApiOperationSupport(order = 17)
@Operation(summary = "导出合同管理")
public void exportContractManage(ContractManageVO contractManage, @RequestParam(required = false) String ids, HttpServletResponse response) {
List<ContractManageExcel> list = contractManageService.exportContractManage(contractManage, ids);
@@ -138,10 +138,28 @@ public class CreditScoreQuantificationController extends BladeController {
@ApiOperationSupport(order = 7)
@Operation(summary = "导入评分量化表", description = "传入excel")
public R importCreditScoreQuantification(MultipartFile file, HttpServletResponse response) {
List<CreditScoreQuantificationExcel> failureList = creditScoreQuantificationService.importCreditScoreQuantification(ExcelUtil.read(file, CreditScoreQuantificationExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CreditScoreQuantificationExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
creditScoreQuantificationService.importCreditScoreQuantification(ExcelUtil.read(file, CreditScoreQuantificationExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一分组失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), CreditScoreQuantificationExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", file, CreditScoreQuantificationExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -38,6 +38,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.secure.utils.AuthUtil;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
@@ -220,10 +221,12 @@ public class CustomerArchiveController extends BladeController {
LambdaQueryWrapper<CustomerArchive> queryWrapper = Wrappers.<CustomerArchive>lambdaQuery()
.eq(CustomerArchive::getIsDeleted, 0)
.orderByDesc(CustomerArchive::getCreateTime);
Long userId = AuthUtil.getUserId();
queryWrapper.and(wrapper -> wrapper
.exists("SELECT 1 FROM blade_user_customer_scope bucs WHERE bucs.user_id = " + userId
+ " AND bucs.customer_id = blade_customer_archive.id"));
if (!TransportBusinessSupport.isAdministrator()) {
Long userId = AuthUtil.getUserId();
queryWrapper.and(wrapper -> wrapper
.exists("SELECT 1 FROM blade_user_customer_scope bucs WHERE bucs.user_id = " + userId
+ " AND bucs.customer_id = blade_customer_archive.id"));
}
if (Func.isNotEmpty(ids)) {
queryWrapper.in(CustomerArchive::getId, Func.toLongList(ids));
}
@@ -30,12 +30,16 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
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.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.core.tool.api.R;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springblade.process.pojo.vo.MkFormBindVO;
import org.springblade.transport.pojo.vo.CustomerArchiveVO;
import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
import org.springblade.transport.service.ICustomerArchiveService;
@@ -65,15 +69,34 @@ public class CustomerArchivePublicController {
private final ICustomerArchiveService customerArchiveService;
private final CustomerArchivePublicProcessService customerArchivePublicProcessService;
private final IBusinessProcessClient businessProcessClient;
/**
* 公开详情
*/
@GetMapping("/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "公开详情", description = "传入id,无需登录")
public R<CustomerArchiveVO> detail(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.data(customerArchiveService.publicDetail(id));
@Operation(summary = "公开详情", description = "传入表单实例id,无需登录;内部反查业务主键后加载详情")
public R<CustomerArchiveVO> detail(@Parameter(description = "表单实例id", required = true) @RequestParam Long id) {
return R.data(customerArchiveService.publicDetail(resolveBizId(id)));
}
/**
* 公开页传入的 id 为 formInstanceId,反查业务主键;查不到时兼容历史(id 即 bizId)
*/
private Long resolveBizId(Long formInstanceId) {
if (formInstanceId == null) {
throw new ServiceException("表单实例id不能为空");
}
try {
FR<MkFormBindVO> result = businessProcessClient.resolveByFormInstanceId(formInstanceId);
if (result != null && result.isSuccess() && result.getData() != null && result.getData().getBizId() != null) {
return result.getData().getBizId();
}
} catch (Exception e) {
log.warn("按表单实例id解析客商业务主键失败,回退按业务主键查询 formInstanceId={}", formInstanceId, e);
}
return formInstanceId;
}
/**
@@ -0,0 +1,153 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.controller;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
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.transport.ocr.constant.BaiduOcrType;
import org.springblade.transport.ocr.service.IBaiduOcrService;
import org.springblade.transport.pojo.dto.OcrRecognizeRequest;
import org.springblade.transport.pojo.dto.PersonCertSubmitRequest;
import org.springblade.transport.pojo.dto.VehicleCertSubmitRequest;
import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
import org.springblade.transport.pojo.vo.TransportCertRecordVO;
import org.springblade.transport.service.IDriverCertService;
import org.springblade.transport.support.OcrTypeMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RestController;
import java.util.List;
/**
* 司机端认证(小程序)
* <p>
* 对外路径:{@code /api/blade-transport/driver/auth/**}
* 同时兼容未去前缀直连 {@code /blade-transport/driver/auth/**}。
* <p>
* 提交审核**不影响**该对象当前的可用性:待审内容只落认证记录,
* 审核通过前对外呈现的仍是旧的生效值。
*/
@RestController
@AllArgsConstructor
@RequestMapping({"/driver/auth", "/blade-transport/driver/auth"})
@Tag(name = "司机端认证", description = "小程序司机个人认证与车辆认证")
public class DriverAuthController extends BladeController {
private final IDriverCertService driverCertService;
private final IBaiduOcrService baiduOcrService;
// ------------------------------------------------------------ 个人认证
@PostMapping("/person")
@ApiOperationSupport(order = 1)
@Operation(summary = "提交个人认证", description = "三证(身份证+驾驶证+从业资格证)一起提交,生成待审记录")
public R<Long> submitPerson(@RequestBody PersonCertSubmitRequest request) {
return R.data(driverCertService.submitPersonCert(request));
}
@GetMapping("/person")
@ApiOperationSupport(order = 2)
@Operation(summary = "个人认证生效值", description = "返回当前生效快照的内容;从未认证通过时 data 为 null")
public R<PersonCertSubmitRequest> person() {
return R.data(driverCertService.getEffectivePersonCert());
}
// ------------------------------------------------------------ 车辆认证
@PostMapping("/vehicle")
@ApiOperationSupport(order = 3)
@Operation(summary = "提交车辆认证", description = "生成待审记录;不改变该车已生效的认证内容")
public R<Long> submitVehicle(@RequestBody VehicleCertSubmitRequest request) {
return R.data(driverCertService.submitVehicleCert(request));
}
@PutMapping("/vehicle")
@ApiOperationSupport(order = 4)
@Operation(summary = "重新认证车辆", description = "与提交同一语义,语义上用于已有车辆重新认证")
public R<Long> updateVehicle(@RequestBody VehicleCertSubmitRequest request) {
return R.data(driverCertService.submitVehicleCert(request));
}
@DeleteMapping("/vehicle/{id}")
@ApiOperationSupport(order = 5)
@Operation(summary = "删除车辆认证", description = "对车辆做逻辑删除;认证记录保留,不物理删除")
public R<Boolean> deleteVehicle(@PathVariable Long id) {
return R.status(driverCertService.deleteVehicleCert(id));
}
@GetMapping("/vehicle/effective")
@ApiOperationSupport(order = 5)
@Operation(summary = "车辆认证生效值", description = "返回该车当前生效快照的内容")
public R<VehicleCertSubmitRequest> vehicleEffective(@RequestParam Long vehicleId) {
return R.data(driverCertService.getEffectiveVehicleCert(vehicleId));
}
// ------------------------------------------------------------ 认证记录
@GetMapping("/record/list")
@ApiOperationSupport(order = 6)
@Operation(summary = "认证记录列表", description = "按提交时间倒序;只能查本人或本人绑定车辆的记录")
public R<List<TransportCertRecordVO>> recordList(@RequestParam String bizType,
@RequestParam Long bizId) {
return R.data(driverCertService.listRecords(bizType, bizId));
}
@GetMapping("/record/detail")
@ApiOperationSupport(order = 7)
@Operation(summary = "认证记录详情", description = "内容取自该次提交的完整快照")
public R<TransportCertRecordVO> recordDetail(@RequestParam Long id) {
return R.data(driverCertService.getRecord(id));
}
// ------------------------------------------------------------ OCR
@PostMapping("/ocr")
@ApiOperationSupport(order = 8)
@Operation(summary = "证件OCR识别",
description = "按小程序既有契约收 {imageUrl, type, side?},转调百度OCR。"
+ "type 用小程序命名:idcard-身份证,driver-驾驶证,vehicle-行驶证,road_transport-道路运输证")
public R<BaiduOcrResultVO> ocr(@RequestBody OcrRecognizeRequest request) {
// 两侧对「驾驶证 / 行驶证」的用词正好相反,故必须经显式映射,不能把入参直接透传下游
BaiduOcrType ocrType = OcrTypeMapping.toBaiduType(request.getType());
// 未指定时传 null,不要在这里补 front:道路运输证没有正副面参数,
// 补了会变成「传了 side 但该证件不支持」,让这条链路恒失败。
// 「默认取正面」由知晓证件类型的一侧(OCR 服务)决定。
String side = OcrTypeMapping.normalizeSide(request.getSide());
return R.data(baiduOcrService.recognizeUrl(ocrType, side, request.getImageUrl()));
}
}
@@ -38,6 +38,7 @@ import org.springblade.transport.pojo.vo.DriverNodePunchVO;
import org.springblade.transport.pojo.vo.DriverWaybillCardVO;
import org.springblade.transport.pojo.vo.DriverWaybillPreviewVO;
import org.springblade.transport.pojo.vo.DriverWaybillTabCountsVO;
import org.springblade.transport.pojo.vo.WaybillTrackVO;
import org.springblade.transport.service.IDriverWaybillService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -64,14 +65,14 @@ public class DriverWaybillController extends BladeController {
@GetMapping("/current-task")
@ApiOperationSupport(order = 1)
@Operation(summary = "首页:当前运输中任务", description = "当前司机绑定车牌下 businessStatus=running 的最新一条运单")
@Operation(summary = "首页:当前运输中任务", description = "当前用户手机号匹配运单 driverPhone 且 businessStatus=running 的最新一条运单")
public R<DriverWaybillCardVO> currentTask() {
return R.data(driverWaybillService.currentTask());
}
@GetMapping("/pending-preview")
@ApiOperationSupport(order = 2)
@Operation(summary = "首页:待接运单预览", description = "当前司机绑定车牌下 businessStatus=pending 的预览列表与总数")
@Operation(summary = "首页:待接运单预览", description = "当前用户手机号匹配运单 driverPhone 且 businessStatus=pending 的预览列表与总数")
public R<DriverWaybillPreviewVO> pendingPreview(
@Parameter(description = "预览条数,默认 2") @RequestParam(required = false) Integer size) {
return R.data(driverWaybillService.pendingPreview(size));
@@ -79,14 +80,14 @@ public class DriverWaybillController extends BladeController {
@GetMapping("/counts")
@ApiOperationSupport(order = 3)
@Operation(summary = "运单 Tab 统计", description = "仅统计当前司机绑定车牌对应的运单:全部 / 待接单 / 进行中 / 已完成")
@Operation(summary = "运单 Tab 统计", description = "仅统计当前用户手机号匹配运单 driverPhone 的运单:全部 / 待接单 / 进行中 / 已完成")
public R<DriverWaybillTabCountsVO> counts() {
return R.data(driverWaybillService.tabCounts());
}
@GetMapping("/page")
@ApiOperationSupport(order = 4)
@Operation(summary = "运单分页列表", description = "仅返回当前司机绑定车牌(driving_vehicle)匹配运单 vehicleNo/trailerVehicleNo 的数据;status:空=全部,0待接单,1运输中,2已完成")
@Operation(summary = "运单分页列表", description = "仅返回运单 driverPhone 等于当前用户手机号的数据;status:空=全部,0待接单,1运输中,2已完成")
public R<IPage<DriverWaybillCardVO>> page(
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
@@ -104,6 +105,13 @@ public class DriverWaybillController extends BladeController {
return R.data(driverWaybillService.detail(id));
}
@PostMapping("/track")
@ApiOperationSupport(order = 11)
@Operation(summary = "司机端行驶轨迹", description = "按当前用户手机号校验归属后,根据打卡记录/计划时间自动推导起止日期并查询 LBS 轨迹(WGS-84)")
public R<WaybillTrackVO> track(@Parameter(description = "运单ID", required = true) @RequestParam Long id) {
return R.data(driverWaybillService.track(id));
}
@PostMapping("/accept")
@ApiOperationSupport(order = 6)
@Operation(summary = "司机确认接单", description = "过程配置接单为「是」时,司机确认接单后运单进入进行中")
@@ -90,10 +90,28 @@ public class EquipmentLedgerController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入设备台账")
public R importEquipmentLedger(MultipartFile file, HttpServletResponse response) {
List<EquipmentLedgerExcel> failureList = equipmentLedgerService.importEquipmentLedger(ExcelUtil.read(file, EquipmentLedgerExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", failureList, EquipmentLedgerExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
equipmentLedgerService.importEquipmentLedger(ExcelUtil.read(file, EquipmentLedgerExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), EquipmentLedgerExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", file, EquipmentLedgerExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -94,10 +94,28 @@ public class EtcRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入ETC记录", description = "传入excel")
public R importEtcRecord(MultipartFile file, HttpServletResponse response) {
List<EtcRecordExcel> failureList = etcRecordService.importEtcRecord(ExcelUtil.read(file, EtcRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, EtcRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
etcRecordService.importEtcRecord(ExcelUtil.read(file, EtcRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), EtcRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", file, EtcRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -30,11 +30,14 @@ 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.ExceptionDisposalFollowRequest;
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
import org.springblade.transport.pojo.entity.ExceptionDisposal;
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
import org.springblade.transport.service.IExceptionDisposalService;
import org.springblade.transport.support.ExceptionDisposalPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -102,4 +105,13 @@ public class ExceptionDisposalController extends BladeController {
return R.success("批量完成成功");
}
@PostMapping("/update")
@PreAuth(ExceptionDisposalPermissions.EDIT)
@ApiOperationSupport(order = 7)
@Operation(summary = "编辑异常处置", description = "运管员订正上报内容;不改动处置状态")
public R update(@RequestBody ExceptionDisposalUpdateRequest request) {
exceptionDisposalService.update(request);
return R.success("编辑成功");
}
}
@@ -22,6 +22,7 @@ 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;
@@ -30,12 +31,13 @@ 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.KingdeeInvoicePoolPageVO;
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;
@@ -62,6 +64,7 @@ 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;
@@ -194,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)
@@ -216,6 +231,13 @@ public class FormalSettlementController extends BladeController {
@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 = "正式结算货物费用快照")
@@ -262,8 +284,8 @@ public class FormalSettlementController extends BladeController {
@GetMapping("/invoice-pool")
@ApiOperationSupport(order = 21)
@Operation(summary = "金蝶票池实时分页查询(认领查询)")
public R<KingdeeInvoicePoolPageVO> invoicePool(KingdeeInvoicePoolQueryDTO query) {
@Operation(summary = "金蝶票据池镜像分页查询(认领查询)")
public R<IPage<KingdeeInvoicePoolVO>> invoicePool(KingdeeInvoicePoolQueryDTO query) {
return R.data(invoiceReceiptService.invoicePoolPage(query));
}
}
@@ -129,10 +129,28 @@ public class InsuranceRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入保险记录", description = "传入excel")
public R importInsuranceRecord(MultipartFile file, HttpServletResponse response) {
List<InsuranceRecordExcel> failureList = insuranceRecordService.importInsuranceRecord(ExcelUtil.read(file, InsuranceRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, InsuranceRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
insuranceRecordService.importInsuranceRecord(ExcelUtil.read(file, InsuranceRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), InsuranceRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", file, InsuranceRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -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("审批通过");
}
@@ -29,17 +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.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
import org.springblade.transport.pojo.vo.KingdeeInvoicePoolPageVO;
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;
@@ -81,11 +83,26 @@ public class InvoiceReceiptController extends BladeController {
@GetMapping("/invoice-pool")
@ApiOperationSupport(order = 3)
@Operation(summary = "金蝶票池实时分页查询(含认领/登记标记)")
public R<KingdeeInvoicePoolPageVO> invoicePool(KingdeeInvoicePoolQueryDTO query) {
@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")
@ApiOperationSupport(order = 4)
@Operation(summary = "可关联的应付正式结算单")
@@ -130,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("审批通过");
}
@@ -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);
}
}
}
@@ -128,10 +128,28 @@ public class MaintenancePlanController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入保养记录", description = "传入excel")
public R importMaintenancePlan(MultipartFile file, HttpServletResponse response) {
List<MaintenancePlanExcel> failureList = maintenancePlanService.importMaintenancePlan(ExcelUtil.read(file, MaintenancePlanExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保养记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MaintenancePlanExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
maintenancePlanService.importMaintenancePlan(ExcelUtil.read(file, MaintenancePlanExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保养计划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MaintenancePlanExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "保养计划导入失败明细" + DateUtil.time(), "导入失败明细", file, MaintenancePlanExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -128,10 +128,28 @@ public class MaintenanceRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入维修记录", description = "传入excel")
public R importMaintenanceRecord(MultipartFile file, HttpServletResponse response) {
List<MaintenanceRecordExcel> failureList = maintenanceRecordService.importMaintenanceRecord(ExcelUtil.read(file, MaintenanceRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MaintenanceRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
maintenanceRecordService.importMaintenanceRecord(ExcelUtil.read(file, MaintenanceRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MaintenanceRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", file, MaintenanceRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -30,6 +30,7 @@ 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.transport.pojo.dto.WaybillPunchRecordSaveRequest;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.AdminDriverOptionVO;
import org.springblade.transport.pojo.vo.AdminHomeStatsVO;
@@ -37,7 +38,9 @@ import org.springblade.transport.pojo.vo.AdminHomeVO;
import org.springblade.transport.pojo.vo.AdminVehicleOptionVO;
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
import org.springblade.transport.service.IManageWaybillService;
import org.springblade.transport.service.IWaybillService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -62,6 +65,7 @@ import java.util.List;
public class ManageWaybillController extends BladeController {
private final IManageWaybillService manageWaybillService;
private final IWaybillService waybillService;
@GetMapping("/stats")
@ApiOperationSupport(order = 1)
@@ -72,7 +76,7 @@ public class ManageWaybillController extends BladeController {
@GetMapping("/home")
@ApiOperationSupport(order = 2)
@Operation(summary = "首页聚合", description = "统计 + 异常/风险角标 + 待处理事项(异常处置≠已完成)+ 当前用户名")
@Operation(summary = "首页聚合", description = "统计 + 异常/风险/重新派单角标 + 待处理事项(异常处置≠已完成,或司机已拒单待重新派单)+ 当前用户名")
public R<AdminHomeVO> home() {
return R.data(manageWaybillService.home());
}
@@ -140,4 +144,19 @@ public class ManageWaybillController extends BladeController {
return R.data(manageWaybillService.searchVehicles(keyword));
}
@GetMapping("/punch-records")
@ApiOperationSupport(order = 9)
@Operation(summary = "打卡记录", description = "小程序调度端:仅需登录态;按过程配置返回节点/在途打卡流水")
public R<WaybillPunchRecordsVO> punchRecords(
@Parameter(description = "运单ID", required = true) @RequestParam Long waybillId) {
return R.data(waybillService.listPunchRecords(waybillId));
}
@PostMapping("/punch-record/save")
@ApiOperationSupport(order = 10)
@Operation(summary = "编辑打卡记录", description = "小程序调度端:补录/订正/取消打卡,仅需登录态")
public R savePunchRecord(@RequestBody WaybillPunchRecordSaveRequest request) {
return R.status(waybillService.savePunchRecord(request));
}
}
@@ -116,10 +116,28 @@ public class MileageRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入里程记录", description = "传入excel")
public R importMileageRecord(MultipartFile file, HttpServletResponse response) {
List<MileageRecordExcel> failureList = mileageRecordService.importMileageRecord(ExcelUtil.read(file, MileageRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MileageRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
mileageRecordService.importMileageRecord(ExcelUtil.read(file, MileageRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MileageRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", file, MileageRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -7,10 +7,14 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.core.tool.api.R;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springblade.process.pojo.vo.MkFormBindVO;
import org.springblade.transport.mk.MkProcessMessageService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -35,13 +39,33 @@ import java.util.Map;
public class MkProcessPublicController {
private final MkProcessMessageService mkProcessMessageService;
private final IBusinessProcessClient businessProcessClient;
@GetMapping("/{bizType}/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "公开详情", description = "无需登录")
@Operation(summary = "公开详情", description = "无需登录;id 为 MK 表单实例id,内部反查业务主键后加载详情")
public R detail(@PathVariable String bizType,
@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.data(mkProcessMessageService.handler(bizType).publicDetail(id));
@Parameter(description = "表单实例id", required = true) @RequestParam Long id) {
Long bizId = resolveBizId(id);
return R.data(mkProcessMessageService.handler(bizType).publicDetail(bizId));
}
/**
* 公开页传入的 id 为 formInstanceId,反查业务主键;查不到时兼容历史(id 即 bizId)
*/
private Long resolveBizId(Long formInstanceId) {
if (formInstanceId == null) {
throw new ServiceException("表单实例id不能为空");
}
try {
FR<MkFormBindVO> result = businessProcessClient.resolveByFormInstanceId(formInstanceId);
if (result != null && result.isSuccess() && result.getData() != null && result.getData().getBizId() != null) {
return result.getData().getBizId();
}
} catch (Exception e) {
log.warn("按表单实例id解析业务主键失败,回退按业务主键查询 formInstanceId={}", formInstanceId, e);
}
return formInstanceId;
}
@PostMapping("/{bizType}/process-message")
@@ -94,10 +94,28 @@ public class OilElectricRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入油电记录", description = "传入excel")
public R importOilElectricRecord(MultipartFile file, HttpServletResponse response) {
List<OilElectricRecordExcel> failureList = oilElectricRecordService.importOilElectricRecord(ExcelUtil.read(file, OilElectricRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, OilElectricRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
oilElectricRecordService.importOilElectricRecord(ExcelUtil.read(file, OilElectricRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), OilElectricRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", file, OilElectricRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -94,10 +94,28 @@ public class OtherExpenseRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入其他费用记录", description = "传入excel")
public R importOtherExpenseRecord(MultipartFile file, HttpServletResponse response) {
List<OtherExpenseRecordExcel> failureList = otherExpenseRecordService.importOtherExpenseRecord(ExcelUtil.read(file, OtherExpenseRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, OtherExpenseRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
otherExpenseRecordService.importOtherExpenseRecord(ExcelUtil.read(file, OtherExpenseRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), OtherExpenseRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", file, OtherExpenseRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -29,6 +29,7 @@ import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO;
@@ -81,7 +82,19 @@ public class PaymentApplicationController extends BladeController {
@PostMapping("/approve")
@ApiOperationSupport(order = 7)
@Operation(summary = "审批通过付款申请")
public R approve(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.approve(request); return R.success("审批通过"); }
public R approve(@RequestBody PaymentApplicationStatusRequest request) {
paymentApplicationService.approve(request);
return approveResponse();
}
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
private R approveResponse() {
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
if (syncFailure != null) {
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
}
return R.success("审批通过");
}
@PostMapping("/return")
@ApiOperationSupport(order = 8)
@Operation(summary = "驳回付款申请")
@@ -51,7 +51,9 @@ import org.springblade.transport.pojo.entity.VoucherManage;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.ProcessConfigVO;
import org.springblade.transport.pojo.vo.VoucherFolderVO;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IVoucherManageService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
@@ -80,6 +82,7 @@ import java.util.concurrent.TimeUnit;
public class ProcessConfigController extends BladeController {
private final IProcessConfigService processConfigService;
private final IVoucherManageService voucherManageService;
private final WaybillMapper waybillMapper;
private final VoucherFileMapper voucherFileMapper;
private final VoucherImageMapper voucherImageMapper;
@@ -88,11 +91,13 @@ public class ProcessConfigController extends BladeController {
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
private String minioBucketName;
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
public ProcessConfigController(IProcessConfigService processConfigService, IVoucherManageService voucherManageService,
WaybillMapper waybillMapper,
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
VoucherManageMapper voucherManageMapper,
ObjectProvider<MinioClient> minioClientProvider) {
this.processConfigService = processConfigService;
this.voucherManageService = voucherManageService;
this.waybillMapper = waybillMapper;
this.voucherFileMapper = voucherFileMapper;
this.voucherImageMapper = voucherImageMapper;
@@ -128,6 +133,23 @@ public class ProcessConfigController extends BladeController {
return R.data(detail);
}
@GetMapping("/voucher-folders")
@ApiOperationSupport(order = 2)
@Operation(summary = "查询运单执行凭证文件夹")
public R<List<VoucherFolderVO>> voucherFolders(
@Parameter(description = "运单主键", required = true) @RequestParam Long waybillId) {
return R.data(voucherManageService.listWaybillFolders(waybillId));
}
@GetMapping("/voucher-folder-detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "查询运单执行凭证文件夹详情")
public R<VoucherFolderVO> voucherFolderDetail(
@Parameter(description = "凭证批次主键", required = true) @RequestParam Long voucherId,
@Parameter(description = "车牌号", required = true) @RequestParam String plateNo) {
return R.data(voucherManageService.folderDetail(voucherId, plateNo));
}
@GetMapping("/voucher-images")
@ApiOperationSupport(order = 2)
@Operation(summary = "查询运单已关联凭证图片")
@@ -84,8 +84,9 @@ 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")
@@ -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.pojo.dto.ReceiptClaimPushResult;
import org.springblade.transport.pojo.dto.ReceiptClaimRequest;
import org.springblade.transport.pojo.vo.ReceiptFlowVO;
import org.springblade.transport.service.IReceiptFlowService;
@@ -98,4 +99,12 @@ public class ReceiptFlowController extends BladeController {
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());
}
}
@@ -128,10 +128,28 @@ public class TireReplacementRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入换胎记录", description = "传入excel")
public R importTireReplacementRecord(MultipartFile file, HttpServletResponse response) {
List<TireReplacementRecordExcel> failureList = tireReplacementRecordService.importTireReplacementRecord(ExcelUtil.read(file, TireReplacementRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TireReplacementRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
tireReplacementRecordService.importTireReplacementRecord(ExcelUtil.read(file, TireReplacementRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TireReplacementRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", file, TireReplacementRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -113,10 +113,28 @@ public class TransportChangeRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入变更记录", description = "传入excel")
public R importTransportChangeRecord(MultipartFile file, HttpServletResponse response) {
List<TransportChangeRecordExcel> failureList = transportChangeRecordService.importTransportChangeRecord(ExcelUtil.read(file, TransportChangeRecordExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "变更记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TransportChangeRecordExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
transportChangeRecordService.importTransportChangeRecord(ExcelUtil.read(file, TransportChangeRecordExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "运输变更记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TransportChangeRecordExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "运输变更记录导入失败明细" + DateUtil.time(), "导入失败明细", file, TransportChangeRecordExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -147,11 +147,29 @@ public class TransportPlanController extends BladeController {
public R importTransportPlan(MultipartFile file, @RequestParam Long projectId, @RequestParam String projectName,
@RequestParam Long contractId, @RequestParam String contractName, @RequestParam String customerName,
HttpServletResponse response) {
List<TransportPlanImportExcel> failureList = transportPlanService.importTransportPlan(
ExcelUtil.read(file, TransportPlanImportExcel.class), projectId, projectName, contractId, contractName, customerName);
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TransportPlanImportExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
transportPlanService.importTransportPlan(
ExcelUtil.read(file, TransportPlanImportExcel.class), projectId, projectName, contractId, contractName, customerName);
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
ImportFailureExcelUtil.export(response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TransportPlanImportExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", file, TransportPlanImportExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
@@ -19,6 +19,7 @@ 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.core.tool.utils.Func;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.transport.excel.CargoReconciliationExcel;
import org.springblade.transport.excel.CargoReconciliationFeeReader;
@@ -126,13 +127,32 @@ public class TransportReconciliationController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入整车总额外部账单")
public R importVehicle(@RequestParam Long id, MultipartFile file, HttpServletResponse response) {
List<VehicleReconciliationExcel> rows = ExcelUtil.read(file, VehicleReconciliationExcel.class);
List<Map<String, BigDecimal>> feeItems = VehicleReconciliationFeeReader.read(file);
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
List<VehicleReconciliationFailureExcel> failures = reconciliationService.importVehicles(id, rows);
if (!failures.isEmpty()) {
ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, VehicleReconciliationFailureExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
List<VehicleReconciliationExcel> rows = ExcelUtil.read(file, VehicleReconciliationExcel.class);
List<Map<String, BigDecimal>> feeItems = VehicleReconciliationFeeReader.read(file);
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
reconciliationService.importVehicles(id, rows);
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 按原表真实表头导出全量列(含动态费用),修正后可直接重新导入
ImportFailureExcelUtil.exportOriginalWithFailures(response, "运输对账导入失败明细" + DateUtil.time(),
"导入失败明细", file, exception.getFailureList(), VehicleReconciliationExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", file, VehicleReconciliationExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
@@ -141,13 +161,32 @@ public class TransportReconciliationController extends BladeController {
@ApiOperationSupport(order = 7)
@Operation(summary = "导入货物明细外部账单")
public R importCargo(@RequestParam Long id, MultipartFile file, HttpServletResponse response) {
List<CargoReconciliationExcel> rows = ExcelUtil.read(file, CargoReconciliationExcel.class);
List<Map<String, BigDecimal>> feeItems = CargoReconciliationFeeReader.read(file);
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
List<CargoReconciliationFailureExcel> failures = reconciliationService.importCargoes(id, rows);
if (!failures.isEmpty()) {
ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, CargoReconciliationFailureExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
List<CargoReconciliationExcel> rows = ExcelUtil.read(file, CargoReconciliationExcel.class);
List<Map<String, BigDecimal>> feeItems = CargoReconciliationFeeReader.read(file);
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
reconciliationService.importCargoes(id, rows);
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 按原表真实表头导出全量列(含动态费用),修正后可直接重新导入
ImportFailureExcelUtil.exportOriginalWithFailures(response, "运输对账导入失败明细" + DateUtil.time(),
"导入失败明细", file, exception.getFailureList(), CargoReconciliationExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", file, CargoReconciliationExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
@@ -35,6 +35,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
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;
@@ -67,6 +68,7 @@ import java.util.List;
*/
@RestController
@AllArgsConstructor
@Slf4j
@PreAuth(menu = "transport_vehicle")
@RequestMapping("/transport-vehicle")
@Tag(name = "车辆管理", description = "车辆管理")
@@ -132,11 +134,27 @@ public class TransportVehicleController extends BladeController {
return R.status(transportVehicleService.changeStatus(id, status));
}
/**
* 审核车辆认证(已弃用)
*
* @deprecated 本接口作用于**车辆行**并原地覆盖认证状态,在「生效值 / 待审值分离」
* 之后语义已不成立:一次审核应对应**一条认证记录**,且通过才建立生效快照、
* 驳回不得影响既得认证。请改用
* {@code POST /blade-transport/certification-audit/audit}。
* <p>
* 保留而非删除,是因为它可能仍被小程序历史版本或其它系统调用;
* 调用时记录告警日志。它只写展示缓存,**不再影响可用性判定**
* (可用性只看 {@code effective_cert_record_id})。
*/
@Deprecated
@PostMapping("/audit-certification")
@ApiOperationSupport(order = 6)
@Operation(summary = "审核车辆认证")
@Operation(summary = "审核车辆认证(已弃用)",
description = "已弃用:作用于车辆行且原地覆盖。请改用 /blade-transport/certification-audit/audit")
public R auditCertification(@RequestParam Long id, @RequestParam Integer certificationStatus,
@RequestParam(required = false) String rejectReason) {
log.warn("[认证审核] 调用了已弃用接口 audit-certification,vehicleId={}, status={};"
+ "请改用 /blade-transport/certification-audit/audit", id, certificationStatus);
return R.status(transportVehicleService.auditCertification(id, certificationStatus, rejectReason));
}
@@ -127,10 +127,28 @@ public class ViolationRecordController extends BladeController {
@ApiOperationSupport(order = 5)
@Operation(summary = "导入违章记录", description = "传入excel")
public R importViolationRecord(MultipartFile file, HttpServletResponse response) {
List<ViolationRecordImportExcel> failureList = violationRecordService.importViolationRecord(ExcelUtil.read(file, ViolationRecordImportExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, ViolationRecordImportExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
violationRecordService.importViolationRecord(ExcelUtil.read(file, ViolationRecordImportExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), ViolationRecordImportExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", file, ViolationRecordImportExcel.class, readException)) {
return null;
}
throw readException;
}
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;
@@ -23,6 +23,7 @@
package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import cn.idev.excel.write.handler.SheetWriteHandler;
import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
@@ -34,6 +35,7 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.apache.poi.ss.usermodel.CellStyle;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.mp.support.Condition;
@@ -42,6 +44,7 @@ import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.excel.WaybillExcel;
import org.springblade.transport.excel.WaybillImportBatchExcel;
import org.springblade.transport.pojo.entity.ContractManage;
@@ -49,15 +52,18 @@ import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.TransportPlan;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.entity.WaybillImportBatch;
import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.LoadingManageVO;
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
import org.springblade.transport.pojo.vo.WaybillLocateVO;
import org.springblade.transport.pojo.vo.WaybillTrackVO;
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
import org.springblade.transport.pojo.vo.WaybillVO;
import org.springblade.transport.pojo.dto.WaybillMileageRequest;
import org.springblade.transport.pojo.dto.WaybillPunchRecordSaveRequest;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IProjectApplyService;
@@ -74,8 +80,11 @@ import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* 运单管理 控制器
@@ -111,6 +120,13 @@ public class WaybillController extends BladeController {
return R.data(waybillService.listPunchRecords(waybillId));
}
@PostMapping("/punch-record/save")
@ApiOperationSupport(order = 2)
@Operation(summary = "编辑打卡记录", description = "按过程配置补录/修正节点或在途打卡;punched=false 表示取消打卡,清除该节点打卡流水")
public R savePunchRecord(@RequestBody WaybillPunchRecordSaveRequest request) {
return R.status(waybillService.savePunchRecord(request));
}
@PostMapping("/locate")
@ApiOperationSupport(order = 1)
@Operation(summary = "车辆实时定位", description = "按运单绑定车牌调用 LBS_LOCATE")
@@ -149,10 +165,22 @@ public class WaybillController extends BladeController {
.map(contract -> option(contract.getId(), "contractName", contract.getContractName(), "projectId", contract.getProjectId()))
.toList());
options.put("carriers", customerArchiveService.list().stream()
.filter(customer -> "承运商".equals(customer.getCustomerType()) && Integer.valueOf(1).equals(customer.getStatus()))
.filter(customer -> isCarrierCustomer(customer) && Integer.valueOf(1).equals(customer.getStatus()))
.map(customer -> option(customer.getId(), "name", customer.getFullName(), null, null))
.toList());
options.put("creators", new ArrayList<>());
// 创建人选项取已有导入批次的创建人,保证筛选项与列表数据可对应。
Set<Long> creatorIds = new LinkedHashSet<>();
waybillImportBatchService.list(Wrappers.<WaybillImportBatch>lambdaQuery()
.select(WaybillImportBatch::getCreateUser)
.eq(WaybillImportBatch::getIsDeleted, 0)
.isNotNull(WaybillImportBatch::getCreateUser))
.stream()
.map(WaybillImportBatch::getCreateUser)
.filter(Objects::nonNull)
.forEach(creatorIds::add);
options.put("creators", creatorIds.stream()
.map(userId -> option(userId, "name", UserCache.getUserRealName(userId), null, null))
.toList());
options.put("plans", transportPlanService.list().stream()
.map(plan -> option(plan.getId(), "planName", plan.getPlanName(), "projectId", plan.getProjectId()))
.toList());
@@ -191,15 +219,22 @@ public class WaybillController extends BladeController {
@PostMapping("/import-batch/validate")
@ApiOperationSupport(order = 6)
@Operation(summary = "校验运单批量导入数据")
public void validateImportBatch(@RequestBody WaybillImportBatchRequest request, HttpServletResponse response) {
waybillImportBatchService.validate(request, response);
public R<WaybillImportValidateResultVO> validateImportBatch(@RequestBody WaybillImportBatchRequest request) {
return R.data(waybillImportBatchService.validate(request));
}
@PostMapping("/import-batch/export-failures")
@ApiOperationSupport(order = 6)
@Operation(summary = "导出运单批量导入失败明细")
public void exportImportFailures(@RequestBody WaybillImportBatchRequest request, HttpServletResponse response) {
waybillImportBatchService.exportFailures(request, response);
}
@PostMapping("/import-batch/confirm")
@ApiOperationSupport(order = 7)
@Operation(summary = "确认运单批量导入")
public void confirmImportBatch(@RequestBody WaybillImportBatchRequest request, HttpServletResponse response) {
waybillImportBatchService.confirm(request, response);
public R<WaybillImportBatchVO> confirmImportBatch(@RequestBody WaybillImportBatchRequest request) {
return R.data(waybillImportBatchService.confirm(request));
}
@PostMapping("/import-batch/remove")
@@ -242,10 +277,28 @@ public class WaybillController extends BladeController {
@ApiOperationSupport(order = 13)
@Operation(summary = "导入运单管理", description = "传入excel")
public R importWaybill(MultipartFile file, HttpServletResponse response) {
List<WaybillExcel> failureList = waybillService.importWaybill(ExcelUtil.read(file, WaybillExcel.class));
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(response, "运单管理导入失败明细" + DateUtil.time(), "导入失败明细", failureList, WaybillExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
waybillService.importWaybill(ExcelUtil.read(file, WaybillExcel.class));
} catch (ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
ImportFailureExcelUtil.exportFailureReasonOnly(response, "运单管理导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), WaybillExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "运单管理导入失败明细" + DateUtil.time(), "导入失败明细", file, WaybillExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
@@ -337,6 +390,12 @@ public class WaybillController extends BladeController {
return option;
}
/** 客商类型支持多选(逗号分隔),包含「承运商」即视为承运商。 */
private boolean isCarrierCustomer(CustomerArchive customer) {
String type = Func.toStr(customer.getCustomerType()).replace(",", ",");
return Func.toStrList(type).stream().anyMatch("承运商"::equals);
}
private static final class TextColumnStyleHandler implements SheetWriteHandler {
private final int[] columnIndexes;
@@ -0,0 +1,44 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
* Copyright of this software remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.event;
/**
* 单据审批通过事件:审批事务提交后由 {@code KingdeeApprovalSyncListener} 消费,
* 自动推送对应单据至金蝶(星瀚),省掉页面手动点「同步金蝶」。
* <p>
* bizType 复用 MkProcessBizHandlers 既有取值;只在审批状态真正变为 approved 的那次发布,
* 重复回调被 approve 状态校验拒绝,天然不重复推送。
*
* @author Chill
*/
public record KingdeeBillApprovedEvent(String bizType, Long id) {
/** 正式结算单(应付 A102-2 / 应收 A302-2) */
public static final String BIZ_FORMAL_SETTLEMENT = "formal-settlement";
/** 付款申请(A103-1 正式 / A103-3 预付) */
public static final String BIZ_PAYMENT_APPLICATION = "payment-application";
/** 开票申请(A303-1) */
public static final String BIZ_INVOICE_APPLICATION = "invoice-application";
/** 收票登记(A303-2 + A302-6) */
public static final String BIZ_INVOICE_RECEIPT = "invoice-receipt";
}
@@ -81,6 +81,9 @@ public class AccidentRecordExportExcel implements Serializable {
@ExcelProperty("事故原因及损坏情况")
private String accidentReasonDamage;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -81,6 +81,9 @@ public class AnnualInspectionRecordExportExcel implements Serializable {
@ExcelProperty("评定(复核)单位")
private String assessmentUnit;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -13,6 +13,7 @@ import cn.idev.excel.FastExcel;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import cn.idev.excel.metadata.data.ReadCellData;
import org.springblade.common.excel.ImportCellReadException;
import org.springblade.core.log.exception.ServiceException;
import org.springframework.web.multipart.MultipartFile;
@@ -60,10 +61,11 @@ public final class CargoReconciliationFeeReader {
@Override
public void invoke(Map<Integer, ReadCellData<?>> row, AnalysisContext context) {
Map<String, BigDecimal> values = new LinkedHashMap<>();
Integer excelRowIndex = context.readRowHolder() == null ? null : context.readRowHolder().getRowIndex();
if (freightColumn >= 0 && settlementColumn > freightColumn) {
for (int column = freightColumn + 1; column < settlementColumn; column++) {
String name = headers.get(column);
if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column)));
if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column), name.trim(), excelRowIndex));
}
}
feeItems.add(values);
@@ -80,7 +82,7 @@ public final class CargoReconciliationFeeReader {
.findFirst().orElse(-1);
}
private BigDecimal decimal(ReadCellData<?> cellData) {
private BigDecimal decimal(ReadCellData<?> cellData, String columnName, Integer excelRowIndex) {
if (cellData == null) return BigDecimal.ZERO.setScale(2);
Object value = cellData.getData();
if (value == null) {
@@ -95,7 +97,10 @@ public final class CargoReconciliationFeeReader {
try {
return new BigDecimal(String.valueOf(value).trim()).setScale(2);
} catch (NumberFormatException exception) {
return BigDecimal.ZERO.setScale(2);
// 这里绝不能静默返回 0:费用列会进 feeItemsJson 参与结算金额与自动匹配,
// 把「一百元」这类写法悄悄变成 0.00 等于往库里写脏数据。
// 改为抛带位置信息的异常,由 Controller 的读取阶段兜底导出原表全量明细。
throw new ImportCellReadException(excelRowIndex, columnName, String.valueOf(value).trim());
}
}
@@ -88,6 +88,9 @@ public class CommonAddressExportExcel implements Serializable {
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@@ -73,7 +73,7 @@ public class CommonCargoExcel implements Serializable {
@ExcelProperty("规格")
private String specification;
@ExcelProperty("型号")
@ExcelProperty("*型号")
private String model;
@ExcelProperty("单价")
@@ -93,6 +93,9 @@ public class CommonCargoExportExcel implements Serializable {
@ExcelProperty("组织")
private String deptName;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@@ -56,7 +56,7 @@ public class ContractManageExcel implements Serializable {
private String contractName;
@ExcelProperty("所属项目")
private String projectName;
@ExcelProperty("所属组织")
@ExcelProperty("合同负责部门")
private String organizationName;
@ExcelProperty("合同类别")
private String contractCategory;
@@ -76,6 +76,10 @@ public class ContractManageExcel implements Serializable {
private LocalDate temporaryEndDate;
@ExcelProperty("经办人")
private String handlerUserName;
@ExcelProperty("签订日期")
private LocalDate signDate;
@ExcelProperty("申请日期")
private LocalDate applyDate;
@ExcelProperty("合同阶段")
private String contractStage;
@ExcelProperty("审核状态")
@@ -105,7 +109,9 @@ public class ContractManageExcel implements Serializable {
private String currentNode;
@ExcelProperty("当前处理人")
private String currentProcessor;
@ExcelProperty("备注")
@ExcelProperty("合同主要内容")
private String mainContent;
@ExcelProperty("其他事项说明")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@@ -93,6 +93,9 @@ public class CustomerArchiveExcel implements Serializable {
@ExcelProperty("法人/负责人")
private String legalPerson;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
private Date createTime;
@@ -35,6 +35,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Date;
/**
* 司机管理 Excel
@@ -128,4 +129,10 @@ public class DriverExcel implements Serializable {
@ExcelProperty("所属组织 *")
private String organizationName;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
private Date createTime;
}
@@ -7,10 +7,12 @@ package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 设备台账 Excel
@@ -29,5 +31,7 @@ public class EquipmentLedgerExcel implements Serializable {
@ExcelProperty("规格型号") private String specificationModel;
@ExcelProperty(value = "出厂日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) private String factoryDate;
@ExcelProperty("备注") private String remark;
@ExcelProperty("创建人") private String createUserName;
@ExcelProperty("创建时间") @DateTimeFormat("yyyy-MM-dd HH:mm:ss") private Date createTime;
@ExcelIgnore private String errorMessage;
}
@@ -49,6 +49,9 @@ public class EtcRecordExportExcel implements Serializable {
@ExcelProperty("出口站")
private String exitStation;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -81,6 +81,9 @@ public class InsuranceRecordExportExcel implements Serializable {
@ExcelProperty("开票日期")
private LocalDate invoiceDate;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -34,7 +34,7 @@ public class LoadingManageExcel implements Serializable {
private String loadingNo;
@ExcelProperty("配载子单号")
private String loadingSubNos;
@ExcelProperty("车牌号/航班号/船号/班列号")
@ExcelProperty("车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("司机")
private String driverName;
@@ -54,6 +54,8 @@ public class LoadingManageExcel implements Serializable {
private String transportType;
@ExcelProperty("数据来源")
private String dataSource;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -24,14 +24,17 @@ public class MaintenancePlanExportExcel extends MaintenancePlanExcel {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "创建时间", index = 12)
@ExcelProperty(value = "创建人", index = 12)
private String createUserName;
@ExcelProperty(value = "创建时间", index = 13)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ExcelProperty(value = "更新人", index = 13)
@ExcelProperty(value = "更新人", index = 14)
private String updateUserName;
@ExcelProperty(value = "更新时间", index = 14)
@ExcelProperty(value = "更新时间", index = 15)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -96,6 +96,9 @@ public class MaintenanceRecordExcel implements Serializable {
@NumberFormat("0.00")
private BigDecimal mileage;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -59,7 +59,7 @@ public class MasterOrderWaybillExcel {
private String carrierName;
@ExcelProperty("司机")
private String driverName;
@ExcelProperty("车牌号/航班号/船号/班列号")
@ExcelProperty("车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("开始日期")
private LocalDate startDate;
@@ -69,6 +69,10 @@ public class MasterOrderWaybillExcel {
private String businessStatus;
@ExcelProperty("备注")
private String remark;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("更新时间")
private Date updateTime;
@ExcelProperty("创建时间")
private Date createTime;
}
@@ -70,6 +70,9 @@ public class MileageRecordExportExcel implements Serializable {
@NumberFormat("0.00")
private BigDecimal totalMileage;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -60,6 +60,9 @@ public class OilElectricRecordExportExcel implements Serializable {
@ExcelProperty("站点")
private String station;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -41,6 +41,9 @@ public class OtherExpenseRecordExportExcel implements Serializable {
@ExcelProperty("金额")
private BigDecimal amount;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -74,6 +74,9 @@ public class TireReplacementRecordExportExcel implements Serializable {
@ExcelProperty("换胎说明")
private String replacementDescription;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -55,6 +55,9 @@ public class TransportChangeRecordExportExcel implements Serializable {
@ExcelProperty("变更内容")
private String changeContent;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -36,6 +36,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Date;
/**
* 船舶管理 Excel
@@ -144,4 +145,10 @@ public class TransportShipExcel implements Serializable {
@ExcelProperty("船舶经营人")
private String shipOperator;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
private Date createTime;
}
@@ -35,6 +35,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Date;
/**
* 车辆管理 Excel
@@ -83,4 +84,10 @@ public class TransportVehicleExcel implements Serializable {
@ExcelProperty("年审有效期")
private LocalDate annualReviewEndDate;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
private Date createTime;
}
@@ -13,6 +13,7 @@ import cn.idev.excel.FastExcel;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import cn.idev.excel.metadata.data.ReadCellData;
import org.springblade.common.excel.ImportCellReadException;
import org.springblade.core.log.exception.ServiceException;
import org.springframework.web.multipart.MultipartFile;
@@ -60,10 +61,11 @@ public final class VehicleReconciliationFeeReader {
@Override
public void invoke(Map<Integer, ReadCellData<?>> row, AnalysisContext context) {
Map<String, BigDecimal> values = new LinkedHashMap<>();
Integer excelRowIndex = context.readRowHolder() == null ? null : context.readRowHolder().getRowIndex();
if (freightColumn >= 0 && settlementColumn > freightColumn) {
for (int column = freightColumn + 1; column < settlementColumn; column++) {
String name = headers.get(column);
if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column)));
if (name != null && !name.isBlank()) values.put(name.trim(), decimal(row.get(column), name.trim(), excelRowIndex));
}
}
feeItems.add(values);
@@ -80,7 +82,7 @@ public final class VehicleReconciliationFeeReader {
.findFirst().orElse(-1);
}
private BigDecimal decimal(ReadCellData<?> cellData) {
private BigDecimal decimal(ReadCellData<?> cellData, String columnName, Integer excelRowIndex) {
if (cellData == null) return BigDecimal.ZERO.setScale(2);
Object value = cellData.getData();
if (value == null) {
@@ -95,7 +97,10 @@ public final class VehicleReconciliationFeeReader {
try {
return new BigDecimal(String.valueOf(value).trim()).setScale(2);
} catch (NumberFormatException exception) {
return BigDecimal.ZERO.setScale(2);
// 这里绝不能静默返回 0:费用列会进 feeItemsJson 参与结算金额与自动匹配,
// 把「一百元」这类写法悄悄变成 0.00 等于往库里写脏数据。
// 改为抛带位置信息的异常,由 Controller 的读取阶段兜底导出原表全量明细。
throw new ImportCellReadException(excelRowIndex, columnName, String.valueOf(value).trim());
}
}
@@ -94,6 +94,9 @@ public class ViolationRecordExcel implements Serializable {
@ExcelProperty("处理结果")
private String processResult;
@ExcelProperty("创建人")
private String createUserName;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@@ -98,7 +98,7 @@ public class WaybillExcel implements Serializable {
private String driverName;
@ExcelProperty("司机手机号")
private String driverPhone;
@ExcelProperty("车/船/航班/班列号")
@ExcelProperty("车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("挂车车牌号")
private String trailerVehicleNo;
@@ -28,7 +28,7 @@ public class WaybillImportBatchExcel implements Serializable {
private String originalNo;
@ExcelProperty("配载标识号")
private String loadingIdentifier;
@ExcelProperty("*车牌号/航班号/船号/班列号")
@ExcelProperty("*车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("*运输方式")
private String transportType;
@@ -0,0 +1,140 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
* Copyright of this software remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.listener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.transport.config.BfiProperties;
import org.springblade.transport.event.KingdeeBillApprovedEvent;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IInvoiceApplicationService;
import org.springblade.transport.service.IInvoiceReceiptService;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import java.util.List;
/**
* 单据审批通过自动同步金蝶监听器:审批事务提交(AFTER_COMMIT)后复用手动按钮同一 Service 方法推送,
* 与客商档案审批后自动推送同线程模式。
* <p>
* 不变式:审批结果不受推送影响——监听器整体 try/catch 吞异常,推送异常只落 failed + 日志;
* 失败单据由 {@code KingdeeAutoSyncRetryTask} 定时重推、手动按钮兜底。
* <p>
* 推送失败原因通过 {@link #pollSyncFailure()} 传递给审批接口响应:AFTER_COMMIT 监听与 approve
* 同线程同请求,Controller 在 approve 返回后取走原因拼进提示文案;OA 回调路径无人取走,
* 由监听入口清空兜底,不会串单。
* <p>
* ponytail: 同步推送占用审批请求线程,若 BFI 网关大面积变慢导致审批接口可感知变慢,
* 再把路由调用提交到守护线程池(MkProcessMessageService 已有同款裸 ScheduledExecutorService 模式)。
* <p>
* 域开关(Nacos thirdParty.bfi.autoSync.*)默认全部关闭,与既有定时任务开关风格一致;
* 关闭某域 = 该域回到现状(审批不自动推送、手动按钮照用)。
*
* @author Chill
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class KingdeeApprovalSyncListener {
private final IFormalSettlementService formalSettlementService;
private final IPaymentApplicationService paymentApplicationService;
private final IInvoiceApplicationService invoiceApplicationService;
private final IInvoiceReceiptService invoiceReceiptService;
private final BfiProperties bfiProperties;
/** 当前审批请求线程的自动推送失败原因,供 Controller 取走提示前端(见类注释线程说明) */
private static final ThreadLocal<String> SYNC_FAILURE = new ThreadLocal<>();
/**
* 取走并清除当前审批请求的自动推送失败原因;null 表示推送成功或未触发推送。
* 须在 service.approve 返回后立即调用(监听器在审批事务提交时同步执行完毕)。
*/
public static String pollSyncFailure() {
String failure = SYNC_FAILURE.get();
SYNC_FAILURE.remove();
return failure;
}
/**
* 事务提交后同步推送。fallbackExecution=true 兜底:approve 未开启事务时事件不再被默认丢弃。
*
* @param event 审批通过事件(bizType + 单据id)
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
public void onBillApproved(KingdeeBillApprovedEvent event) {
SYNC_FAILURE.remove();
try {
switch (event.bizType()) {
case KingdeeBillApprovedEvent.BIZ_FORMAL_SETTLEMENT -> pushSettlement(event.id());
case KingdeeBillApprovedEvent.BIZ_PAYMENT_APPLICATION -> pushPayment(event.id());
case KingdeeBillApprovedEvent.BIZ_INVOICE_APPLICATION -> pushInvoiceApply(event.id());
case KingdeeBillApprovedEvent.BIZ_INVOICE_RECEIPT -> pushReceipt(event.id());
default -> log.warn("审批通过自动同步金蝶跳过:未知业务类型 bizType={} id={}", event.bizType(), event.id());
}
} catch (Exception exception) {
// 推送失败只记日志(失败状态已在推送方法内独立事务落库),不影响审批结果,交给重推任务与手动按钮兜底;
// 原因暂存线程变量,由审批 Controller 取走提示前端
log.error("审批通过自动同步金蝶失败 bizType={} id={}", event.bizType(), event.id(), exception);
String reason = exception.getMessage() == null ? "金蝶接口调用异常" : exception.getMessage();
// 手动按钮同文案已带"推送金蝶失败:"前缀,拼提示时去掉避免重复
SYNC_FAILURE.set(reason.startsWith("推送金蝶失败:") ? reason.substring("推送金蝶失败:".length()) : reason);
}
}
private void pushSettlement(Long id) {
if (!bfiProperties.getAutoSync().isSettlement()) {
return;
}
log.info("正式结算单[{}]审批通过自动同步金蝶开始", id);
String billNo = formalSettlementService.syncKingdee(id);
log.info("正式结算单[{}]审批通过自动同步金蝶完成:billNo={}", id, billNo);
}
private void pushPayment(Long id) {
if (!bfiProperties.getAutoSync().isPayment()) {
return;
}
log.info("付款申请[{}]审批通过自动同步金蝶开始", id);
String kingdeeBillId = paymentApplicationService.syncKingdee(id);
log.info("付款申请[{}]审批通过自动同步金蝶完成:kingdeeBillId={}", id, kingdeeBillId);
}
private void pushInvoiceApply(Long id) {
if (!bfiProperties.getAutoSync().isInvoiceApply()) {
return;
}
log.info("开票申请[{}]审批通过自动同步金蝶开始", id);
String billNo = invoiceApplicationService.syncKingdee(id);
log.info("开票申请[{}]审批通过自动同步金蝶完成:billNo={}", id, billNo);
}
private void pushReceipt(Long id) {
if (!bfiProperties.getAutoSync().isReceipt()) {
return;
}
log.info("收票登记[{}]审批通过自动推送金蝶收票单开始", id);
String summary = invoiceReceiptService.pushKingdeeReceivedInvoice(List.of(id), true);
log.info("收票登记[{}]审批通过自动推送金蝶收票单完成:{}", id, summary);
}
}
@@ -42,8 +42,8 @@
accident_location,
accident_nature,
accident_responsibility,
CASE WHEN direct_economic_loss &lt; 0 THEN 0 ELSE direct_economic_loss END AS direct_economic_loss,
CASE WHEN insurance_claim_amount &lt; 0 THEN 0 ELSE insurance_claim_amount END AS insurance_claim_amount,
CASE WHEN direct_economic_loss = -1 THEN NULL WHEN direct_economic_loss &lt; 0 THEN 0 ELSE direct_economic_loss END AS direct_economic_loss,
CASE WHEN insurance_claim_amount = -1 THEN NULL WHEN insurance_claim_amount &lt; 0 THEN 0 ELSE insurance_claim_amount END AS insurance_claim_amount,
accident_reason_damage,
attachments,
remark
@@ -88,7 +88,7 @@
valid_until_date,
passenger_type_level,
inspection_unit,
CASE WHEN fee &lt; 0 THEN 0 ELSE fee END AS fee,
CASE WHEN fee = -1 THEN NULL WHEN fee &lt; 0 THEN 0 ELSE fee END AS fee,
assessment_unit,
attachments,
remark
@@ -46,6 +46,12 @@
<result column="emergency_contact_name" property="emergencyContactName"/>
<result column="emergency_contact_mobile" property="emergencyContactMobile"/>
<result column="remark" property="remark"/>
<result column="id_card_valid_start" property="idCardValidStart"/>
<result column="id_card_valid_end" property="idCardValidEnd"/>
<result column="id_card_long_term" property="idCardLongTerm"/>
<result column="certification_status" property="certificationStatus"/>
<result column="certification_reject_reason" property="certificationRejectReason"/>
<result column="effective_cert_record_id" property="effectiveCertRecordId"/>
</resultMap>
<sql id="BaseColumn">
@@ -70,6 +76,9 @@
posts,
id_card_front,
id_card_back,
id_card_valid_start,
id_card_valid_end,
id_card_long_term,
head_photo,
driving_type,
driving_license_no,
@@ -91,7 +100,10 @@
organization_name,
emergency_contact_name,
emergency_contact_mobile,
remark
remark,
certification_status,
certification_reject_reason,
effective_cert_record_id
</sql>
<select id="selectByIdCardNoIncludingDeleted" resultType="org.springblade.transport.pojo.entity.Driver">
@@ -44,8 +44,8 @@
policy_no,
start_date,
end_date,
CASE WHEN insured_amount &lt; 0 THEN 0 ELSE insured_amount END AS insured_amount,
CASE WHEN premium &lt; 0 THEN 0 ELSE premium END AS premium,
CASE WHEN insured_amount = -1 THEN NULL ELSE insured_amount END AS insured_amount,
CASE WHEN premium = -1 THEN NULL ELSE premium END AS premium,
invoice_no,
invoice_date,
ocr_template,
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.KingdeeAttachmentSync;
/**
* 金蝶附件同步 Mapper
*
* @author Chill
*/
@Mapper
public interface KingdeeAttachmentSyncMapper extends BaseMapper<KingdeeAttachmentSync> {
}
@@ -0,0 +1,18 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.KingdeeBillLock;
/** 金蝶单据锁定记录 Mapper @author Chill */
@Mapper
public interface KingdeeBillLockMapper extends BaseMapper<KingdeeBillLock> {
}
@@ -26,8 +26,12 @@
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
/**
* 金蝶进项发票票据池 Mapper 接口
@@ -37,4 +41,14 @@ import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
@Mapper
public interface KingdeeInvoicePoolMapper extends BaseMapper<KingdeeInvoicePool> {
/**
* 金蝶票据池镜像统一分页查询:仅出可用票(剩余可用金额>0 且未使用),认领/登记标记由 EXISTS 子查询直出
*
* @param page 分页参数
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
* @param tenantId 当前租户
* @return 统一视图分页
*/
IPage<KingdeeInvoicePoolVO> selectInvoicePoolPage(IPage<KingdeeInvoicePoolVO> page,
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId);
}
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.transport.mapper.KingdeeInvoicePoolMapper">
<!-- 已认领:任一有效正式结算单发票行出现过(结算单作废不删发票行,须关联主单剔除作废与已删;自定义 SQL 无租户插件,手工带租户与逻辑删除) -->
<sql id="claimedExists">
EXISTS (SELECT 1 FROM blade_formal_settlement_invoice c
WHERE c.is_deleted = 0 AND c.tenant_id = p.tenant_id AND c.invoice_no = p.invoice_no
AND EXISTS (SELECT 1 FROM blade_formal_settlement s
WHERE s.id = c.formal_settlement_id AND s.is_deleted = 0
AND s.approval_status != 'voided'))
</sql>
<!-- 已登记:存在非作废收票登记 -->
<sql id="registeredExists">
EXISTS (SELECT 1 FROM blade_invoice_receipt r
WHERE r.is_deleted = 0 AND r.tenant_id = p.tenant_id
AND r.approval_status != 'voided' AND r.invoice_no = p.invoice_no)
</sql>
<!-- 统一视图列:价税合计与可认领/默认认领金额同源;剩余可用金额为金蝶可用税额口径(仅展示);镜像 invoice_type 落库即中文名 -->
<sql id="invoicePoolVoColumns">
p.id, p.invoice_no, p.serial_no,
DATE_FORMAT(p.invoice_date, '%Y-%m-%d') AS invoice_date,
p.invoice_type, p.tax_rate, p.invoice_amount, p.tax_amount,
p.receiver_name, p.buyer_tax_no, p.issuer_name, p.saler_tax_no,
p.bank_name, p.bank_account, p.issuing_bank, p.phone,
p.customer_emails, p.department_emails,
p.kingdee_bill_no, p.kingdee_status, p.check_status, p.attachments_json,
p.invoice_amount AS available_invoice_amount,
p.invoice_amount AS matched_amount,
p.available_amount AS available_tax_amount,
<include refid="claimedExists"/> AS claimed,
<include refid="registeredExists"/> AS registered
</sql>
<!-- 选票只出可用票:剩余可用金额>0 且金额使用状态未使用(与实时方案 isUsablePoolRow 同口径,本地化执行) -->
<sql id="usableFilter">
AND p.available_amount &gt; 0 AND p.amount_use_status = '0'
</sql>
<select id="selectInvoicePoolPage" resultType="org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO">
SELECT
<include refid="invoicePoolVoColumns"/>
FROM blade_kingdee_invoice_pool p
WHERE p.is_deleted = 0 AND p.status = 1 AND p.tenant_id = #{tenantId}
<include refid="usableFilter"/>
<if test="query.invoiceNo != null and query.invoiceNo != ''">
AND p.invoice_no LIKE CONCAT('%', #{query.invoiceNo}, '%')
</if>
<if test="query.salerName != null and query.salerName != ''">
AND p.issuer_name LIKE CONCAT('%', #{query.salerName}, '%')
</if>
<if test="query.buyerName != null and query.buyerName != ''">
AND p.receiver_name LIKE CONCAT('%', #{query.buyerName}, '%')
</if>
<if test="query.invoiceType != null and query.invoiceType != ''">
AND p.invoice_type_code = #{query.invoiceType}
</if>
<if test="query.salerTaxNo != null and query.salerTaxNo != ''">
AND p.saler_tax_no = #{query.salerTaxNo}
</if>
<if test="query.buyerTaxNo != null and query.buyerTaxNo != ''">
AND p.buyer_tax_no = #{query.buyerTaxNo}
</if>
<if test="query.invoiceDateStart != null and query.invoiceDateStart != ''">
AND p.invoice_date &gt;= #{query.invoiceDateStart}
</if>
<if test="query.invoiceDateEnd != null and query.invoiceDateEnd != ''">
AND p.invoice_date &lt;= #{query.invoiceDateEnd}
</if>
<if test="query.claimed != null and query.claimed">
AND <include refid="claimedExists"/>
</if>
<if test="query.claimed != null and !query.claimed">
AND NOT <include refid="claimedExists"/>
</if>
<if test="query.registered != null and query.registered">
AND <include refid="registeredExists"/>
</if>
<if test="query.registered != null and !query.registered">
AND NOT <include refid="registeredExists"/>
</if>
ORDER BY p.source_updated_time DESC, p.create_time DESC
</select>
</mapper>
@@ -44,7 +44,7 @@
vehicle_no,
maintainer,
maintenance_time,
CASE WHEN mileage &lt; 0 THEN 0 ELSE mileage END AS mileage,
CASE WHEN mileage = -1 THEN NULL WHEN mileage &lt; 0 THEN 0 ELSE mileage END AS mileage,
mileage_unit,
maintenance_item,
cost,
@@ -52,7 +52,7 @@
contact_phone,
address,
next_maintenance_time,
CASE WHEN next_maintenance_mileage &lt; 0 THEN 0 ELSE next_maintenance_mileage END AS next_maintenance_mileage,
CASE WHEN next_maintenance_mileage = -1 THEN NULL WHEN next_maintenance_mileage &lt; 0 THEN 0 ELSE next_maintenance_mileage END AS next_maintenance_mileage,
attachments,
remark
FROM
@@ -36,10 +36,10 @@
is_deleted,
vehicle_type,
vehicle_no,
CASE WHEN previous_month_mileage &lt; 0 THEN 0 ELSE previous_month_mileage END AS previous_month_mileage,
CASE WHEN current_month_mileage &lt; 0 THEN 0 ELSE current_month_mileage END AS current_month_mileage,
CASE WHEN monthly_mileage &lt; 0 THEN 0 ELSE monthly_mileage END AS monthly_mileage,
CASE WHEN total_mileage &lt; 0 THEN 0 ELSE total_mileage END AS total_mileage,
CASE WHEN previous_month_mileage = -1 THEN NULL WHEN previous_month_mileage &lt; 0 THEN 0 ELSE previous_month_mileage END AS previous_month_mileage,
CASE WHEN current_month_mileage = -1 THEN NULL WHEN current_month_mileage &lt; 0 THEN 0 ELSE current_month_mileage END AS current_month_mileage,
CASE WHEN monthly_mileage = -1 THEN NULL WHEN monthly_mileage &lt; 0 THEN 0 ELSE monthly_mileage END AS monthly_mileage,
CASE WHEN total_mileage = -1 THEN NULL WHEN total_mileage &lt; 0 THEN 0 ELSE total_mileage END AS total_mileage,
mileage_unit,
attachments,
remark
@@ -48,10 +48,10 @@
vehicle_no,
card_holder,
data_source,
CASE WHEN quantity &lt; 0 THEN 0 ELSE quantity END AS quantity,
CASE WHEN unit_price &lt; 0 THEN 0 ELSE unit_price END AS unit_price,
CASE WHEN transaction_amount &lt; 0 THEN 0 ELSE transaction_amount END AS transaction_amount,
CASE WHEN balance &lt; 0 THEN 0 ELSE balance END AS balance,
CASE WHEN quantity = -1 THEN NULL WHEN quantity &lt; 0 THEN 0 ELSE quantity END AS quantity,
CASE WHEN unit_price = -1 THEN NULL WHEN unit_price &lt; 0 THEN 0 ELSE unit_price END AS unit_price,
CASE WHEN transaction_amount = -1 THEN NULL WHEN transaction_amount &lt; 0 THEN 0 ELSE transaction_amount END AS transaction_amount,
CASE WHEN balance = -1 THEN NULL WHEN balance &lt; 0 THEN 0 ELSE balance END AS balance,
station,
attachments,
remark
@@ -22,7 +22,6 @@
c.attachments_json,
c.remark,
c.claim_status,
c.awrp_src_code,
c.kingdee_bill_no,
c.kingdee_bill_status,
c.voided_by,
@@ -37,6 +36,7 @@
f.summary,
f.transaction_time,
f.detail_serial_no,
f.push_status,
(SELECT GROUP_CONCAT(rcs.formal_settlement_no ORDER BY rcs.id SEPARATOR ',')
FROM blade_receipt_claim_settlement rcs
WHERE rcs.receipt_claim_id = c.id AND rcs.is_deleted = 0) AS associated_settlement_nos
@@ -92,6 +92,9 @@
<if test="query.kingdeeBillStatus != null and query.kingdeeBillStatus != ''">
AND c.kingdee_bill_status = #{query.kingdeeBillStatus}
</if>
<if test="query.pushStatus != null and query.pushStatus != ''">
AND f.push_status = #{query.pushStatus}
</if>
</sql>
<select id="selectClaimRecordPage" resultType="org.springblade.transport.pojo.vo.ReceiptClaimRecordVO">
@@ -38,8 +38,8 @@
handler,
replacement_time,
tire_brand,
CASE WHEN tire_quantity &lt; 0 THEN 0 ELSE tire_quantity END AS tire_quantity,
CASE WHEN replacement_cost &lt; 0 THEN 0 ELSE replacement_cost END AS replacement_cost,
CASE WHEN tire_quantity = -1 THEN NULL WHEN tire_quantity &lt; 0 THEN 0 ELSE tire_quantity END AS tire_quantity,
CASE WHEN replacement_cost = -1 THEN NULL WHEN replacement_cost &lt; 0 THEN 0 ELSE replacement_cost END AS replacement_cost,
replacement_description,
attachments,
remark
@@ -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>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Param;
import org.springblade.transport.pojo.entity.TransportCertRecord;
import org.springblade.transport.pojo.vo.TransportCertRecordVO;
import java.util.List;
/**
* 认证记录 Mapper 接口
*
* @author Chill
*/
public interface TransportCertRecordMapper extends BaseMapper<TransportCertRecord> {
/**
* 审核列表分页:认证记录 + 该记录的司机姓名与手机号
* <p>
* 必须走自定义 SQL 而不是单表 Wrapper:司机姓名与手机号取的是司机档案的**当前**值,
* 且「按司机姓名/手机号筛选」必须发生在**分页之前**——若在 Java 层补字段再过滤,
* 就变成「先分页再过滤」,每页条数与总数都不准。
*
* @param page 分页参数
* @param bizType 认证对象类型:driver-司机,vehicle-车辆
* @param status 审核状态:0审核中 1已通过 2已驳回 3已作废
* @param bizKey 展示键模糊匹配(提交时的司机姓名 / 车牌号),可为空
* @param driverName 司机姓名模糊匹配(当前姓名,并兼顾提交时的旧名),可为空
* @param driverMobile 司机手机号模糊匹配,可为空
* @return 认证记录列表
*/
List<TransportCertRecordVO> selectAuditPage(IPage<TransportCertRecordVO> page,
@Param("bizType") String bizType,
@Param("status") Integer status,
@Param("bizKey") String bizKey,
@Param("driverName") String driverName,
@Param("driverMobile") String driverMobile);
}
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.transport.mapper.TransportCertRecordMapper">
<resultMap id="certRecordAuditMap" type="org.springblade.transport.pojo.vo.TransportCertRecordVO">
<result column="id" property="id"/>
<result column="tenant_id" property="tenantId"/>
<result column="biz_type" property="bizType"/>
<result column="biz_id" property="bizId"/>
<result column="biz_key" property="bizKey"/>
<result column="status" property="status"/>
<result column="reject_reason" property="rejectReason"/>
<result column="submit_time" property="submitTime"/>
<result column="submit_user" property="submitUser"/>
<result column="audit_time" property="auditTime"/>
<result column="audit_user" property="auditUser"/>
<result column="create_user" property="createUser"/>
<result column="create_dept" property="createDept"/>
<result column="create_time" property="createTime"/>
<result column="update_user" property="updateUser"/>
<result column="update_time" property="updateTime"/>
<result column="is_deleted" property="isDeleted"/>
<result column="driver_name" property="driverName"/>
<result column="driver_mobile" property="driverMobile"/>
</resultMap>
<!--
审核列表:认证记录 + 该记录的司机姓名与手机号(司机档案的当前值)。
司机是谁:司机侧就是认证对象本身(biz_id);车辆侧是这条记录的**提交司机**
(车辆认证强制「只能操作本人绑定车辆」,故提交司机必然是这辆车的绑定司机)。
提交人存的是用户 ID,而司机档案的 user_id 是同一个值,故按 user_id 定位。
子查询带 ORDER BY update_time DESC LIMIT 1 有实际作用,不是保险起见:
user_id 上没有唯一索引,脏数据里同一用户可能对应多条司机档案,
不收敛就会让一条记录在列表里出现多行(并让总数虚高)。
按 update_time 最新取,与司机端解析当前司机档案的既有口径一致。
-->
<select id="selectAuditPage" resultMap="certRecordAuditMap">
SELECT
r.id,
r.tenant_id,
r.biz_type,
r.biz_id,
r.biz_key,
r.status,
r.reject_reason,
r.submit_time,
r.submit_user,
r.audit_time,
r.audit_user,
r.create_user,
r.create_dept,
r.create_time,
r.update_user,
r.update_time,
r.is_deleted,
d.driver_name AS driver_name,
d.mobile AS driver_mobile
FROM
blade_transport_cert_record r
LEFT JOIN blade_transport_driver d
ON d.is_deleted = 0
AND d.id = CASE
WHEN r.biz_type = 'driver' THEN r.biz_id
ELSE (
SELECT dr.id
FROM blade_transport_driver dr
WHERE dr.is_deleted = 0
AND dr.user_id = r.submit_user
ORDER BY dr.update_time DESC, dr.id DESC
LIMIT 1
)
END
WHERE
r.is_deleted = 0
AND r.biz_type = #{bizType}
AND r.status = #{status}
<if test="bizKey != null and bizKey != ''">
AND r.biz_key LIKE CONCAT('%', #{bizKey}, '%')
</if>
<!--
姓名同时匹配「档案当前姓名」与「提交时的姓名」:
司机改名后,按新名与按旧名都应该搜得到他的记录,
只认一个会让审核员两头试。手机号则只有档案当前值一个来源。
-->
<if test="driverName != null and driverName != ''">
AND (
d.driver_name LIKE CONCAT('%', #{driverName}, '%')
OR r.biz_key LIKE CONCAT('%', #{driverName}, '%')
)
</if>
<if test="driverMobile != null and driverMobile != ''">
AND d.mobile LIKE CONCAT('%', #{driverMobile}, '%')
</if>
ORDER BY
r.submit_time DESC
</select>
</mapper>
@@ -48,6 +48,7 @@
<result column="remark" property="remark"/>
<result column="certification_status" property="certificationStatus"/>
<result column="certification_reject_reason" property="certificationRejectReason"/>
<result column="effective_cert_record_id" property="effectiveCertRecordId"/>
</resultMap>
<sql id="BaseColumn">
@@ -96,6 +97,7 @@
remark
, certification_status
, certification_reject_reason
, effective_cert_record_id
</sql>
<sql id="ExpiryWithin30Condition">
@@ -46,8 +46,8 @@
violation_item,
violation_time,
location,
CASE WHEN fine_amount &lt; 0 THEN 0 ELSE fine_amount END AS fine_amount,
CASE WHEN deduct_points &lt; 0 THEN 0 ELSE deduct_points END AS deduct_points,
CASE WHEN fine_amount = -1 THEN NULL WHEN fine_amount &lt; 0 THEN 0 ELSE fine_amount END AS fine_amount,
CASE WHEN deduct_points = -1 THEN NULL WHEN deduct_points &lt; 0 THEN 0 ELSE deduct_points END AS deduct_points,
penalty_unit,
process_status,
process_description,
@@ -3,15 +3,25 @@ package org.springblade.transport.mk;
import lombok.RequiredArgsConstructor;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.pojo.dto.BillPaymentStatusRequest;
import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest;
import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest;
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
import org.springblade.transport.pojo.entity.BillPayment;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.InvoiceApplication;
import org.springblade.transport.pojo.entity.InvoiceReceipt;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.PreSettlement;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.service.IBillPaymentService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IInvoiceApplicationService;
import org.springblade.transport.service.IInvoiceReceiptService;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.constant.HcsSyncConstant;
@@ -214,57 +224,6 @@ public final class MkProcessBizHandlers {
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class WaybillHandler implements IMkProcessBizHandler {
private final IWaybillService service;
@Override
public String bizType() {
return "waybill-manage";
}
@Override
public Object publicDetail(Long id) {
return service.publicDetail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
if (Func.isEmpty(node)) {
return false;
}
entity.setCurrentProcessNode(node);
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
entity.setCurrentProcessNode("审核通过");
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
entity.setCurrentProcessNode("审核不通过");
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
@@ -371,11 +330,18 @@ public final class MkProcessBizHandlers {
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
return service.updateById(entity);
// 统一走 service.approve:与手动审批同一入口(补变更记录),审批事务提交后自动触发金蝶推送事件;
// 仅审批中单据允许通过,重复回调被状态校验拒绝,不重复推送
FormalSettlementStatusRequest request = new FormalSettlementStatusRequest();
request.setId(id);
service.approve(request);
FormalSettlement updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
@Override
@@ -434,12 +400,20 @@ public final class MkProcessBizHandlers {
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
// HCS 推送零埋点:MK 审批回调仅置待同步状态,与 /approve 入口收敛于同一权威状态(ADR-0001)
entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
return service.updateById(entity);
// 统一走 service.approve:与手动审批同一入口(补票据额度联动 useBillBalance),审批事务提交后自动触发金蝶推送事件;
// 仅审批中单据允许通过,重复回调被状态校验拒绝,不重复推送
PaymentApplicationStatusRequest request = new PaymentApplicationStatusRequest();
request.setId(id);
service.approve(request);
PaymentApplication updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
// HCS 推送零埋点:MK 审批回调仅置待同步状态,与 /approve 入口收敛于同一权威状态(ADR-0001)
updated.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
service.updateById(updated);
}
return true;
}
@Override
@@ -454,4 +428,229 @@ public final class MkProcessBizHandlers {
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class InvoiceApplicationHandler implements IMkProcessBizHandler {
private final IInvoiceApplicationService service;
@Override
public String bizType() {
return "invoice-application";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
InvoiceApplication entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
InvoiceApplication entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
InvoiceApplicationStatusRequest request = new InvoiceApplicationStatusRequest();
request.setId(id);
service.approve(request);
InvoiceApplication updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
InvoiceApplication entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
InvoiceApplicationStatusRequest request = new InvoiceApplicationStatusRequest();
request.setId(id);
request.setReason("MK流程驳回");
service.returnBill(request);
InvoiceApplication updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核不通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class InvoiceReceiptHandler implements IMkProcessBizHandler {
private final IInvoiceReceiptService service;
@Override
public String bizType() {
return "invoice-receipt";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
InvoiceReceipt entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
InvoiceReceipt entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
InvoiceReceiptStatusRequest request = new InvoiceReceiptStatusRequest();
request.setId(id);
service.approve(request);
InvoiceReceipt updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
InvoiceReceipt entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
InvoiceReceiptStatusRequest request = new InvoiceReceiptStatusRequest();
request.setId(id);
request.setReason("MK流程驳回");
service.returnBill(request);
InvoiceReceipt updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核不通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class BillPaymentHandler implements IMkProcessBizHandler {
private final IBillPaymentService service;
@Override
public String bizType() {
return "bill-payment";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
BillPayment entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
BillPayment entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
BillPaymentStatusRequest request = new BillPaymentStatusRequest();
request.setId(id);
service.approve(request);
BillPayment updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
BillPayment entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
BillPaymentStatusRequest request = new BillPaymentStatusRequest();
request.setId(id);
request.setReason("MK流程驳回");
service.returnBill(request);
BillPayment updated = service.getById(id);
if (updated != null) {
updated.setCurrentNode("审核不通过");
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(updated);
}
return true;
}
}
}
@@ -10,7 +10,6 @@ import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -106,7 +105,7 @@ public class MkProcessMessageService {
bizType, processId, loginName, JSON.toJSONString(processInfo));
if (isProcessFinished(processInfo)) {
applyProcessResult(handler, bizType, bizIdText, processId, formValues, true);
} else if (isProcessRejected(processInfo)) {
} else if (MkProcessNodeHelper.isProcessRejected(processInfo)) {
applyProcessResult(handler, bizType, bizIdText, processId, formValues, false);
} else if (StringUtil.isNotBlank(bizIdText)) {
try {
@@ -124,21 +123,6 @@ public class MkProcessMessageService {
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
}
private static boolean isProcessRejected(Object processInfo) {
Map<String, Object> info = asMap(processInfo);
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
return false;
}
if (hasItems(info.get("currentHandlers"))) {
return false;
}
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
}
private static boolean hasItems(Object value) {
return value instanceof Collection<?> collection && !collection.isEmpty();
}
private void applyProcessResult(IMkProcessBizHandler handler, String bizType, String bizIdText,
String processId, Map<String, Object> formValues, boolean approved) {
String action = approved ? "审核通过" : "审核驳回";
@@ -154,8 +138,18 @@ public class MkProcessMessageService {
} else {
handler.rejectFromProcess(bizId, processorName);
}
try {
FR<Boolean> markResult = businessProcessClient.markProcessFinished(processId, approved);
log.info("公开页标记业务流程完结 bizType={} processId={} approved={} result={}",
bizType, processId, approved, JSON.toJSONString(markResult));
} catch (Exception markException) {
log.error("公开页标记业务流程完结失败 bizType={} processId={} approved={}",
bizType, processId, approved, markException);
}
} catch (NumberFormatException e) {
log.warn("流程{}但业务id格式不正确 bizType={} id={}", action, bizType, bizIdText);
} catch (Exception e) {
log.error("流程{}同步业务状态失败 bizType={} id={} processId={}", action, bizType, bizIdText, processId, e);
}
}
@@ -11,20 +11,47 @@ import java.util.Map;
import java.util.stream.Collectors;
/**
* 解析 MK 流程实例详情中的当前节点、当前处理人
* 解析 MK 流程实例详情中的当前节点、当前处理人,以及流程是否驳回到起草节点。
*/
public final class MkProcessNodeHelper {
/**
* 起草节点编号。审批节点编号随流程模板变化,不能用来判断驳回。
*/
private static final String DRAFT_NODE_NUMBER = "N2";
private static final List<String> CURRENT_NODE_KEYS = List.of("handlerInfos", "drafterInfos", "adminInfos");
private MkProcessNodeHelper() {
}
/**
* 流程未结束,且当前待办已经回到起草节点时,才视为审核驳回。
* 中间审批节点通过后流程状态仍是 20,不能把历史节点编号当成驳回。
*/
public static boolean isProcessRejected(Object processInfo) {
Map<String, Object> info = asMap(processInfo);
String processStatus = text(info.get("fdProcessStatus"));
if (!"11".equals(processStatus) && !"20".equals(processStatus)) {
return false;
}
return hasActiveDraftNode(asMap(info.get("fdTaskInfo")));
}
public static String currentProcessor(Object processInfo) {
return joinDistinct(extractTexts(asMap(processInfo).get("currentHandlers"), "fdName", "name"), "、");
}
public static String currentNode(Object processInfo) {
Map<String, Object> taskInfo = asMap(asMap(processInfo).get("fdTaskInfo"));
return joinDistinct(extractTexts(taskInfo.get("handlerInfos"), "nodeName"), "、");
List<String> nodeNames = new ArrayList<>();
for (String key : CURRENT_NODE_KEYS) {
nodeNames.addAll(extractActiveNodeNames(taskInfo.get(key)));
}
if (nodeNames.isEmpty()) {
nodeNames.addAll(extractTexts(taskInfo.get("handlerInfos"), "nodeName"));
}
return joinDistinct(nodeNames, "、");
}
public static String text(String value, String fallback) {
@@ -52,6 +79,59 @@ public final class MkProcessNodeHelper {
}
}
private static boolean hasActiveDraftNode(Map<String, Object> taskInfo) {
for (String key : CURRENT_NODE_KEYS) {
Object nodes = taskInfo.get(key);
if (!(nodes instanceof Collection<?> collection)) {
continue;
}
for (Object item : collection) {
if (isActiveDraftNode(asMap(item))) {
return true;
}
}
}
return false;
}
private static boolean isActiveDraftNode(Map<String, Object> node) {
if (!"20".equals(text(node.get("status")))) {
return false;
}
String nodeNumber = text(node.get("nodeNumber"));
if (Func.isEmpty(nodeNumber)) {
nodeNumber = text(node.get("nodeId"));
}
if (DRAFT_NODE_NUMBER.equals(nodeNumber)) {
return true;
}
String activityType = text(node.get("activityType"));
return "draft".equalsIgnoreCase(activityType) || "draftTask".equalsIgnoreCase(activityType);
}
private static List<String> extractActiveNodeNames(Object listObj) {
List<String> result = new ArrayList<>();
if (!(listObj instanceof Collection<?> collection)) {
return result;
}
for (Object item : collection) {
Map<String, Object> node = asMap(item);
if (!"20".equals(text(node.get("status")))) {
continue;
}
String nodeName = text(node.get("nodeName"));
if (Func.isNotEmpty(nodeName)) {
result.add(nodeName);
}
}
return result;
}
private static String text(Object value) {
String text = Func.toStr(value, "").trim();
return "null".equalsIgnoreCase(text) ? "" : text;
}
private static List<String> extractTexts(Object listObj, String... keys) {
List<String> result = new ArrayList<>();
if (!(listObj instanceof Collection<?> collection) || keys == null) {
@@ -36,6 +36,7 @@ import org.springblade.transport.ocr.config.BaiduOcrProperties;
import org.springblade.transport.ocr.constant.BaiduOcrType;
import org.springblade.transport.ocr.service.IBaiduOcrService;
import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
import org.springblade.transport.support.OcrFieldSupport;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
@@ -143,6 +144,9 @@ public class BaiduOcrServiceImpl implements IBaiduOcrService {
response.setType(type.name());
response.setSide(normalizedSide);
response.setResult(objectMapper.convertValue(result, RESULT_TYPE));
// 同时下发归一化字段:调用方不该去认识百度那套中文键名,
// 且键名对不上时前端只会「静默什么都不填」,是最难查的一类问题
response.setFields(OcrFieldSupport.extract(type, normalizedSide, result));
return response;
}
@@ -0,0 +1,83 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.transport.pojo.entity.TransportCertRecord;
import org.springblade.transport.pojo.vo.CertAuditDetailVO;
import org.springblade.transport.pojo.vo.TransportCertRecordVO;
/**
* 认证审核服务(管理端)
* <p>
* 审核对象是**一条认证记录**,不是「一辆车」或「一个人」。
* 通过使该记录成为生效快照;驳回只终结该记录,生效快照保持不变。
*
* @author Chill
*/
public interface ICertAuditService {
/**
* 认证记录分页(按认证对象类型与审核状态)
* <p>
* 审核页按「待审核 / 已通过 / 已驳回」分页签展示,故状态可传;
* 不传时按待审核处理,兼容只想要待审队列的调用方。
* <p>
* 列表同时下发该记录的司机姓名与手机号,取的是司机档案的**当前**值
* (认证记录里的姓名是提交时的历史值,手机号则从来不在快照里)。
* 解析不到司机档案的记录,两个字段为空,不回落成提交时的值——
* 那会让「同一个人的多条记录显示成不同的人」。
*
* @param bizType 认证对象类型:driver-司机,vehicle-车辆
* @param bizKey 展示键模糊匹配(提交时的司机姓名 / 车牌号),可为空
* @param driverName 司机姓名模糊匹配(当前姓名,并兼顾提交时的旧名),可为空
* @param driverMobile 司机手机号模糊匹配,可为空
* @param status 审核状态:0审核中 1已通过 2已驳回 3已作废;为空时取 0(审核中)
* @param page 分页参数(由 Controller 用 Condition.getPage 构造)
* @return 认证记录分页
*/
IPage<TransportCertRecordVO> pendingPage(String bizType, String bizKey, String driverName, String driverMobile,
Integer status, IPage<TransportCertRecordVO> page);
/**
* 审核详情:同时返回待审快照与当前生效值,供前端做 diff
*
* @param recordId 认证记录主键
* @return 审核详情
*/
CertAuditDetailVO detail(Long recordId);
/**
* 审核一条认证记录
*
* @param recordId 认证记录主键
* @param pass true 通过,false 驳回
* @param rejectReason 驳回原因;驳回时必填,≤200 字
* @return 是否成功
*/
boolean audit(Long recordId, boolean pass, String rejectReason);
}
@@ -64,4 +64,12 @@ public interface IContractManageService extends BaseService<ContractManage> {
Map<String, Long> expireStats(ContractManageVO contractManage);
List<ContractManageExcel> exportContractManage(ContractManageVO contractManage, String ids);
/**
* 预览下一合同编号(按合同负责部门前缀 + 年份 + 三位流水)
*
* @param organizationId 合同负责部门ID
* @return 预览编号
*/
String nextContractNo(Long organizationId);
}
@@ -22,6 +22,7 @@
*/
package org.springblade.transport.service;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.vo.DriverVehicleCardVO;
import org.springblade.transport.pojo.vo.DriverVO;
@@ -44,4 +45,15 @@ public interface IDriverAppService {
*/
List<DriverVehicleCardVO> myVehicles();
/**
* 当前登录用户对应的司机**实体**
* <p>
* 认证提交要写回司机表,且越权校验要读 {@code driving_vehicle},
* 故这里返回实体而非视图对象。解析顺序:先按登录手机号匹配,
* 匹配不到再按 {@code user_id} 兜底。
*
* @return 司机实体;未登录或查不到时返回 null
*/
Driver currentDriverEntity();
}
@@ -0,0 +1,111 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service;
import org.springblade.transport.pojo.dto.PersonCertSubmitRequest;
import org.springblade.transport.pojo.dto.VehicleCertSubmitRequest;
import org.springblade.transport.pojo.vo.TransportCertRecordVO;
import java.util.List;
/**
* 司机端认证服务(小程序)
* <p>
* 认证对象是「一事一议」的:司机的个人认证与每一辆车的车辆认证各自独立成立、
* 独立审核、独立留痕。本服务只负责司机端能做什么,管理端审核在审核服务里。
*
* @author Chill
*/
public interface IDriverCertService {
/**
* 提交个人认证(全量:身份证 + 驾驶证 + 从业资格证,三证一起审)
* <p>
* 生成一条待审记录,并把本人前一条待审记录置为已作废。
* **不修改生效快照**:已认证的司机在本次审核通过前仍按旧生效值认定。
*
* @param request 个人认证内容
* @return 新生成的认证记录主键
*/
Long submitPersonCert(PersonCertSubmitRequest request);
/**
* 读取本人当前生效的个人认证内容(供个人认证卡片展示)
*
* @return 生效快照内容;从未认证通过时返回 null
*/
PersonCertSubmitRequest getEffectivePersonCert();
/**
* 提交车辆认证(新增或重新认证)
* <p>
* 生成一条待审记录,并把该车前一条待审记录置为已作废。
* **不修改生效快照**:已认证的车辆在本次审核通过前仍按旧生效值认定,
* 因此提交审核不会影响车辆正常接单。
*
* @param request 车辆认证内容
* @return 新生成的认证记录主键
*/
Long submitVehicleCert(VehicleCertSubmitRequest request);
/**
* 删除车辆认证(按逻辑删除处理)
* <p>
* 只把车辆标记为已删除,**认证记录一律保留**——记录是不可变快照,
* 也是「曾通过即永久可用」这一判定的依据,物理删除会让历史无从追溯。
* 只能删除本人绑定的车辆,越权拒绝。
*
* @param vehicleId 车辆主键
* @return 是否成功
*/
boolean deleteVehicleCert(Long vehicleId);
/**
* 读取某辆车当前生效的认证内容
*
* @param vehicleId 车辆主键
* @return 生效快照内容;从未认证通过时返回 null
*/
VehicleCertSubmitRequest getEffectiveVehicleCert(Long vehicleId);
/**
* 查询某认证对象的认证记录列表(按提交时间倒序)
*
* @param bizType 认证对象类型:driver-司机,vehicle-车辆
* @param bizId 认证对象主键
* @return 认证记录列表
*/
List<TransportCertRecordVO> listRecords(String bizType, Long bizId);
/**
* 查询单条认证记录详情
*
* @param recordId 认证记录主键
* @return 认证记录;不存在或无权查看时抛业务异常
*/
TransportCertRecordVO getRecord(Long recordId);
}
@@ -30,6 +30,9 @@ import org.springblade.transport.pojo.vo.DriverNodePunchVO;
import org.springblade.transport.pojo.vo.DriverWaybillCardVO;
import org.springblade.transport.pojo.vo.DriverWaybillPreviewVO;
import org.springblade.transport.pojo.vo.DriverWaybillTabCountsVO;
import org.springblade.transport.pojo.vo.WaybillTrackVO;
import java.util.Map;
/**
* 司机端运单服务(小程序首页 / 列表)
@@ -68,6 +71,19 @@ public interface IDriverWaybillService {
*/
DriverWaybillCardVO detail(Long id);
/**
* 司机端运单行驶轨迹:按打卡记录 / 计划时间自动推导起止日期后查询 LBS 轨迹。
* <p>
* 起止日期优先级:
* <ol>
* <li>有打卡且多于 1 条:首条打卡日 → 末条打卡日</li>
* <li>有打卡且仅 1 条:首条打卡日 → 今天</li>
* <li>无打卡有计划开始日:计划开始日 → 今天</li>
* <li>无打卡无计划:3 天前 → 今天</li>
* </ol>
*/
WaybillTrackVO track(Long id);
/**
* 按运单ID组装详情打卡数据(punchNodes / enrouteRecords),不校验当前登录人是否为该司机。
* 供调度端 manage/detail 复用。
@@ -99,4 +115,26 @@ public interface IDriverWaybillService {
*/
boolean complete(Long id);
/**
* 按运单与节点 key 取该运单实际生效的过程节点配置;节点不存在时返回 null。
* <p>
* 运单优先用项目启用中的过程配置,不可用时回退运单自身快照。
*/
Map<String, Object> findPunchNode(Long waybillId, String nodeCode);
/**
* 该运单的指定节点当前是否存在打卡流水;节点编码忽略大小写。
* <p>
* 以打卡流水为准而非运单当前节点:管理端取消打卡后这里要立刻回到未打卡。
*/
boolean isNodePunched(Long waybillId, String nodeCode);
/**
* 断言当前登录司机对该运单有归属:运单车牌须落在司机档案自述绑定的车牌内。
* <p>
* 无司机档案、未绑定车辆、车牌不匹配都会抛异常。管理端与调度员账号没有司机档案,
* 会停在「未找到当前登录司机档案」——这是刻意的,不提供代报。
*/
void assertCurrentDriverOwnsWaybill(Long waybillId);
}

Some files were not shown because too many files have changed in this diff Show More