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
+82
View File
@@ -0,0 +1,82 @@
# TMS(运输业务中台)—— 业财集成语境
TMS 与集团财务系统(金蝶星瀚)、合同合规系统(HCS)集成,单据在三个系统间流转。
本表只收录集成语境下易混淆的业务术语,不含实现细节。
## Language
**合同合规系统(HCS)**:
集团合同合规管控系统(桂物数字合同合规),接收 TMS 推送的项目、合同、履约与付款数据并做合规审批。
_Avoid_: 桂物系统、合同系统、数字合同系统
**相对方**:
HCS 对签约主体(甲乙方)的称谓,与 TMS 的"客商"同指一个外部单位。
_Avoid_: 在 HCS 集成语境用"客户/供应商"
**来源单据键**:
TMS 单据在 HCS 侧的对账主键(接口字段 `fd_kingdee_id`,字段名沿用金蝶),统一存 TMS 业务单据主键。
_Avoid_: 外部ID、kingdeeId(泛指金蝶单据号时易混淆)
**来源标识**:
TMS 推送给 HCS 时声明自身身份的常量(接口字段 `fd_sources`,约定值 "TMS")。
_Avoid_: XH(那是星瀚侧的来源标识)
**履约规则**:
合同维度的资金计划阶段(付款/收款、款项性质、计划金额、计划日期、开票规则),由 TMS 维护并同步到 HCS。
_Avoid_: 履约计划(旧版已废弃接口的叫法)、付款计划
**履约阶段**:
履约规则表中的一行;HCS 为其生成阶段ID,后续更新/删除必须凭该ID。
_Avoid_: 阶段项、plan 行
**履约记录**:
付款/收款执行事实在 HCS 的登记(一笔付款申请一条记录,随实际付款推进更新),与履约规则是"计划 vs 执行"的关系。
_Avoid_: 履约流水
**额度校验**:
付款提交时由 HCS 判定申请金额是否超出合同/项目剩余额度的同步检查;超额不阻断提交,转入特批链路。
_Avoid_: 余额检查
**特批流程**:
额度超额(或风险不通过)时在 HCS 发起的审批流程;TMS 持有其流程ID,流程结束由 HCS 经 B103 回传结果。
_Avoid_: 特殊审批、绿色通道
**特批放行**:
特批结果为"通过"后对**本次**付款申请解除推送拦截的动作;只改特批状态,不触发推送,单次有效。
_Avoid_: 特批通过自动付款、放行推送
**锁单(B901)**:
金蝶确认接收 TMS 推送的单据后,经 B901 通知 TMS 将本地源单据置为不可编辑、不可作废、不可重推的状态;金蝶反审核及删除后经 B901 解锁。
_Avoid_: 冻结、金蝶锁定(那是金蝶侧单据自己的状态)
**解锁**:
B901 通知金蝶侧单据已反审核及删除后,TMS 解除锁单、同步状态置失败、清金蝶单号留痕;不自动重推,人工核实后重新推送。
_Avoid_: 反审核(那是金蝶侧的动作)、自动重推
**合规附件ID**:
TMS 附件先上传 HCS 附件服务换取的 fdId;同一附件以 TMS 附件元数据为键缓存映射,不重复上传。
_Avoid_: 附件fdId(指代不明)
**同步状态**:
TMS 单据对 HCS 的推送生命周期(待同步/已同步/失败),权威状态在 TMS 侧字段,HCS 不回写业务状态。
_Avoid_: 推送标志
**二次认领**:
金蝶司库完成第一轮自动认领(客户维度)后,TMS 把收款流水认领到合同/结算单维度的动作;只在 TMS 本地登记,本身不产生金蝶收款单。
_Avoid_: 收款认领(泛指整条链路时易与金蝶第一轮自动认领混淆)
**公告未认领金额**:
金蝶认领公告(A301-2)上尚未被认领的金额(字段 unclaimamount),是 TMS 二次认领的上限和"认领完成"的判定基准;以金蝶侧为准,推送前须重查最新值。
_Avoid_: 剩余可认领金额(那是本地展示口径)、未认领金额(不指明公告侧/本地侧时指代不明)
**认领完成**:
一条收款流水的累计有效认领金额等于公告未认领金额的状态;只有认领完成的流水才满足推送金蝶的条件,不认领完就不推。
_Avoid_: 认领完毕、全部认领(未指明对照基准)
**认领状态**:
收款流水本地认领进度(未认领/部分认领/认领完成),对照公告未认领金额计算;作废的认领分摊不计入累计。
_Avoid_: 与推送状态混用
**推送状态**:
收款流水对金蝶 A301-3 的推送生命周期(未推送/已推送/推送失败),权威在流水级,认领主单仅冗余展示;已推送的流水不可二次推送。
_Avoid_: 金蝶状态(那是 A301-1 回写的单据生命周期:已推送/审核通过/已收款)
@@ -51,6 +51,7 @@ import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.system.feign.IAuthLockClient;
import org.springblade.system.feign.IAuthLogClient;
import org.springblade.core.tenant.BladeTenantProperties;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.feign.IUserClient;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -117,8 +118,8 @@ public class BladeAuthConfiguration {
}
@Bean
public OAuth2UserService oAuth2UserService(IUserClient userClient) {
return new BladeUserDetailService(userClient);
public OAuth2UserService oAuth2UserService(IUserClient userClient, IMpUserClient mpUserClient) {
return new BladeUserDetailService(userClient, mpUserClient);
}
@Primary
@@ -33,6 +33,8 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.feign.ISysClient;
import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springblade.system.utils.MpUserAuth;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
@@ -72,7 +74,12 @@ public class OAuth2UserInfoResponseAdvice implements ResponseBodyAdvice<Object>
if (userInfo == null) {
return body;
}
userInfo.setPermission(loadPermission(bladeUser.getRoleId()));
if (MpUserAuth.isMpUser(bladeUser)) {
// 小程序用户:按身份直接生成权限,不走系统角色菜单
userInfo.setPermission(MpUserIdentity.toPermissions(bladeUser.getRoleName()));
} else {
userInfo.setPermission(loadPermission(bladeUser.getRoleId()));
}
return userInfo;
}
@@ -88,7 +88,8 @@ public class Oauth2SmsEndpoint {
return R.fail(USER_PHONE_NOT_FOUND);
}
// 校验手机是否已注册,防止恶意发送验证码
OAuth2Request request = OAuth2Request.create();
// buildArgs:带上 User-Type 等请求头,小程序传 User-Type=mp 时查 blade_mp_user
OAuth2Request request = OAuth2Request.create().buildArgs();
request.setTenantId(tenantId);
OAuth2User oAuth2User = userService.loadByPhone(decryptedPhone, request);
if (oAuth2User == null) {
@@ -2,10 +2,12 @@ package org.springblade.auth.granter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springblade.auth.utils.TokenUtil;
import org.springblade.core.oauth2.exception.UserInvalidException;
import org.springblade.core.oauth2.granter.AbstractTokenGranter;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
@@ -13,7 +15,8 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springblade.system.feign.IUserClient;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.thirdparty.wechat.constant.WechatMiniConstant;
import org.springblade.thirdparty.wechat.exception.WechatMiniException;
import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO;
@@ -25,7 +28,8 @@ import org.springframework.stereotype.Component;
* 微信小程序手机号一键登录。
* <p>
* grant_type=wechat_applet,参数:loginCode(wx.login)、phoneCode(getPhoneNumber)。
* 流程:换 openid + 手机号 → 按手机号查用户(不存在则拒绝)→ 记录 openid → 发令牌(与密码登录一致)。
* 流程:换 openid + 手机号 → 按手机号查小程序用户(blade_mp_user,不存在则拒绝)
* → 记录 openid → 按身份发令牌(不依赖系统角色)。
*/
@Slf4j
@Component
@@ -33,17 +37,17 @@ public class WechatMiniTokenGranter extends AbstractTokenGranter {
private final OAuth2UserService userService;
private final IWechatMiniService wechatMiniService;
private final IUserClient userClient;
private final IMpUserClient mpUserClient;
public WechatMiniTokenGranter(OAuth2ClientService clientService,
OAuth2UserService userService,
PasswordHandler passwordHandler,
IWechatMiniService wechatMiniService,
IUserClient userClient) {
IMpUserClient mpUserClient) {
super(clientService, userService, passwordHandler);
this.userService = userService;
this.wechatMiniService = wechatMiniService;
this.userClient = userClient;
this.mpUserClient = mpUserClient;
}
@Override
@@ -54,7 +58,7 @@ public class WechatMiniTokenGranter extends AbstractTokenGranter {
@Override
public OAuth2User user(OAuth2Request request) {
// 先校验客户端是否允许该授权类型,避免先调微信再因客户端配置失败
var oauthClient = client(request);
OAuth2Client oauthClient = client(request);
HttpServletRequest httpRequest = WebUtil.getRequest();
String loginCode = httpRequest.getParameter("loginCode");
@@ -78,16 +82,21 @@ public class WechatMiniTokenGranter extends AbstractTokenGranter {
throw new UserInvalidException("未获取到微信手机号");
}
OAuth2User user = userService.loadByPhone(phone, request);
if (!userService.validateUser(user)) {
R<MpUserInfo> mpUserResult = mpUserClient.userInfoByPhone(request.getTenantId(), phone);
if (mpUserResult == null || !mpUserResult.isSuccess() || mpUserResult.getData() == null
|| mpUserResult.getData().getUser() == null) {
throw new UserInvalidException("用户不存在,无法登录");
}
R<Boolean> bindResult = userClient.bindWxMiniOpenId(
OAuth2User user = TokenUtil.convertMpUser(mpUserResult.getData());
if (!userService.validateUser(user)) {
throw new UserInvalidException("用户身份无效,无法登录");
}
R<Boolean> bindResult = mpUserClient.bindOpenId(
request.getTenantId(),
Func.toLong(user.getUserId()),
session.getOpenid(),
phone
session.getOpenid()
);
if (bindResult == null || !bindResult.isSuccess()) {
String msg = bindResult != null ? bindResult.getMsg() : "绑定 openid 失败";
@@ -18,9 +18,6 @@
* 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.auth.service;
@@ -35,8 +32,11 @@ import org.springblade.core.oauth2.utils.OAuth2ExceptionUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.feign.IUserClient;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.entity.UserInfo;
import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springblade.system.pojo.enums.UserType;
import java.util.Optional;
@@ -49,63 +49,121 @@ import java.util.Optional;
@RequiredArgsConstructor
public class BladeUserDetailService implements OAuth2UserService {
private final IUserClient userClient;
private final IMpUserClient mpUserClient;
@Override
public OAuth2User loadByUserId(String userId, OAuth2Request request) {
// 获取用户参数
String userType = Optional.ofNullable(request.getUserType())
.filter(s -> !StringUtil.isBlank(s))
.orElse(UserType.WEB.getName());
// 获取用户信息
R<UserInfo> result = userClient.userInfo(Func.toLong(userId), userType);
if (!result.isSuccess()) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
// 小程序刷新/鉴权:User-Type=mp 时只查 blade_mp_user
if (isMpLoginRequest(request)) {
OAuth2User mpUser = loadMpUserById(userId);
if (mpUser == null) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
}
return mpUser;
}
// 构建oauth2用户信息
return TokenUtil.convertUser(result.getData(), request);
String userType = resolveUserType(request);
R<UserInfo> result = userClient.userInfo(Func.toLong(userId), userType);
if (result != null && result.isSuccess() && result.getData() != null
&& result.getData().getUser() != null) {
return TokenUtil.convertUser(result.getData(), request);
}
// 兼容:刷新令牌未带 User-Type=mp 时,回落查询小程序用户
OAuth2User mpUser = loadMpUserById(userId);
if (mpUser != null) {
return mpUser;
}
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
return null;
}
@Override
public OAuth2User loadByUsername(String username, OAuth2Request request) {
// 获取用户参数
String userType = Optional.ofNullable(request.getUserType())
.filter(s -> !StringUtil.isBlank(s))
.orElse(UserType.WEB.getName());
String tenantId = request.getTenantId();
// 获取用户信息
// 小程序账号密码 / 图形验证码登录:仅查 blade_mp_user
if (isMpLoginRequest(request)) {
R<MpUserInfo> mpResult = mpUserClient.userInfoByAccount(tenantId, username);
if (mpResult == null || !mpResult.isSuccess() || mpResult.getData() == null) {
return null;
}
return TokenUtil.convertMpUser(mpResult.getData());
}
String userType = resolveUserType(request);
R<UserInfo> result = userClient.userInfo(tenantId, username, userType);
if (!result.isSuccess()) {
if (result == null || !result.isSuccess()) {
return null;
}
// 构建oauth2用户信息
return TokenUtil.convertUser(result.getData(), request);
}
@Override
public OAuth2User loadByPhone(String phone, OAuth2Request request) {
// 获取用户参数
String userType = Optional.ofNullable(request.getUserType())
.filter(s -> !StringUtil.isBlank(s))
.orElse(UserType.WEB.getName());
String tenantId = request.getTenantId();
// 获取用户信息
R<UserInfo> result = userClient.userInfoByPhone(tenantId, phone, userType);
if (!result.isSuccess()) {
return null;
// 小程序短信登录:仅查 blade_mp_user
if (isMpLoginRequest(request)) {
return loadMpUserByPhone(tenantId, phone);
}
// 构建oauth2用户信息
return TokenUtil.convertUser(result.getData(), request);
String userType = resolveUserType(request);
R<UserInfo> result = userClient.userInfoByPhone(tenantId, phone, userType);
if (result != null && result.isSuccess() && result.getData() != null) {
return TokenUtil.convertUser(result.getData(), request);
}
// 发短信校验等未带 User-Type 时,回落小程序用户,避免 mp 账号收不到验证码
return loadMpUserByPhone(tenantId, phone);
}
@Override
public boolean validateUser(OAuth2User user) {
return Optional.ofNullable(user)
.filter(u -> u.getUserId() != null && !u.getUserId().isEmpty()) // 检查userId不为空
.filter(u -> u.getAuthorities() != null && !u.getAuthorities().isEmpty()) // 检查authorities不为空
.isPresent(); // 如果上述条件都满足,则返回true,否则返回false
.filter(u -> u.getUserId() != null && !u.getUserId().isEmpty())
.filter(u -> u.getAuthorities() != null && !u.getAuthorities().isEmpty())
.isPresent();
}
/**
* 是否小程序登录:请求头 User-Type=mp(Web 与小程序共用 saber3 客户端,不能按 clientId 分流)
*/
private boolean isMpLoginRequest(OAuth2Request request) {
return request != null && isMpUserType(request.getUserType());
}
private boolean isMpUserType(String userType) {
return MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(Func.toStr(userType));
}
private String resolveUserType(OAuth2Request request) {
return Optional.ofNullable(request.getUserType())
.filter(s -> !StringUtil.isBlank(s))
.orElse(UserType.WEB.getName());
}
private OAuth2User loadMpUserById(String userId) {
if (Func.isBlank(userId)) {
return null;
}
try {
R<MpUserInfo> result = mpUserClient.userInfoById(Func.toLong(userId));
if (result != null && result.isSuccess() && result.getData() != null
&& result.getData().getUser() != null) {
return TokenUtil.convertMpUser(result.getData());
}
} catch (Exception exception) {
// 非小程序用户场景忽略
}
return null;
}
private OAuth2User loadMpUserByPhone(String tenantId, String phone) {
R<MpUserInfo> result = mpUserClient.userInfoByPhone(tenantId, phone);
if (result == null || !result.isSuccess() || result.getData() == null) {
return null;
}
return TokenUtil.convertMpUser(result.getData());
}
}
@@ -28,12 +28,15 @@ package org.springblade.auth.utils;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.impl.OAuth2UserDetail;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.pojo.entity.UserInfo;
import java.util.Collections;
import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springblade.system.utils.MpUserAuth;
/**
* 认证工具类
@@ -75,6 +78,7 @@ public class TokenUtil {
userDetail.setRealName(user.getRealName());
userDetail.setAccount(user.getAccount());
userDetail.setPassword(user.getPassword());
userDetail.setPhone(user.getPhone());
userDetail.setDeptId(user.getDeptId());
userDetail.setPostId(user.getPostId());
userDetail.setRoleId(user.getRoleId());
@@ -85,4 +89,40 @@ public class TokenUtil {
return userDetail;
}
/**
* 小程序用户转换为OAuth2标准用户(身份直接写入 authorities,不走系统角色)
*
* @param userInfo 小程序用户信息
* @return OAuth2User
*/
public static OAuth2User convertMpUser(MpUserInfo userInfo) {
if (userInfo == null || userInfo.getUser() == null) {
return null;
}
MpUser user = userInfo.getUser();
OAuth2UserDetail userDetail = new OAuth2UserDetail();
userDetail.setUserId(String.valueOf(user.getId()));
userDetail.setTenantId(user.getTenantId());
userDetail.setName(user.getName());
userDetail.setRealName(user.getRealName());
userDetail.setAccount(user.getAccount());
userDetail.setPassword(user.getPassword());
userDetail.setPhone(user.getPhone());
// 严格令牌模式要求 deptId/roleId 非空且 >=0;小程序用户用占位值,身份写在 roleName/authorities
userDetail.setDeptId(Func.toStr(user.getDeptId(), MpUserAuth.PLACEHOLDER_ID));
if (Func.isBlank(userDetail.getDeptId()) || "-1".equals(userDetail.getDeptId())) {
userDetail.setDeptId(MpUserAuth.PLACEHOLDER_ID);
}
userDetail.setPostId(Func.toStr(user.getPostId(), MpUserAuth.PLACEHOLDER_ID));
userDetail.setRoleId(MpUserAuth.PLACEHOLDER_ID);
userDetail.setRoleName(Func.join(userInfo.getIdentities()));
userDetail.setAvatar(user.getAvatar());
userDetail.setAuthorities(userInfo.getIdentities());
userDetail.setPermissions(userInfo.getPermissions());
userDetail.setDetail(userInfo.getDetail() == null
? Kv.create().set("type", MpUserIdentity.DETAIL_TYPE)
: userInfo.getDetail());
return userDetail;
}
}
@@ -0,0 +1,93 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.common.excel;
import org.springblade.core.log.exception.ServiceException;
import java.io.Serial;
/**
* 单元格读取失败异常 —— 用于「异常不是 fastexcel 抛出、但同样发生在读取阶段」的场景。
* <p>
* fastexcel 自身解析失败时抛 {@code ExcelDataConvertException},位置信息齐全,
* {@link ImportFailureExcelUtil#exportReadFailure} 可以直接定位到行与列。但有些模块
* 在读取阶段有自己的解析逻辑(例如运输对账的动态费用列:模板列不固定,由
* {@code VehicleReconciliationFeeReader} 自行按表头解析),这些代码如果只是
* {@code catch (NumberFormatException)} 后返回 {@code BigDecimal.ZERO},
* 脏数据就会静默入库(费用 0.00 还会参与结算金额与自动匹配)。
* <p>
* 正确做法是改抛本异常,把位置信息补齐,让导出工具能把原因挂到正确的行、标红正确的列。
* <p>
* 本类位于 blade-common,供 blade-system 与 blade-transport 等模块共用。
*
* @author Chill
*/
public class ImportCellReadException extends ServiceException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 出错的单元格所在的 Excel 行号,<b>绝对 0 基</b>(表头占 0,第一个数据行为 1)。
* 与 {@code ExcelDataConvertException.getRowIndex()} 保持同一口径,
* 便于导出工具统一换算。
*/
private final transient Integer rowIndex;
/**
* 出错的列名(导入模板表头原文),会写进原因文案里,导出工具靠它标红对应列。
*/
private final transient String columnName;
public ImportCellReadException(Integer rowIndex, String columnName, String cellValue) {
super(buildMessage(rowIndex, columnName, cellValue));
this.rowIndex = rowIndex;
this.columnName = columnName;
}
/**
* 文案格式与 {@link ImportFailureExcelUtil#describeReadFailure} 保持一致:
* 「第N行「列名」格式不正确:<原值>」。列名必须包在「」里,导出工具才能标红该列。
*/
private static String buildMessage(Integer rowIndex, String columnName, String cellValue) {
String rowText = rowIndex == null ? "第?行" : "第" + (rowIndex + 1) + "行";
String columnText = columnName == null || columnName.isBlank() ? "该列" : "「" + columnName + "」";
StringBuilder message = new StringBuilder(rowText).append(columnText).append("格式不正确");
if (cellValue != null && !cellValue.isBlank()) {
message.append(":").append(cellValue);
}
return message.toString();
}
public Integer getRowIndex() {
return rowIndex;
}
public String getColumnName() {
return columnName;
}
}
@@ -45,7 +45,9 @@ import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -80,6 +82,443 @@ public class ImportFailureExcelUtil {
addValidationError(validationErrors, value != null && value.length() > maxLength, message);
}
/**
* 把「读取 Excel 阶段」的转换异常转成可读的失败原因。
* <p>
* 背景:单元格里填了无法转换的内容(如金额列填「一万二千元」)时,EasyExcel 会在
* {@code ExcelUtil.read(...)} 阶段就抛 {@link cn.idev.excel.exception.ExcelDataConvertException},
* 根本走不到行级校验;若不兜底,用户看到的是
* 「Convert data ... to class java.math.BigDecimal error」这类技术文案。
* <p>
* 本方法据异常里的行列位置与目标类型,生成「第N行「列名」格式不正确:<原值>」这样
* 用户能看懂、能定位的文案;列名靠表头反射拿 {@code @ExcelProperty},拿不到则退回列序号。
*
* @param exception 读取阶段抛出的异常
* @param excelClass 导入模型类(用于按列序号反查表头名)
* @return 可读的失败原因;非转换类异常返回 null(交由上层按原样处理)
*/
public static String describeReadFailure(Throwable exception, Class<?> excelClass) {
cn.idev.excel.exception.ExcelDataConvertException convertException = findCause(exception,
cn.idev.excel.exception.ExcelDataConvertException.class);
if (convertException == null) {
// 不是 fastexcel 抛的转换异常:模块自带的读取期解析逻辑(如运输对账的动态费用列)
// 用 ImportCellReadException 补齐了位置信息,直接透传它已经拼好的原因文案。
ImportCellReadException cellReadException = findCause(exception, ImportCellReadException.class);
return cellReadException == null ? null : cellReadException.getMessage();
}
Integer rowIndex = convertException.getRowIndex();
Integer columnIndex = convertException.getColumnIndex();
String columnName = columnName(excelClass, columnIndex);
// Excel 行号:getRowIndex() 是绝对 0 基行号(表头占 0),故 Excel 行号 = rowIndex + 1
String rowText = rowIndex == null ? "第?行" : "第" + (rowIndex + 1) + "行";
String columnText = columnName == null ? ("第" + (columnIndex == null ? "?" : columnIndex + 1) + "列") : "「" + columnName + "」";
String value = cellValue(convertException);
StringBuilder message = new StringBuilder(rowText).append(columnText).append("格式不正确");
if (value != null && !value.isBlank()) {
message.append(":").append(value);
}
return message.toString();
}
/**
* 在异常链里查找指定类型的 cause
*/
private static <T extends Throwable> T findCause(Throwable throwable, Class<T> type) {
for (Throwable current = throwable; current != null; current = current.getCause()) {
if (type.isInstance(current)) {
return type.cast(current);
}
if (current.getCause() == current) {
break;
}
}
return null;
}
/**
* 按列序号反查表头名(@ExcelProperty 的 value,取不到则用字段名)
*/
private static String columnName(Class<?> excelClass, Integer columnIndex) {
if (excelClass == null || columnIndex == null) {
return null;
}
List<Field> fields = excelFields(excelClass);
if (columnIndex < 0 || columnIndex >= fields.size()) {
return null;
}
Field field = fields.get(columnIndex);
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
if (excelProperty == null) {
return field.getName();
}
String[] value = excelProperty.value();
String columnName = value.length == 0 ? field.getName() : value[0];
return columnName.replace("*", "");
}
/**
* 取触发转换失败的单元格原值
*/
private static String cellValue(cn.idev.excel.exception.ExcelDataConvertException exception) {
try {
cn.idev.excel.metadata.data.CellData<?> cellData = exception.getCellData();
if (cellData == null) {
return null;
}
Object data = cellData.getData();
if (data == null) {
data = cellData.getStringValue();
}
return data == null ? null : String.valueOf(data).trim();
} catch (Exception ignored) {
return null;
}
}
/**
* 以「原始单元格文本」方式重读导入文件,得到的行用于构造失败明细。
* <p>
* 优先使用 Excel 文件真实表头(含动态费用列等模型类未声明的列),保证失败明细
* 与原导入模板列一致,用户修正后可直接用该文件重新导入。
* <p>
* 用途:当 {@code ExcelUtil.read(file, XxxExcel.class)} 因单元格无法转换而失败时,
* 已解析的实体列表是不完整的(异常中断了整批读取)。此时用本方法重读一次:
* 不做类型转换(直接读成 Map),因此绝不会再抛转换异常,可拿到**原表全部行**,
* 供导出「原表全量 + 仅出错行带原因」的失败明细。
*
* @param file 上传的 Excel 文件
* @param excelClass 导入模型类(表头缺失时按列序号反查表头名)
* @return 按行组织的明细数据:每行是「表头名 → 单元格文本」的有序 Map;
* 读不出内容时返回空列表(调用方按空明细处理)
*/
public static List<Map<String, String>> readRawRows(java.io.InputStream file, Class<?> excelClass) {
List<Map<Integer, String>> rawRows = new ArrayList<>();
Map<Integer, String> headerByIndex = new LinkedHashMap<>();
try {
cn.idev.excel.read.builder.ExcelReaderBuilder builder = cn.idev.excel.FastExcel
.read(file, new cn.idev.excel.event.AnalysisEventListener<Map<Integer, String>>() {
@Override
public void invokeHeadMap(Map<Integer, String> headMap, cn.idev.excel.context.AnalysisContext context) {
if (headMap == null) {
return;
}
headMap.forEach((index, header) -> {
if (index == null) {
return;
}
String name = header == null ? "" : header.trim();
// 历史失败明细再导入时忽略旧的失败原因列,导出时重新追加
if (!name.isEmpty() && !FAILURE_REASON.equals(name)) {
headerByIndex.put(index, name);
}
});
}
@Override
public void invoke(Map<Integer, String> data, cn.idev.excel.context.AnalysisContext context) {
rawRows.add(data == null ? new LinkedHashMap<>() : new LinkedHashMap<>(data));
}
@Override
public void doAfterAllAnalysed(cn.idev.excel.context.AnalysisContext context) {
// 无需处理
}
});
builder.headRowNumber(1).sheet().doRead();
} catch (Exception ignored) {
// 重读本身失败时退回空明细,由调用方按「无法读取」提示
return new ArrayList<>();
}
List<Map<String, String>> rows = new ArrayList<>();
for (Map<Integer, String> rawRow : rawRows) {
Map<String, String> row = new LinkedHashMap<>();
Set<Integer> columnIndexes = new LinkedHashSet<>(headerByIndex.keySet());
if (rawRow != null) {
columnIndexes.addAll(rawRow.keySet());
}
List<Integer> sortedIndexes = new ArrayList<>(columnIndexes);
Collections.sort(sortedIndexes);
for (Integer columnIndex : sortedIndexes) {
if (columnIndex == null) {
continue;
}
String columnName = headerByIndex.get(columnIndex);
if (columnName == null || columnName.isBlank()) {
columnName = columnName(excelClass, columnIndex);
}
if (columnName == null || columnName.isBlank()) {
columnName = "第" + (columnIndex + 1) + "列";
}
if (FAILURE_REASON.equals(columnName)) {
continue;
}
row.put(columnName, rawRow == null ? null : rawRow.get(columnIndex));
}
rows.add(row);
}
return rows;
}
/**
* 按原上传文件真实表头导出失败明细(含动态列),并追加「导入失败原因」。
* <p>
* 适用于运输对账等「模板列 = 固定列 + 动态费用列」场景:按实体字段反射导出
* 会丢掉动态列,用户无法用失败文件修正后重新导入。本方法重读原文件全量列,
* 再按 failureList 行序挂上失败原因。
*
* @param response 响应
* @param fileName 文件名
* @param sheetName 工作表名
* @param file 原导入文件
* @param failureList 校验失败列表(与原表行序一致,成功行 errorMessage 可为空)
* @param excelClass 导入模型类(原文件重读失败时回退到按实体导出)
*/
public static void exportOriginalWithFailures(HttpServletResponse response, String fileName, String sheetName,
org.springframework.web.multipart.MultipartFile file,
List<?> failureList, Class<?> excelClass) {
List<Map<String, String>> rawRows = new ArrayList<>();
if (file != null && !file.isEmpty()) {
try (java.io.InputStream inputStream = file.getInputStream()) {
rawRows = readRawRows(inputStream, excelClass);
} catch (IOException ignored) {
rawRows = new ArrayList<>();
}
}
if (rawRows.isEmpty()) {
// 原文件读不出时回退实体导出,至少保证失败原因可下载
if (failureList != null && !failureList.isEmpty()) {
export(response, fileName, sheetName, failureList, excelClass);
}
return;
}
Map<Integer, String> failureMessageByRow = new HashMap<>();
if (failureList != null) {
for (int index = 0; index < failureList.size(); index++) {
Object message = errorMessage(failureList.get(index));
if (message != null && !String.valueOf(message).isBlank()) {
failureMessageByRow.put(index, String.valueOf(message));
}
}
}
exportRawFailure(response, fileName, sheetName, rawRows, failureMessageByRow);
}
/**
* 导出「读取阶段失败」明细:原表全量行 + 末尾「导入失败原因」列。
* <p>
* 与 {@link #export} 的区别:本方法直接按重读得到的「表头名 → 单元格文本」写出,
* 不依赖导入模型的字段反射,因此适用于「单元格无法转换为实体字段」的场景
* (此时实体列表不完整,无法走常规导出)。
*
* @param response 响应
* @param fileName 文件名
* @param sheetName 工作表名
* @param rawRows {@link #readRawRows} 得到的原表全部行
* @param failureMessageByRow 行号(0 基,对应 rawRows 下标)→ 该行失败原因;为 null 表示该行无错
*/
public static void exportRawFailure(HttpServletResponse response, String fileName, String sheetName,
List<Map<String, String>> rawRows, Map<Integer, String> failureMessageByRow) {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
response.setHeader("Content-disposition", "attachment;filename=" + encodeFileName + ".xlsx");
List<String> headers = new ArrayList<>();
for (Map<String, String> row : rawRows) {
for (String key : row.keySet()) {
if (!headers.contains(key)) {
headers.add(key);
}
}
}
headers.add(FAILURE_REASON);
List<List<String>> head = new ArrayList<>();
for (String header : headers) {
head.add(List.of(header));
}
List<List<Object>> rows = new ArrayList<>();
Map<Integer, Set<Integer>> redColumnsByRow = new HashMap<>();
List<List<String>> headerKeywords = new ArrayList<>();
for (int columnIndex = 0; columnIndex < headers.size() - 1; columnIndex++) {
headerKeywords.add(headerKeywords(headers.get(columnIndex)));
}
for (int rowIndex = 0; rowIndex < rawRows.size(); rowIndex++) {
Map<String, String> rawRow = rawRows.get(rowIndex);
List<Object> row = new ArrayList<>();
String failureMessage = failureMessageByRow == null ? null : failureMessageByRow.get(rowIndex);
for (int columnIndex = 0; columnIndex < headers.size() - 1; columnIndex++) {
row.add(rawRow.get(headers.get(columnIndex)));
}
row.add(failureMessage == null ? "" : failureMessage);
rows.add(row);
redColumnsByRow.put(rowIndex, resolveRawRedColumns(failureMessage, headerKeywords, headers.size() - 1));
}
try {
FastExcel.write(response.getOutputStream())
.registerWriteHandler(new RawFailureCellStyleHandler(rows, redColumnsByRow))
.head(head)
.sheet(sheetName)
.doWrite(rows);
} catch (IOException exception) {
throw new IllegalStateException("导出导入失败明细失败", exception);
}
}
/**
* 读取阶段失败明细的单元格样式处理器:统一列宽 + 标红失败原因列与出错列
*/
private static class RawFailureCellStyleHandler implements CellWriteHandler {
private final int failureReasonColumnIndex;
private final Map<Integer, Set<Integer>> redColumnsByRow;
private final Map<Short, CellStyle> redStyleCache = new HashMap<>();
private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>();
private final Map<Integer, Integer> columnWidthCache = new HashMap<>();
private RawFailureCellStyleHandler(List<List<Object>> rows, Map<Integer, Set<Integer>> redColumnsByRow) {
this.failureReasonColumnIndex = rows.isEmpty() ? 0 : rows.get(0).size() - 1;
this.redColumnsByRow = redColumnsByRow;
}
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
List<WriteCellData<?>> cellDataList, Cell cell, cn.idev.excel.metadata.Head head,
Integer relativeRowIndex, Boolean isHead) {
if (Boolean.TRUE.equals(isHead)) {
adjustColumnWidth(cell);
markNoWrap(cell);
return;
}
adjustColumnWidth(cell);
if (relativeRowIndex == null || relativeRowIndex < 0) {
return;
}
Set<Integer> redColumns = redColumnsByRow.get(relativeRowIndex);
if (redColumns != null && redColumns.contains(cell.getColumnIndex())) {
markRed(cell);
}
}
private void markRed(Cell cell) {
CellStyle currentStyle = cell.getCellStyle();
CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
Workbook workbook = cell.getSheet().getWorkbook();
CellStyle newStyle = workbook.createCellStyle();
newStyle.cloneStyleFrom(currentStyle);
Font font = workbook.createFont();
font.setColor(IndexedColors.RED.getIndex());
newStyle.setFont(font);
newStyle.setWrapText(true);
return newStyle;
});
cell.setCellStyle(redStyle);
}
private void markNoWrap(Cell cell) {
CellStyle currentStyle = cell.getCellStyle();
CellStyle noWrapStyle = noWrapStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
CellStyle newStyle = cell.getSheet().getWorkbook().createCellStyle();
newStyle.cloneStyleFrom(currentStyle);
newStyle.setWrapText(false);
return newStyle;
});
cell.setCellStyle(noWrapStyle);
}
private void adjustColumnWidth(Cell cell) {
int columnIndex = cell.getColumnIndex();
int columnWidth = Math.min(Math.max(displayWidth(cell.toString()) + 4, 12), 80) * 256;
Integer currentWidth = columnWidthCache.get(columnIndex);
if (currentWidth == null || columnWidth > currentWidth) {
columnWidthCache.put(columnIndex, columnWidth);
cell.getSheet().setColumnWidth(columnIndex, columnWidth);
}
}
private int displayWidth(String value) {
int width = 0;
for (int index = 0; index < value.length(); index++) {
width += value.charAt(index) > 255 ? 2 : 1;
}
return width;
}
}
/**
* 读取阶段异常的一站式兜底:重读原表 → 导出「全量行 + 出错行原因」失败明细。
* <p>
* 控制器用法:在导入的 try 块末尾追加
* <pre>
* } catch (Exception exception) {
* if (ImportFailureExcelUtil.exportReadFailure(response, fileName, sheetName, file, XxxExcel.class, exception)) {
* return null;
* }
* throw exception;
* }
* </pre>
*
* @return true 表示已识别为「单元格转换失败」并已写出明细(调用方直接 return null);
* false 表示不是转换类异常(调用方应继续抛出,保持原有处理)
*/
public static boolean exportReadFailure(HttpServletResponse response, String fileName, String sheetName,
org.springframework.web.multipart.MultipartFile file,
Class<?> excelClass, Throwable exception) {
String failureMessage = describeReadFailure(exception, excelClass);
if (failureMessage == null) {
return false;
}
List<Map<String, String>> rawRows;
try (java.io.InputStream inputStream = file.getInputStream()) {
rawRows = readRawRows(inputStream, excelClass);
} catch (IOException ioException) {
rawRows = new ArrayList<>();
}
Map<Integer, String> failureMessageByRow = new HashMap<>();
if (rawRows.isEmpty()) {
// 重读也拿不到内容时,退化为「仅失败原因」一行,至少让用户看到可读原因
Map<String, String> single = new java.util.LinkedHashMap<>();
single.put(FAILURE_REASON, failureMessage);
rawRows.add(single);
failureMessageByRow.put(0, failureMessage);
exportRawFailure(response, fileName, sheetName, rawRows, failureMessageByRow);
return true;
}
// 异常里的数据行下标(0 基)用于把原因挂到对应行
Integer rowIndex = readFailureRowIndex(exception);
int targetRow = rowIndex == null ? 0 : Math.max(0, Math.min(rowIndex, rawRows.size() - 1));
failureMessageByRow.put(targetRow, failureMessage);
exportRawFailure(response, fileName, sheetName, rawRows, failureMessageByRow);
return true;
}
/**
* 取读取阶段异常中「失败数据行」的下标(0 基,只对数据行计数),供挂失败原因使用。
* <p>
* 无论哪种异常,源信息都是<b>绝对 0 基行号</b>(表头占 0,第一个数据行为 1),
* 而这里要的下标是相对数据行的,故统一减 1。
* 不减的话原因会整体下移一行:错误恰好落在最后一行时会因调用处的 clamping
* 侥幸正确,落在首行/中间行就会标到不该标的行上,误导用户去改正确的那一行。
*/
private static Integer readFailureRowIndex(Throwable exception) {
Integer excelRowIndex = readFailureExcelRowIndex(exception);
return excelRowIndex == null ? null : Math.max(0, excelRowIndex - 1);
}
/**
* 取读取阶段异常的绝对 0 基行号(表头 = 0)。
* <p>
* 两条来源统一到这里:fastexcel 自带的 {@code ExcelDataConvertException},
* 以及模块自行解析时抛出的 {@link ImportCellReadException}。
*/
private static Integer readFailureExcelRowIndex(Throwable exception) {
cn.idev.excel.exception.ExcelDataConvertException convertException = findCause(exception,
cn.idev.excel.exception.ExcelDataConvertException.class);
if (convertException != null) {
return convertException.getRowIndex();
}
ImportCellReadException cellReadException = findCause(exception, ImportCellReadException.class);
return cellReadException == null ? null : cellReadException.getRowIndex();
}
private static final String FAILURE_REASON = "导入失败原因";
private static final String ERROR_MESSAGE_FIELD = "errorMessage";
private static final String FAILURE_REASON_FIELD = "failureReason";
@@ -247,6 +686,69 @@ public class ImportFailureExcelUtil {
.toList();
}
/**
* 按真实表头生成定位关键词(用于原表失败明细标红)
*/
private static List<String> headerKeywords(String header) {
if (header == null || header.isBlank()) {
return List.of();
}
List<String> keywords = new ArrayList<>();
keywords.add(header);
keywords.addAll(Arrays.asList(header.replace("*", "").split("[//、()()\\s]+")));
return keywords.stream()
.map(ImportFailureExcelUtil::normalize)
.filter(keyword -> keyword.length() >= 2)
.distinct()
.toList();
}
/**
* 按失败原因命中表头关键词,计算需要标红的列(含失败原因列)
*/
private static Set<Integer> resolveRawRedColumns(String failureMessage, List<List<String>> headerKeywords,
int failureReasonColumnIndex) {
Set<Integer> redColumns = new LinkedHashSet<>();
redColumns.add(failureReasonColumnIndex);
String normalizedReason = normalize(failureMessage);
if (normalizedReason.isEmpty()) {
return redColumns;
}
List<int[]> matches = new ArrayList<>();
for (int column = 0; column < headerKeywords.size(); column++) {
for (String keyword : headerKeywords.get(column)) {
int fromIndex = 0;
while (true) {
int index = normalizedReason.indexOf(keyword, fromIndex);
if (index < 0) {
break;
}
matches.add(new int[]{index, index + keyword.length(), column});
fromIndex = index + 1;
}
}
}
for (int[] match : matches) {
if (!isRawMatchSubsumed(match, matches)) {
redColumns.add(match[2]);
}
}
return redColumns;
}
private static boolean isRawMatchSubsumed(int[] match, List<int[]> matches) {
int matchLength = match[1] - match[0];
for (int[] other : matches) {
if (other[2] == match[2]) {
continue;
}
if ((other[1] - other[0]) > matchLength && other[0] <= match[0] && other[1] >= match[1]) {
return true;
}
}
return false;
}
private static class ImportFailureCellStyleHandler implements CellWriteHandler {
private final int failureReasonColumnIndex;
@@ -23,7 +23,7 @@
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.excel;
package org.springblade.common.excel;
import org.springblade.core.log.exception.ServiceException;
@@ -33,8 +33,14 @@ import java.util.List;
/**
* 导入失败异常,携带失败明细用于导出原表并标注错误。
* <p>
* 批量导入采用「全失败即整批回滚」语义:任一行校验失败都会抛出本异常触发事务回滚,
* 失败明细在抛异常前已收集完毕,因此回滚不影响明细的内容。
* 批量导入采用「任一行失败即整批回滚」语义:循环内先收集完全部错误明细,
* 再抛出本异常触发事务回滚。明细在抛异常前已收集完毕,因此回滚不影响明细内容。
* <p>
* 注意:仅标注 {@code @Transactional} 并不足够。若循环内把异常吞掉(catch 后
* 只往 errorList 塞数据并正常 return),方法会正常返回,Spring 会照常提交事务。
* 必须抛出异常才能让事务真正回滚。
* <p>
* 本类位于 blade-common,供 blade-system 与 blade-transport 等模块共用。
*
* @author Chill
*/
@@ -34,9 +34,13 @@ import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 自定义Feign日志记录器,只记录出入参信息
@@ -50,6 +54,7 @@ public class CustomFeignLogger extends Logger {
private static final String IMAGE_CONTENT_TYPE_PREFIX = "image/";
private static final String AUDIO_CONTENT_TYPE_PREFIX = "audio/";
private static final String VIDEO_CONTENT_TYPE_PREFIX = "video/";
private static final String MULTIPART_CONTENT_TYPE_PREFIX = "multipart/";
@Override
protected void log(String configKey, String format, Object... args) {
@@ -63,10 +68,13 @@ public class CustomFeignLogger extends Logger {
Map<String, Collection<String>> headers = request.headers();
byte[] body = request.body();
String contentType = getContentType(headers);
String bodyStr;
if (body == null || body.length == 0) {
bodyStr = "[无]";
} else if (isBinaryContent(getContentType(headers))) {
} else if (isMultipartContent(contentType)) {
bodyStr = parseMultipartBody(body, contentType);
} else if (isBinaryContent(contentType)) {
bodyStr = "[二进制数据,长度=" + body.length + "字节]";
} else {
bodyStr = new String(body, StandardCharsets.UTF_8);
@@ -160,4 +168,75 @@ public class CustomFeignLogger extends Logger {
|| lowerContentType.startsWith(AUDIO_CONTENT_TYPE_PREFIX)
|| lowerContentType.startsWith(VIDEO_CONTENT_TYPE_PREFIX);
}
/**
* 判断是否为multipart内容
*/
private boolean isMultipartContent(String contentType) {
return contentType != null && contentType.toLowerCase().startsWith(MULTIPART_CONTENT_TYPE_PREFIX);
}
/**
* 解析multipart请求体,文件部分用占位符替代,文本字段保留打印
*/
private String parseMultipartBody(byte[] body, String contentType) {
String boundary = extractBoundary(contentType);
if (boundary == null) {
return "[multipart数据,长度=" + body.length + "字节]";
}
String bodyStr = new String(body, StandardCharsets.UTF_8);
String delimiter = "--" + boundary;
String[] parts = bodyStr.split(delimiter);
List<String> fields = new ArrayList<>();
for (String part : parts) {
if (part.isEmpty() || part.startsWith("--") || part.trim().isEmpty()) {
continue;
}
int headerEnd = part.indexOf("\r\n\r\n");
if (headerEnd < 0) {
continue;
}
String headers = part.substring(0, headerEnd);
String value = part.substring(headerEnd + 4);
if (value.endsWith("\r\n")) {
value = value.substring(0, value.length() - 2);
}
String name = extractParam(headers, "name");
if (headers.contains("filename=")) {
String filename = extractParam(headers, "filename");
fields.add(name + "=[文件: " + filename + "]");
} else {
fields.add(name + "=" + value);
}
}
return fields.isEmpty() ? "[multipart数据]" : "{" + String.join(", ", fields) + "}";
}
/**
* 从Content-Type中提取boundary
*/
private String extractBoundary(String contentType) {
for (String param : contentType.split(";")) {
param = param.trim();
if (param.toLowerCase().startsWith("boundary=")) {
return param.substring("boundary=".length()).trim();
}
}
return null;
}
/**
* 从headers中提取参数值(如name、filename)
*/
private String extractParam(String headers, String paramName) {
Pattern pattern = Pattern.compile(paramName + "=\"([^\"]*)\"");
Matcher matcher = pattern.matcher(headers);
if (matcher.find()) {
return matcher.group(1);
}
return paramName;
}
}
@@ -2,6 +2,9 @@ server:
port: 80
spring:
codec:
# 运单批量导入等大 JSON / 失败明细导出场景,默认 256KB 会触发网关 500
max-in-memory-size: 20MB
cloud:
gateway:
server:
@@ -7,6 +7,7 @@ import org.springblade.process.pojo.dto.BusinessProcessDeleteDTO;
import org.springblade.process.pojo.dto.BusinessProcessSubmitDTO;
import org.springblade.process.pojo.dto.BusinessProcessUpdateDTO;
import org.springblade.process.pojo.vo.BusinessProcessVO;
import org.springblade.process.pojo.vo.MkFormBindVO;
import org.springblade.process.pojo.vo.ProcessApprovedRecordVO;
import org.springblade.process.pojo.vo.ProcessTodoVO;
import org.springframework.cloud.openfeign.FeignClient;
@@ -40,6 +41,8 @@ public interface IBusinessProcessClient {
String QUERY_APPROVED_RECORD_LIST = API_PREFIX + "/queryApprovedRecordsNoAttachments";
String GET_CURRENT_NODES = API_PREFIX + "/getCurrentNodes";
String GET_PROCESS_INFO = API_PREFIX + "/getProcessInfo";
String RESOLVE_BY_FORM_INSTANCE_ID = API_PREFIX + "/resolveByFormInstanceId";
String MARK_PROCESS_FINISHED = API_PREFIX + "/markProcessFinished";
/**
* 提交业务流程
@@ -126,4 +129,24 @@ public interface IBusinessProcessClient {
@GetMapping(GET_PROCESS_INFO)
FR<Object> getProcessInfo(@RequestParam("processInstanceId") String processInstanceId,
@RequestParam(value = "loginName", required = false) String loginName);
/**
* 按表单实例id解析业务绑定
*
* @param formInstanceId 表单实例id
* @return 绑定信息(含业务主键)
*/
@GetMapping(RESOLVE_BY_FORM_INSTANCE_ID)
FR<MkFormBindVO> resolveByFormInstanceId(@RequestParam("formInstanceId") Long formInstanceId);
/**
* 公开页流程结束时标记业务流程通过/驳回并完结
*
* @param processInstanceId 流程实例id
* @param approved true=通过 false=驳回
* @return 是否更新成功
*/
@PostMapping(MARK_PROCESS_FINISHED)
FR<Boolean> markProcessFinished(@RequestParam("processInstanceId") String processInstanceId,
@RequestParam("approved") boolean approved);
}
@@ -65,6 +65,12 @@ public class BusinessProcess implements Serializable {
*/
@Schema(description = "业务id")
private Long bizId;
/**
* MK表单实例id
*/
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "MK表单实例id")
private Long formInstanceId;
/**
* 流程实例id
*/
@@ -0,0 +1,94 @@
/**
* 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.process.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springblade.core.tool.utils.DateUtil;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* MK表单实例绑定表 实体类
*
* @author BladeX
*/
@Data
@TableName("blade_mk_form_bind")
@Schema(description = "MkFormBind对象")
public class MkFormBind implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "主键")
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@Schema(description = "租户ID")
private String tenantId;
@Schema(description = "业务类型")
private String bizType;
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "业务主键")
private Long bizId;
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "MK表单实例id")
private Long formInstanceId;
@Schema(description = "MK流程实例id")
private String processInstanceId;
@Schema(description = "是否当前生效(1是0否)")
private Integer isCurrent;
@DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
@JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
@Schema(description = "创建时间", hidden = true)
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME)
@JsonFormat(pattern = DateUtil.PATTERN_DATETIME)
@Schema(description = "更新时间", hidden = true)
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
@@ -0,0 +1,61 @@
/**
* 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.process.pojo.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* MK表单实例绑定 VO
*
* @author BladeX
*/
@Data
@Schema(description = "MkFormBindVO")
public class MkFormBindVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "业务主键")
private Long bizId;
@Schema(description = "业务类型")
private String bizType;
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "MK表单实例id")
private Long formInstanceId;
@Schema(description = "MK流程实例id")
private String processInstanceId;
}
@@ -138,6 +138,18 @@ public class Dept extends TenantEntity {
@Schema(description = "部门编码")
private String deptCode;
/**
* 金蝶部门代码
*/
@Schema(description = "金蝶部门代码")
private String kingdeeDeptCode;
/**
* 合同编码前缀(合同编号:前缀(年份)三位流水号)
*/
@Schema(description = "合同编码前缀")
private String contractCodePrefix;
/**
* 组织简称
*/
@@ -204,4 +216,12 @@ public class Dept extends TenantEntity {
@TableField(updateStrategy = FieldStrategy.NOT_NULL)
private String oaSupSubComId;
/**
* OA上次更新时间(OA 的 modified 字段),仅同步比对使用
*/
@JsonIgnore
@Schema(description = "OA上次更新时间", hidden = true)
@TableField(updateStrategy = FieldStrategy.NOT_NULL)
private String oaModified;
}
@@ -25,6 +25,7 @@
*/
package org.springblade.system.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -63,6 +64,7 @@ public class RailwayStation extends BaseEntity {
* 电报略码
*/
@Schema(description = "电报略码")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String telegraphCode;
/**
* 车站名称
@@ -4,29 +4,34 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 部门类型枚举
* 组织类型枚举,与业务字典 organization_type 对齐
*
* @author bfhuange
* @date 2024/8/26
*/
@AllArgsConstructor
@Getter
public enum DeptCategory {
/**
* 公司
*/
COMPANY(1, "公司"),
/**
* 部门
*/
DEPT(2, "部门"),
;
/**
* 集团
*/
GROUP(1, "集团"),
/**
* 公司
*/
COMPANY(2, "公司"),
/**
* 部门
*/
DEPT(3, "部门"),
;
/**
* 部门类型编码
*/
private final Integer code;
/**
* 部门类型名称
*/
private final String name;
/**
* 组织类型编码
*/
private final Integer code;
/**
* 组织类型名称
*/
private final String name;
}
@@ -35,8 +35,12 @@ public enum UserSex {
if (name == null || name.isEmpty()) {
return null;
}
String sexName = name.trim();
if ("-1".equals(sexName) || "0".equals(sexName) || "未知".equals(sexName)) {
return null;
}
return Stream.of(values())
.filter(e -> e.getName().equals(name))
.filter(e -> e.getName().equals(sexName) || String.valueOf(e.getCode()).equals(sexName))
.map(UserSex::getCode)
.findFirst().orElse(null);
}
@@ -52,6 +52,13 @@ public class AirportMasterVO extends AirportMaster {
@Schema(description = "导入错误信息")
private String errorMessage;
/**
* 创建人姓名
*/
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
/**
* 更新人姓名
*/
@@ -49,6 +49,10 @@ public class FeeItemVO extends FeeItem {
@Schema(description = "组织")
private String createDeptName;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
@TableField(exist = false)
@Schema(description = "更新人")
private String updateUserName;
@@ -66,4 +66,7 @@ public class OaOrgSyncPageVO implements Serializable {
@Schema(description = "是否已到最后一页")
private Boolean finished;
@Schema(description = "增量同步使用的 OA modified 起始时间,后续分页需原样回传")
private String modifiedSince;
}
@@ -63,4 +63,7 @@ public class OaPersonSyncPageVO implements Serializable {
@Schema(description = "是否已到最后一页")
private Boolean finished;
@Schema(description = "增量同步使用的 OA modified 起始时间,后续分页需原样回传")
private String modifiedSince;
}
@@ -52,6 +52,13 @@ public class PortTerminalVO extends PortTerminal {
@Schema(description = "导入错误信息")
private String errorMessage;
/**
* 创建人姓名
*/
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
/**
* 更新人姓名
*/
@@ -52,6 +52,13 @@ public class RailwayStationVO extends RailwayStation {
@Schema(description = "导入错误信息")
private String errorMessage;
/**
* 创建人姓名
*/
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
/**
* 更新人姓名
*/
@@ -0,0 +1,148 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.jackson;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* LocalDate 宽松反序列化,对齐 OCR / 司机侧日期归一化规则。
* <p>
* 兼容写法:
* <ul>
* <li>ISO:{@code yyyy-MM-dd}</li>
* <li>紧凑:{@code yyyyMMdd}</li>
* <li>点分 / 斜杠 / 年月日:{@code 2018.3.9}、{@code 2026/03/17}、{@code 2030年05月20日}</li>
* <li>任意非数字分隔:年与月、月与日之间只要不是数字即可</li>
* <li>区间文本:按字段名取起期(first)或止期(last)</li>
* </ul>
*
* @author Chill
*/
public class FlexibleLocalDateDeserializer extends JsonDeserializer<LocalDate> {
/**
* 4 位年 + 1~2 位月 + 1~2 位日,中间允许任意非数字分隔(与 OcrFieldSupport 一致)
*/
private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})\\D+(\\d{1,2})\\D+(\\d{1,2})");
private static final Pattern COMPACT_PATTERN = Pattern.compile("(\\d{4})(\\d{2})(\\d{2})");
@Override
public LocalDate deserialize(JsonParser parser, DeserializationContext context) throws IOException {
String text = parser.getValueAsString();
if (text == null) {
return null;
}
String value = text.trim();
if (value.isEmpty()) {
return null;
}
// 「长期」等非日期文案:按空值处理,避免把无法解析的字符串打成 400
if (value.contains("长期") || value.contains("永久")) {
return null;
}
boolean preferLast = isEndDateField(parser.getCurrentName());
LocalDate parsed = parseFlexible(value, preferLast);
if (parsed != null) {
return parsed;
}
return (LocalDate) context.handleWeirdStringValue(LocalDate.class, value,
"期望日期格式 yyyy-MM-dd / yyyyMMdd / yyyy年M月d日 等 OCR 常见写法");
}
/**
* 解析宽松日期文本;区间时按 preferLast 取首段或末段。
*
* @param value 原始文本
* @param preferLast true 取止期,false 取起期
* @return 合法 LocalDate;无法识别时返回 null
*/
public static LocalDate parseFlexible(String value, boolean preferLast) {
if (value == null) {
return null;
}
String text = value.trim();
if (text.isEmpty()) {
return null;
}
List<LocalDate> candidates = matchDates(text);
if (!candidates.isEmpty()) {
return preferLast ? candidates.get(candidates.size() - 1) : candidates.get(0);
}
// 去掉所有非数字后恰为 8 位时按 yyyyMMdd 处理(如 20260317、2026-0317 混杂)
String compact = text.replaceAll("\\D", "");
if (compact.length() == 8) {
return toLocalDate(compact.substring(0, 4), compact.substring(4, 6), compact.substring(6, 8));
}
Matcher compactMatcher = COMPACT_PATTERN.matcher(text);
if (compactMatcher.find()) {
return toLocalDate(compactMatcher.group(1), compactMatcher.group(2), compactMatcher.group(3));
}
return null;
}
private static List<LocalDate> matchDates(String text) {
List<LocalDate> dates = new ArrayList<>();
Matcher matcher = DATE_PATTERN.matcher(text);
while (matcher.find()) {
LocalDate date = toLocalDate(matcher.group(1), matcher.group(2), matcher.group(3));
if (date != null) {
dates.add(date);
}
}
return dates;
}
private static LocalDate toLocalDate(String yearText, String monthText, String dayText) {
try {
int year = Integer.parseInt(yearText);
int month = Integer.parseInt(monthText);
int day = Integer.parseInt(dayText);
return LocalDate.of(year, month, day);
} catch (NumberFormatException | DateTimeException exception) {
return null;
}
}
/**
* 止期字段名通常以 End / EndDate 结尾,区间文本应取最后一段日期。
*/
private static boolean isEndDateField(String fieldName) {
if (fieldName == null || fieldName.isEmpty()) {
return false;
}
return fieldName.endsWith("End") || fieldName.endsWith("EndDate")
|| fieldName.contains("EndDate") || fieldName.contains("ValidEnd");
}
}
@@ -46,4 +46,7 @@ public class ExceptionDisposalFollowRequest implements Serializable {
@Schema(description = "跟进说明")
private String followContent;
@Schema(description = "附件JSON,[{\"name\":\"...\",\"url\":\"...\"}]")
private String attachmentsJson;
}
@@ -0,0 +1,63 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 异常处置编辑请求
* <p>
* 只承载上报四件套。车牌、项目、承运商、配载单号等字段由上报时从运单回填,
* 不在此列,避免编辑后与运单脱钩。
* <p>
* 不含处置状态:状态只能经跟进与完成流转,编辑不得改变它。
*
* @author Chill
*/
@Data
@Schema(description = "异常处置编辑请求")
public class ExceptionDisposalUpdateRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "异常处置ID")
private Long id;
@Schema(description = "异常类型")
private String exceptionType;
@Schema(description = "异常原因")
private String exceptionReason;
@Schema(description = "上报说明")
private String reportDescription;
@Schema(description = "现场照片,逗号分隔")
private String scenePhotos;
}
@@ -5,23 +5,22 @@
* 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
* 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
* Using this software signifies agreement to the License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.springblade.transport.pojo.dto;
@@ -32,12 +31,12 @@ import java.io.Serial;
import java.io.Serializable;
/**
* 金蝶票池实时统一查询条件(收票登记选票与正式结算单认领共用)
* 金蝶票据池镜像统一查询条件(收票登记选票与正式结算单认领共用,查本地镜像)
*
* @author Chill
*/
@Data
@Schema(description = "金蝶票池实时统一查询条件")
@Schema(description = "金蝶票据池镜像统一查询条件")
public class KingdeeInvoicePoolQueryDTO implements Serializable {
@Serial
@@ -73,6 +72,24 @@ public class KingdeeInvoicePoolQueryDTO implements Serializable {
@Schema(description = "开票日期止 yyyy-MM-dd")
private String invoiceDateEnd;
/**
* 开票单位(销方名称),模糊
*/
@Schema(description = "开票单位(销方名称,模糊)")
private String salerName;
/**
* 受票单位(购方名称),模糊
*/
@Schema(description = "受票单位(购方名称,模糊)")
private String buyerName;
/**
* 发票类型编码(票易通字典,如 26/27,镜像按 invoice_type_code 精确匹配)
*/
@Schema(description = "发票类型编码(票易通字典,如 26/27)")
private String invoiceType;
/**
* 销方税号,精确
*/
@@ -86,14 +103,14 @@ public class KingdeeInvoicePoolQueryDTO implements Serializable {
private String buyerTaxNo;
/**
* 发票类型编码(票易通字典,如 26/27)
* 认领过滤:true 仅已认领,false 仅未认领,null 不过滤(已认领=存在非作废正式结算单发票行)
*/
@Schema(description = "发票类型编码(票易通字典,如 26/27)")
private String invoiceType;
@Schema(description = "认领过滤:true已认领/false未认领/null不限")
private Boolean claimed;
/**
* 续扫游标:下一页翻页时原样回传上页返回的 nextCursor,空表示按页码锚定重新扫描
* 登记过滤:true 仅已登记,false 仅未登记,null 不过滤(已登记=存在非作废收票登记)
*/
@Schema(description = "续扫游标(下一页请求回传上页 nextCursor)")
private String cursor;
@Schema(description = "登记过滤:true已登记/false未登记/null不限")
private Boolean registered;
}
@@ -30,7 +30,19 @@ public class NodeSubmitDTO implements Serializable {
@Schema(description = "定位信息")
private Location location;
@Schema(description = "凭证照片 URL 列表")
/**
* 带类型的打卡照片,类型由提交方显式给出;非空时优先于 photos。
*/
@Schema(description = "带类型的照片列表,非空时优先于 photos")
private List<PunchPhotoDTO> photoItems;
/**
* 旧版小程序提交的裸 URL 列表,类型只能按下标回推过程配置的凭证类型。
* <p>
* 与 photoItems 并存是为了不强制升级小程序:新版提交 photoItems,旧版仍走这里;
* 等旧版本覆盖率达标后由后续改造删除。
*/
@Schema(description = "凭证照片 URL 列表,类型按下标回推")
private List<String> photos;
@Schema(description = "重量(吨)")
@@ -23,44 +23,31 @@
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.vo;
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 金蝶票池实时查询游标分页返回体(A801-1 无总数:hasMore 驱动上一页/下一页)
* 证件 OCR 识别请求(小程序)
*
* @author Chill
*/
@Data
@Schema(description = "金蝶票池游标分页返回体")
public class KingdeeInvoicePoolPageVO implements Serializable {
@Schema(description = "证件OCR识别请求")
public class OcrRecognizeRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 本页数据(金蝶实时行 + 本地认领/登记装饰标记) */
@Schema(description = "本页数据")
private List<KingdeeInvoicePoolVO> records;
@Schema(description = "已上传图片的地址,必须是百度可访问的HTTP或HTTPS地址")
private String imageUrl;
@Schema(description = "证件类型:idcard-身份证,driver-驾驶证,vehicle-行驶证,road_transport-道路运输证")
private String type;
@Schema(description = "正副面:front或back,默认front")
private String side;
/** 当前页码,从 1 开始 */
@Schema(description = "当前页码")
private Long current;
/** 每页条数 */
@Schema(description = "每页条数")
private Long size;
/** 是否还有下一页(服务端有界补页扫描判断) */
@Schema(description = "是否还有下一页")
private Boolean hasMore;
/** 下一页续扫游标("远程页码:已消费过滤行数"),null 表示没有更多;翻下一页时原样回传 */
@Schema(description = "下一页续扫游标,null 表示没有更多")
private String nextCursor;
}
@@ -0,0 +1,98 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDate;
/**
* 司机端个人认证提交请求
* <p>
* 一次提交承载个人认证的**全部**内容(身份证 + 驾驶证 + 从业资格证),
* 三证一起提交、一起审核,不拆开。
*
* @author Chill
*/
@Data
@Schema(description = "司机端个人认证提交请求")
public class PersonCertSubmitRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "真实姓名")
private String realName;
@Schema(description = "身份证号")
private String idCardNo;
@Schema(description = "身份证正面照")
private String idCardFrontUrl;
@Schema(description = "身份证反面照")
private String idCardBackUrl;
@Schema(description = "身份证有效期起")
private LocalDate idCardValidStart;
@Schema(description = "身份证有效期止")
private LocalDate idCardValidEnd;
@Schema(description = "身份证长期有效")
private Integer idCardLongTerm;
@Schema(description = "驾驶证档案编号")
private String driverFileNo;
@Schema(description = "准驾车型")
private String driverClass;
@Schema(description = "驾驶证主页照片")
private String driverLicenseFrontUrl;
@Schema(description = "驾驶证副页照片")
private String driverLicenseBackUrl;
@Schema(description = "驾驶证有效期起")
private LocalDate driverValidStart;
@Schema(description = "驾驶证有效期止")
private LocalDate driverValidEnd;
@Schema(description = "驾驶证长期有效")
private Integer driverLongTerm;
@Schema(description = "从业资格证类型")
private String qualificationType;
@Schema(description = "从业资格证号")
private String qualificationNo;
@Schema(description = "从业资格证封面页")
private String qualificationFront;
@Schema(description = "从业资格证内容页")
private String qualificationBack;
@Schema(description = "从业资格证有效期起")
private LocalDate qualificationValidStart;
@Schema(description = "从业资格证有效期止")
private LocalDate qualificationValidEnd;
@Schema(description = "从业资格证长期有效")
private Integer qualificationLongTerm;
@Schema(description = "大头照")
private String headPhoto;
}
@@ -0,0 +1,32 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 带类型的打卡照片
* <p>
* 类型取值域是固定的「打卡照片」加该节点过程配置勾选的凭证类型,域外类型一律拒绝:
* 类型是照片分组的唯一依据,放进来路不明的类型会让回显分组错乱。
*/
@Data
@Schema(description = "带类型的打卡照片")
public class PunchPhotoDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "照片类型")
private String type;
@Schema(description = "图片URL")
private String url;
}
@@ -0,0 +1,70 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for their own commercial purposes.
* <p>
* Using this software signifies agreement to this license agreement and terms of use,
* and you expressly agree to be bound by the terms of use of this software.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 收款认领推送金蝶结果(A301-3)
* <p>
* 推送失败不抛异常跨事务边界(保证失败状态落库不被回滚),以本结果对象向调用方报告成败与原因。
*
* @author Chill
*/
@Data
@Schema(description = "收款认领推送金蝶结果")
public class ReceiptClaimPushResult implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "是否推送成功")
private boolean success;
@Schema(description = "金蝶收款单号(成功时返回,可能为空:金蝶响应未返回单号)")
private String kingdeeBillNo;
@Schema(description = "失败原因(成功时为空)")
private String message;
public static ReceiptClaimPushResult success(String kingdeeBillNo) {
ReceiptClaimPushResult result = new ReceiptClaimPushResult();
result.setSuccess(true);
result.setKingdeeBillNo(kingdeeBillNo);
return result;
}
public static ReceiptClaimPushResult fail(String message) {
ReceiptClaimPushResult result = new ReceiptClaimPushResult();
result.setSuccess(false);
result.setMessage(message);
return result;
}
}
@@ -0,0 +1,115 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDate;
/**
* 司机端车辆认证提交请求
* <p>
* 一次提交承载该车辆认证的**全部**内容(行驶证 + 道路运输证),两证一起提交、一起审核。
* 提交只产生新的待审记录,不覆盖车辆当前已生效的认证内容。
*
* @author Chill
*/
@Data
@Schema(description = "司机端车辆认证提交请求")
public class VehicleCertSubmitRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "车辆ID:新增时为空,重新认证时必填")
private Long id;
@Schema(description = "车牌号")
private String plateNo;
@Schema(description = "所有人:行驶证登记的单位名称,不是司机")
private String ownerName;
@Schema(description = "车牌颜色")
private String plateColor;
@Schema(description = "车辆类型")
private String vehicleType;
@Schema(description = "车辆识别代号(车架号),选填")
private String vin;
@Schema(description = "外廓长度,毫米")
private Integer outerLength;
@Schema(description = "外廓宽度,毫米")
private Integer outerWidth;
@Schema(description = "外廓高度,毫米")
private Integer outerHeight;
@Schema(description = "核定载质量,KG")
private Integer approvedLoadKg;
@Schema(description = "准牵引总质量,KG")
private Integer tractionMassKg;
@Schema(description = "能源类型")
private String energyType;
@Schema(description = "业务关系:自有 / 管理挂靠 / 业务合作")
private String businessRelation;
@Schema(description = "强制报废日期")
private LocalDate compulsoryScrapDate;
@Schema(description = "强制报废长期有效:0否 1是")
private Integer compulsoryScrapLongTerm;
@Schema(description = "行驶证档案编号")
private String drivingLicenseNo;
@Schema(description = "行驶证主页正面照片")
private String drivingLicenseImage;
@Schema(description = "行驶证主页反面照片")
private String drivingLicenseMainBack;
@Schema(description = "行驶证副页正面照片")
private String drivingLicenseViceFront;
@Schema(description = "行驶证副页反面照片")
private String drivingLicenseViceBack;
@Schema(description = "行驶证有效期起")
private LocalDate drivingLicenseStartDate;
@Schema(description = "行驶证有效期止")
private LocalDate drivingLicenseEndDate;
@Schema(description = "行驶证长期有效:0否 1是")
private Integer drivingLicenseLongTerm;
@Schema(description = "道路运输证号")
private String roadTransportCertNo;
@Schema(description = "道路运输证照片")
private String roadTransportCertImage;
@Schema(description = "道路运输证有效期起")
private LocalDate roadTransportCertStartDate;
@Schema(description = "道路运输证有效期止")
private LocalDate roadTransportCertEndDate;
@Schema(description = "道路运输证长期有效:0否 1是")
private Integer roadTransportCertLongTerm;
@Schema(description = "机动车登记编号")
private String registrationNo;
@Schema(description = "机动车登记本照片")
private String registrationImage;
}
@@ -32,5 +32,13 @@ public class WaybillImportBatchRequest {
private Long planId;
private String planName;
private String remark;
/** 列表筛选:创建人 */
private Long createUser;
/** 列表筛选:创建时间起(yyyy-MM-dd HH:mm:ss) */
private String createTimeStart;
/** 列表筛选:创建时间止(yyyy-MM-dd HH:mm:ss) */
private String createTimeEnd;
private List<Map<String, Object>> rows;
/** 分批确认导入时,是否追加到已有批次(首批为 false,后续批为 true)。 */
private Boolean append;
}
@@ -0,0 +1,114 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 管理端打卡记录编辑请求
* <p>
* 承载时间轴上单条打卡记录的全部可编辑项,字段按过程配置取舍:
* 打卡定位(地址/经纬度)、上传货量(重量/体积/数量)、上传凭证(照片)。
* <p>
* {@code punched=false} 表示取消打卡,服务端会清除该节点的打卡流水
* ——时间轴上「未打卡」的判据就是没有流水,故不能只改状态位。
*
* @author Chill
*/
@Data
@Schema(description = "管理端打卡记录编辑请求")
public class WaybillPunchRecordSaveRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "运单ID")
private Long waybillId;
@Schema(description = "打卡类型:node=过程节点打卡,enroute=在途打卡")
private String type;
@Schema(description = "过程节点 key")
private String nodeCode;
@Schema(description = "过程节点名称")
private String nodeName;
@Schema(description = "打卡状态:true 已打卡,false 未打卡")
private Boolean punched;
@Schema(description = "打卡时间 yyyy-MM-dd HH:mm:ss,为空时取当前时间")
private String punchTime;
@Schema(description = "打卡地址")
private String address;
@Schema(description = "经度")
private String longitude;
@Schema(description = "纬度")
private String latitude;
@Schema(description = "重量(吨)")
private String weight;
@Schema(description = "体积(方)")
private String volume;
@Schema(description = "数量(件)")
private String quantity;
@Schema(description = "备注")
private String remark;
@Schema(description = "打卡凭证图,按上传顺序回写凭证类型")
private List<Photo> photos = new ArrayList<>();
/**
* 打卡凭证图
* <p>
* 类型随图一起回写:展示层对「逗号分隔的裸 URL」按凭证类型下标回推标签,
* 编辑换了顺序就会串标,所以这里保留原类型。
*/
@Data
@Schema(description = "打卡凭证图")
public static class Photo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "凭证类型")
private String type;
@Schema(description = "图片URL")
private String url;
}
}
@@ -61,10 +61,10 @@ public class ContractManage extends TenantEntity {
@Schema(description = "所属项目")
private String projectName;
@Schema(description = "所属组织ID")
@Schema(description = "合同负责部门ID")
private Long organizationId;
@Schema(description = "所属组织")
@Schema(description = "合同负责部门")
private String organizationName;
@Schema(description = "合同类别")
@@ -100,6 +100,9 @@ public class ContractManage extends TenantEntity {
@Schema(description = "签订日期")
private LocalDate signDate;
@Schema(description = "申请日期(提交正式合同时写入)")
private LocalDate applyDate;
@Schema(description = "结算方式")
private String settlementMode;
@@ -107,6 +110,7 @@ public class ContractManage extends TenantEntity {
private String contractFormat;
@Schema(description = "是否需要加盖法人章")
@TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
private Integer legalSealFlag;
@Schema(description = "一式份数")
@@ -206,7 +210,10 @@ public class ContractManage extends TenantEntity {
@Schema(description = "终止原因")
private String terminateReason;
@Schema(description = "备注")
@Schema(description = "合同主要内容")
private String mainContent;
@Schema(description = "其他事项说明")
private String remark;
@Schema(description = "合规系统单据ID(fdContractId)")
@@ -28,10 +28,15 @@ package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import org.springblade.transport.jackson.FlexibleLocalDateDeserializer;
import java.io.Serial;
import java.time.LocalDate;
@@ -64,6 +69,7 @@ public class Driver extends TenantEntity {
* 出生年月
*/
@Schema(description = "出生年月")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate birthday;
/**
* 性别
@@ -111,6 +117,23 @@ public class Driver extends TenantEntity {
*/
@Schema(description = "身份证反面照")
private String idCardBack;
/**
* 身份证有效期起
*/
@Schema(description = "身份证有效期起")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate idCardValidStart;
/**
* 身份证有效期止
*/
@Schema(description = "身份证有效期止")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate idCardValidEnd;
/**
* 身份证长期有效
*/
@Schema(description = "身份证长期有效")
private Integer idCardLongTerm;
/**
* 大头照
*/
@@ -130,11 +153,13 @@ public class Driver extends TenantEntity {
* 驾驶证有效期起
*/
@Schema(description = "驾驶证有效期起")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate drivingLicenseStartDate;
/**
* 驾驶证有效期止
*/
@Schema(description = "驾驶证有效期止")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate drivingLicenseEndDate;
/**
* 驾驶证长期有效
@@ -165,6 +190,7 @@ public class Driver extends TenantEntity {
* 从业资格证有效期止
*/
@Schema(description = "从业资格证有效期止")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate qualificationEndDate;
/**
* 从业资格证长期有效
@@ -192,9 +218,10 @@ public class Driver extends TenantEntity {
@Schema(description = "手机号")
private String mobile;
/**
* 关联系统用户ID
* 关联小程序用户ID(blade_mp_user.id)
*/
@Schema(description = "关联系统用户ID")
@Schema(description = "关联小程序用户ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
/**
* 与联系人关系
@@ -221,5 +248,32 @@ public class Driver extends TenantEntity {
*/
@Schema(description = "备注")
private String remark;
/**
* 最新提交状态展示缓存:0审核中 1已通过 2已驳回 3已作废,null 表示从未提交
* <p>
* 仅为列表展示而冗余,<b>不是</b>可用性判据;「是否已认证」一律读生效快照指针。
*/
@Schema(description = "最新提交状态展示缓存:0审核中 1已通过 2已驳回 3已作废,null为从未提交")
private Integer certificationStatus;
/**
* 最新一次驳回原因展示缓存
*/
@Schema(description = "最新一次驳回原因展示缓存")
private String certificationRejectReason;
/**
* 生效快照指针:指向 blade_transport_cert_record.id
* <p>
* 「该司机是否已认证」的<b>唯一</b>判据。为 null 表示从未认证通过;
* 一经设置<b>永不回退</b>——提交、驳回、作废都不得修改它,
* 因此重新提交认证不会让一个已认证的司机失去接单资格。
* <p>
* ⚠️ {@code READ_ONLY}:本字段<b>只能由审核通过时</b>写入。
* 司机档案的 {@code /submit} 直接以本实体接收请求体,若允许反序列化,
* 调用方就能自行填一个记录 ID 把自己「认证」掉,绕过审核。
*/
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "生效快照指针")
private Long effectiveCertRecordId;
}
@@ -51,6 +51,12 @@ public class ExceptionDisposal extends TenantEntity {
@Schema(description = "运单号")
private String waybillNo;
@Schema(description = "过程节点 key(为空表示未指明节点)")
private String nodeCode;
@Schema(description = "过程节点名称(上报时复制,此后不随过程配置变化)")
private String nodeName;
@Schema(description = "配载单号/总单号")
private String loadingOrMasterNo;
@@ -51,6 +51,9 @@ public class ExceptionDisposalFollowRecord extends TenantEntity {
@Schema(description = "跟进说明")
private String followContent;
@Schema(description = "附件JSON")
private String attachmentsJson;
@Schema(description = "跟进人")
private Long followUser;
@@ -144,6 +144,10 @@ public class FormalSettlement extends TenantEntity {
* 金蝶单据编号
*/
private String kingdeeBillNo;
/**
* 金蝶单据id(星瀚内部主键,A902 附件上传定位用)
*/
private String kingdeeBillId;
/**
* 金蝶同步状态:unsynced-未同步 synced-已同步 failed-同步失败
*/
@@ -159,6 +159,10 @@ public class InvoiceApplication extends TenantEntity {
* 金蝶开票申请单编号(A303-1 返回,A303-5 查询 serialNo)
*/
private String kingdeeBillNo;
/**
* 金蝶开票申请单id(星瀚内部主键,A902 附件上传定位用)
*/
private String kingdeeBillId;
/**
* 金蝶同步状态:unsynced-未同步 synced-已同步 failed-同步失败 invoiced-开票完成(终态)
*/
@@ -33,6 +33,7 @@ import java.time.LocalDateTime;
* 开票申请金蝶发票结果实体类
* <p>
* 定时任务经 A303-5 开票申请发票查询回写的发票明细(发票号唯一键幂等落库),
* 发票文件地址(板式文件/数电PDF/XML/预览图)独立落库供详情页查看下载,
* 原文 JSON 整体留档便于联调与追溯
*
* @author Chill
@@ -61,6 +62,18 @@ public class InvoiceApplicationInvoice extends TenantEntity {
@Schema(description = "价税合计")
private BigDecimal invoiceAmount;
@Schema(description = "板式文件下载地址(数电票为OFD,航信百望税盘普通电票为PDF,电子专票/UKey电票为OFD)")
private String invoiceFileUrl;
@Schema(description = "PDF图片预览地址")
private String invoiceImageUrl;
@Schema(description = "数电PDF下载地址(发票云5.0.025版本返回)")
private String invoicePdfFileUrl;
@Schema(description = "数电XML下载地址(发票云5.0.025版本返回)")
private String invoiceXmlFileUrl;
@Schema(description = "A303-5 返回原文JSON留档")
private String rawJson;
@@ -152,6 +152,10 @@ public class InvoiceReceipt extends TenantEntity {
* 金蝶收票单单据编号(A303-2 批量保存生成/幂等预检命中回填)
*/
private String kingdeeReceivedInvoiceNo;
/**
* 金蝶收票单id(星瀚内部主键,A902 附件上传定位用)
*/
private String kingdeeReceivedInvoiceId;
/**
* 收票单推送状态(unsynced 未推送 / pushed 已保存收票单 / assigned 已指定应付 / failed 推送失败)
*/
@@ -0,0 +1,84 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 金蝶附件同步实体类(A902 附件上传到指定单据,单附件行级状态)
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("blade_kingdee_attachment_sync")
@Schema(description = "金蝶附件同步")
public class KingdeeAttachmentSync extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 业务单据类型(pay_application/formal_settlement_ap/formal_settlement_ar/invoice_receipt/invoice_application)
*/
private String bizType;
/**
* 业务单据id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long bizId;
/**
* 附件幂等键(用户附件=附件url;发票文件=A801-2 的 path)
*/
private String attachmentKey;
/**
* 附件名称(展示/排查用)
*/
private String attachmentName;
/**
* 金蝶侧文件路径(A902 返回 data.path)
*/
private String kingdeePath;
/**
* 同步状态:1 成功 / 0 失败
*/
private Integer syncStatus;
/**
* 失败原因(超限/下载失败/金蝶报错)
*/
private String failReason;
/**
* 最近上传时间
*/
private LocalDateTime syncTime;
}
@@ -0,0 +1,87 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 金蝶单据锁定记录实体类
* <p>
* 一行 = 一张金蝶目标单:推送成功后 lockBusiData 锁定(FAILED→LOCKED),金蝶经 unLockNotifyBusiData
* 回调解锁(→UNLOCKED,DELETE 时废除源单并还原关联数据);同时承担当前锁状态(编辑守卫/回调定位)
* 与锁定补偿扫表(FAILED+try_count)、锁定/解锁留痕
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("blade_kingdee_bill_lock")
@Schema(description = "金蝶单据锁定记录")
public class KingdeeBillLock extends TenantEntity {
@Serial private static final long serialVersionUID = 1L;
/**
* WLTMS 单据类型(SETTLEMENT/INVOICE_APPLY,预留 PAY_APPLY/RECEIVED_INVOICE)
*/
private String sourceBillType;
/**
* WLTMS 业务主键
*/
private Long sourceBillId;
/**
* WLTMS 单号(正式结算单号/开票申请单号)
*/
private String sourceBillNo;
/**
* 金蝶单据类型(ApFin_other_BT_S/arfin_other_BT_S/cdm_paybill_bank_elect_BT_S 等固定值)
*/
private String targetBillType;
/**
* 金蝶单据编号(即业务单 kingdeeBillNo,开票申请按发票张次逐张一行)
*/
private String targetBillNo;
/**
* 锁状态:failed-锁定失败(补偿重试对象)/locked-金蝶锁定中/unlocked-已解锁(终态)
*/
private String lockStatus;
/**
* 最近动作来源操作类型:SAVE-单据新增,DELETE-单据删除(DELETE 回调触发废除还原)
*/
private String operationType;
/**
* 锁定重试次数(补偿任务用,超上限停试)
*/
private Integer tryCount;
/**
* 锁定时间
*/
private LocalDateTime lockTime;
/**
* 解锁时间
*/
private LocalDateTime unlockTime;
/**
* 推单发起人(锁定操作人)
*/
private String lockOperator;
/**
* 回调操作人(金蝶侧操作人)
*/
private String notifyOperator;
/**
* 锁定失败原因
*/
private String failReason;
}
@@ -143,4 +143,12 @@ public class KingdeeInvoicePool extends TenantEntity {
* 票池数据更新时间(A801-1 billCreateTime,镜像排序字段)
*/
private LocalDateTime sourceUpdatedTime;
/**
* 剩余可用金额(A801-1 availableAmount,金蝶可用税额口径,选票本地过滤用:>0 才可选)
*/
private BigDecimal availableAmount;
/**
* 金额使用状态(A801-1 amountUseStatus,0 未使用可选 / 2 全部使用;选票本地过滤用)
*/
private String amountUseStatus;
}
@@ -80,16 +80,76 @@ public class KingdeeReceiptFlow extends TenantEntity {
* 收款人(A301-2 recpayee)
*/
private String payeeName;
/**
* 收款通知单号(A301-2 billno)
*/
private String receiptNoticeNo;
/**
* 付款人名称(A301-2 recviewpayer)
*/
private String payerName;
/**
* 收款金额(A301-2 reamount)
*/
private BigDecimal receiptAmount;
/**
* 对方单位名称(A301-2 oppunit)
*/
private String counterpartyName;
/**
* 对方银行账号(A301-2 oppbanknumber)
*/
private String counterpartyAccount;
/**
* 对方开户行(A301-2 oppbank)
*/
private String counterpartyBank;
/**
* 摘要(A301-2 description)
*/
private String summary;
/**
* 交易日期(A301-2 tradetime)
*/
private LocalDateTime transactionTime;
/**
* 明细流水号(A301-2 detailid,回退 tradedetailno / bankdetailno,幂等键)
*/
private String detailSerialNo;
/**
* 已认领金额(本地认领驱动,初始为0)
*/
private BigDecimal claimedAmount;
/**
* 认领状态(unclaimed未认领/partial部分认领/claimed认领完成,累计认领与公告未认领金额对表)
*/
private String claimStatus;
/**
* 推送状态(none未推送/pushed已推送/failed推送失败;已推送为终态闸门,不允许二次推送)
*/
private String pushStatus;
/**
* 最近一次推送失败原因(成功后清空)
*/
private String pushFailReason;
/**
* 首次推送金蝶成功时间
*/
private LocalDateTime pushedTime;
/**
* 金蝶推送来源单据编码(分摊结算单号下划线拼接,推送时生成落库;分摊不变则重试编码不变,A301-1 回写匹配键)
*/
private String awrpSrcCode;
/**
* 金蝶收款单号(推送响应回填 / A301-1 回写推进)
*/
private String kingdeeBillNo;
/**
* 金蝶回写状态(none未推送/pushed已推送/approved审核通过/received已收款/failed失败)
*/
private String kingdeeBillStatus;
/**
* 来源更新时间(取交易日期,同步时刷新)
*/
private LocalDateTime sourceUpdatedTime;
}
@@ -56,7 +56,7 @@ public class LoadingManage extends TenantEntity {
@Schema(description = "货物名称")
private String cargoName;
@Schema(description = "车/船/航班/班列")
@Schema(description = "车牌号/船班号/船次号/班次号")
private String vehicleNo;
@Schema(description = "挂车车牌号")
@@ -70,10 +70,10 @@ public class ProjectApply extends TenantEntity {
@Schema(description = "业务部门")
private String businessDeptName;
@Schema(description = "承办部门ID")
private Long undertakeDeptId;
@Schema(description = "平台公司ID,多个逗号分隔")
private String undertakeDeptId;
@Schema(description = "承办部门")
@Schema(description = "平台公司,多个顿号分隔")
private String undertakeDeptName;
@Schema(description = "项目由来")
@@ -66,10 +66,12 @@ public class ReceiptClaim extends TenantEntity {
private String remark;
private String claimStatus;
/**
* 金蝶认领结果推送来源单据编码(A301-3 awrp_src_code,A301-1 回写按此匹配)
* 金蝶收款单号(冗余展示:推送成功/A301-1 回写时由流水级联刷新)
*/
private String awrpSrcCode;
private String kingdeeBillNo;
/**
* 金蝶回写状态(冗余展示:与流水 kingdee_bill_status 同步刷新)
*/
private String kingdeeBillStatus;
@JsonSerialize(using = ToStringSerializer.class)
private Long voidedBy;
@@ -0,0 +1,158 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 认证记录实体类
* <p>
* 认证对象(司机 / 车辆)<b>每一次提交</b>所产生的、<b>不可变</b>的快照。
* 记录一经创建,其 {@code snapshotJson} 与提交元信息永不修改;被同对象的后续提交取代时
* 标记为「已作废」,而<b>不是</b>删除。判定某个对象「是否已认证」只看它持有的生效快照指针,
* 与记录表里的任何一条记录的状态都无关。
* <p>
* ⚠️ <b>{@code status} 承载的是「审核状态」,不是 BladeX 的「启用 / 停用」。</b>
* 该字段继承自 {@link TenantEntity},带来两条必须堵住的框架通道:
* 一是 {@code BaseServiceImpl#resolveEntity} 在插入时会把 {@code null} 的 {@code status}
* <b>静默填成 {@code 1}</b>(恰好是 {@link #STATUS_APPROVED});
* 二是继承来的 {@code changeStatus(ids, 1)} 会直接写 {@code status},等于批量通过审核。
* 二者都已在 {@code TransportCertRecordServiceImpl} 中封堵(前者校验抛错、后者覆写抛错)。
* 审核必须逐条、带审核人与审核时间落库,不得借道 {@code changeStatus} 批量改状态。
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("blade_transport_cert_record")
@Schema(description = "认证记录")
public class TransportCertRecord extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 认证对象类型:司机
*/
public static final String BIZ_TYPE_DRIVER = "driver";
/**
* 认证对象类型:车辆
*/
public static final String BIZ_TYPE_VEHICLE = "vehicle";
/**
* 审核状态:审核中(即该对象的「待审快照」)
*/
public static final Integer STATUS_PENDING = 0;
/**
* 审核状态:已通过(该对象最新一条已通过者即其「生效快照」)
*/
public static final Integer STATUS_APPROVED = 1;
/**
* 审核状态:已驳回(审核的否定结论,需带驳回原因)
*/
public static final Integer STATUS_REJECTED = 2;
/**
* 审核状态:已作废(被同对象的再次提交取代,非驳回)
*/
public static final Integer STATUS_SUPERSEDED = 3;
/**
* 驳回原因最大长度,与 {@code reject_reason} 列宽保持一致
*/
public static final int REJECT_REASON_MAX_LENGTH = 200;
/**
* 认证对象类型:driver-司机,vehicle-车辆
* <p>
* {@code NEVER}:记录一经创建,认证对象的身份不可改。
*/
@TableField(updateStrategy = FieldStrategy.NEVER)
@Schema(description = "认证对象类型:driver-司机,vehicle-车辆")
private String bizType;
/**
* 认证对象主键:司机ID 或 车辆ID(车辆按具体车辆,不是司机)
*/
@TableField(updateStrategy = FieldStrategy.NEVER)
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "认证对象主键")
private Long bizId;
/**
* 展示键:司机姓名 / 车牌号,供审核列表直接展示
*/
@Schema(description = "展示键:司机姓名 / 车牌号")
private String bizKey;
/**
* 该次提交的全量认证内容(JSON),一经创建不可修改
* <p>
* {@code NEVER}:本字段<b>永不进入 UPDATE 语句</b>,因此不存在「某条链路
* 忘了保护快照」的可能。改内容只能靠<b>新提交一条记录</b>。
*/
@TableField(updateStrategy = FieldStrategy.NEVER)
@Schema(description = "该次提交的全量认证内容JSON")
private String snapshotJson;
/**
* 驳回原因,仅当审核状态为已驳回时有值
*/
@Schema(description = "驳回原因")
private String rejectReason;
/**
* 提交时间
*/
@TableField(updateStrategy = FieldStrategy.NEVER)
@Schema(description = "提交时间")
private LocalDateTime submitTime;
/**
* 提交人
*/
@TableField(updateStrategy = FieldStrategy.NEVER)
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "提交人")
private Long submitUser;
/**
* 审核时间
*/
@Schema(description = "审核时间")
private LocalDateTime auditTime;
/**
* 审核人
*/
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "审核人")
private Long auditUser;
}
@@ -26,10 +26,15 @@
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import org.springblade.transport.jackson.FlexibleLocalDateDeserializer;
import java.io.Serial;
import java.time.LocalDate;
@@ -63,6 +68,11 @@ public class TransportVehicle extends TenantEntity {
*/
@Schema(description = "车牌号")
private String plateNo;
/**
* 所有人:行驶证登记的单位名称,不是司机
*/
@Schema(description = "所有人(行驶证登记的单位名称,不是司机)")
private String ownerName;
/**
* 车牌颜色
*/
@@ -73,6 +83,11 @@ public class TransportVehicle extends TenantEntity {
*/
@Schema(description = "车辆类型")
private String vehicleType;
/**
* 车辆识别代号(车架号)
*/
@Schema(description = "车辆识别代号(车架号)")
private String vin;
/**
* 外廓长度,毫米
*/
@@ -112,6 +127,7 @@ public class TransportVehicle extends TenantEntity {
* 强制报废日期
*/
@Schema(description = "强制报废日期")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate compulsoryScrapDate;
/**
* 强制报废长期有效
@@ -132,11 +148,13 @@ public class TransportVehicle extends TenantEntity {
* 行驶证有效期起
*/
@Schema(description = "行驶证有效期起")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate drivingLicenseStartDate;
/**
* 行驶证有效期止
*/
@Schema(description = "行驶证有效期止")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate drivingLicenseEndDate;
/**
* 行驶证长期有效
@@ -172,11 +190,13 @@ public class TransportVehicle extends TenantEntity {
* 道路运输证有效期起
*/
@Schema(description = "道路运输证有效期起")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate roadTransportCertStartDate;
/**
* 道路运输证有效期止
*/
@Schema(description = "道路运输证有效期止")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate roadTransportCertEndDate;
/**
* 道路运输证长期有效
@@ -192,6 +212,7 @@ public class TransportVehicle extends TenantEntity {
* 道路运输年审有效期
*/
@Schema(description = "道路运输年审有效期")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate annualReviewEndDate;
/**
* 道路运输年审长期有效
@@ -207,6 +228,7 @@ public class TransportVehicle extends TenantEntity {
* 机动车登记日期
*/
@Schema(description = "机动车登记日期")
@JsonDeserialize(using = FlexibleLocalDateDeserializer.class)
private LocalDate registrationDate;
/**
* 机动车登记本图片
@@ -224,5 +246,21 @@ public class TransportVehicle extends TenantEntity {
/** 认证驳回原因 */
@Schema(description = "认证驳回原因")
private String certificationRejectReason;
/**
* 生效快照指针:指向 blade_transport_cert_record.id
* <p>
* 「该车辆是否已认证」的<b>唯一</b>判据。为 null 表示从未认证通过;
* 一经设置<b>永不回退</b>——重新提交、待审、被驳回都不得修改它,
* 因此重新提交认证不会让一辆已认证的车掉出可用池。
* {@link #certificationStatus} 是「最新提交状态」的展示缓存,不代表是否已认证。
* <p>
* ⚠️ {@code READ_ONLY}:本字段<b>只能由审核通过时</b>写入。
* {@code POST /transport-vehicle/submit} 直接以本实体接收请求体,
* 若允许反序列化,调用方就能自行填一个记录 ID 把自己「认证」掉,绕过审核。
*/
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@JsonSerialize(using = ToStringSerializer.class)
@Schema(description = "生效快照指针")
private Long effectiveCertRecordId;
}
@@ -25,6 +25,8 @@ package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.NullSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -82,6 +84,7 @@ public class Waybill extends TenantEntity {
@Schema(description = "型号")
private String model;
@JsonSerialize(nullsUsing = NullSerializer.class)
@Schema(description = "数量")
private BigDecimal quantity;
@@ -162,7 +165,7 @@ public class Waybill extends TenantEntity {
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String driverRejectReason;
@Schema(description = "车/船/航班/班列号")
@Schema(description = "车牌号/船班号/船次号/班次号")
private String vehicleNo;
@Schema(description = "船长")
@@ -183,6 +186,7 @@ public class Waybill extends TenantEntity {
@Schema(description = "押运人手机号")
private String escortPhone;
@JsonSerialize(nullsUsing = NullSerializer.class)
@Schema(description = "里程")
private BigDecimal mileage;
@@ -195,12 +199,14 @@ public class Waybill extends TenantEntity {
@Schema(description = "预计完成日期")
private LocalDate estimatedEndTime;
@JsonSerialize(nullsUsing = NullSerializer.class)
@Schema(description = "单价")
private BigDecimal unitPrice;
@Schema(description = "计价单位")
private String priceUnit;
@JsonSerialize(nullsUsing = NullSerializer.class)
@Schema(description = "其他费用合计")
private BigDecimal otherFeeTotal;
@@ -234,6 +240,9 @@ public class Waybill extends TenantEntity {
@Schema(description = "配载单号")
private String loadingNo;
@Schema(description = "配载标识号")
private String loadingIdentifier;
@Schema(description = "运单批次号")
private String batchNo;
@@ -80,6 +80,13 @@ public class AccidentRecordVO extends AccidentRecord {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
/**
* 创建人姓名
*/
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
/**
* 更新人姓名
*/
@@ -44,4 +44,7 @@ public class AdminHomeBadgesVO implements Serializable {
@Schema(description = "风险记录待办数(disposalStatus=pending)")
private long risk;
@Schema(description = "重新派单待办数(司机已拒单且运单未取消未完成)")
private long pendingReassign;
}
@@ -49,7 +49,7 @@ public class AdminHomeVO implements Serializable {
@Schema(description = "快捷入口角标")
private AdminHomeBadgesVO badges = new AdminHomeBadgesVO();
@Schema(description = "待处理事项(异常处置状态≠已完成)")
@Schema(description = "待处理事项(异常处置状态≠已完成,或司机已拒单待重新派单)")
private List<AdminTodoItemVO> feed = new ArrayList<>();
}
@@ -68,6 +68,10 @@ public class AnnualInspectionRecordVO extends AnnualInspectionRecord {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
@TableField(exist = false)
@Schema(description = "更新人姓名")
private String updateUserName;
@@ -56,4 +56,14 @@ public class BaiduOcrResultVO implements Serializable {
@Schema(description = "百度OCR原始返回结果")
private Map<String, Object> result;
/**
* 归一化后的业务字段
* <p>
* 键与提交请求里的字段同名({@code plateNo}、{@code ownerName}…),调用方直接回填即可,
* 不需要认识百度那套中文键名。只包含**识别到**的字段,没识别到的不出现(而不是给空串),
* 调用方据此就能区分「没识别到」与「识别到空值」,不会把用户已填的内容覆盖成空。
*/
@Schema(description = "归一化后的业务字段,键与提交请求同名")
private Map<String, Object> fields;
}
@@ -0,0 +1,97 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 认证审核详情视图
* <p>
* 审核是「拿新提交的内容和当前生效的内容比」的过程,所以这里**同时**给出两侧:
* {@code pendingSnapshotJson} 是本次待审的全量内容,
* {@code effectiveSnapshotJson} 是该对象当前生效的全量内容。
* 前端对这两个 JSON 做字段级 diff,即可看出司机这次改了什么。
* <p>
* 两侧的 JSON 结构完全一致(同一套提交结构),因此可以逐键直接比较。
*
* @author Chill
*/
@Data
@Schema(description = "认证审核详情")
public class CertAuditDetailVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "认证记录主键")
private Long id;
@Schema(description = "认证对象类型:driver-司机,vehicle-车辆")
private String bizType;
@Schema(description = "认证对象主键")
private Long bizId;
@Schema(description = "展示键:司机姓名 / 车牌号")
private String bizKey;
@Schema(description = "审核状态:0审核中 1已通过 2已驳回 3已作废")
private Integer status;
@Schema(description = "状态中文文案")
private String statusText;
@Schema(description = "本次提交时间")
private LocalDateTime submitTime;
@Schema(description = "本次提交人")
private Long submitUser;
@Schema(description = "本次提交人姓名")
private String submitUserName;
@Schema(description = "本次审核人;未审核时为 null")
private Long auditUser;
@Schema(description = "本次审核人姓名;未审核时为空")
private String auditUserName;
@Schema(description = "审核时间(通过或驳回的时间);未审核时为 null")
private LocalDateTime auditTime;
@Schema(description = "待审快照:本次提交的全量内容")
private String pendingSnapshotJson;
@Schema(description = "生效快照:该对象当前生效的全量内容;从未认证通过时为 null")
private String effectiveSnapshotJson;
@Schema(description = "生效快照记录ID;null 表示该对象从未认证通过")
private Long effectiveRecordId;
/**
* 该对象是否已认证
* <p>
* 审核页据此提示「本次驳回不会影响该对象继续使用」——
* 已认证的对象即便本次被驳回,既得认证依然有效。
*/
@TableField(exist = false)
@Schema(description = "该对象当前是否已认证")
private Boolean certified;
}
@@ -58,6 +58,10 @@ public class CustomerArchiveVO extends CustomerArchive {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
@TableField(exist = false)
@Schema(description = "联系人")
private List<CustomerContactVO> contacts = new ArrayList<>();
@@ -49,4 +49,7 @@ public class DriverNodePunchVO implements Serializable {
@Schema(description = "数量")
private String quantity;
@Schema(description = "本次打卡是否使运单自动完成")
private Boolean waybillCompleted;
}
@@ -4,6 +4,8 @@
*/
package org.springblade.transport.pojo.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -34,9 +36,37 @@ public class DriverPunchNodeVO implements Serializable {
@Schema(description = "是否已打卡(在途=今日已打)")
private Boolean done;
@Schema(description = "当前是否可打卡(顺序门禁 + 在途频次时段)")
@Schema(description = "当前是否可打卡(已打卡、被异常阻断、在途频次时段都会置否)")
private Boolean actionable;
@Schema(description = "是否被未了结的异常阻断:异常所在节点及其后的节点不可打卡;在途只要有未了结异常即不可打卡")
private Boolean blocked;
@Schema(description = "是否因前置节点未打卡而被顺序门禁拦住(在途豁免,恒为否)")
private Boolean blockedByOrder;
@Schema(description = "顺序门禁拦住的原因,如「请先完成装货打卡」")
private String orderLockReason;
@Schema(description = "是否允许展开:顺序门禁不允许,异常门禁允许(可查看回填内容)")
private Boolean expandable;
@Schema(description = "该节点自身是否被上报了未了结异常(仅异常所在节点为是,其后被牵连阻断的节点为否)")
private Boolean hasException;
@Schema(description = "该运单是否存在未了结的整单异常;整单异常不归属任何节点,故每个节点都带此标记")
private Boolean hasWaybillException;
@Schema(description = "与该节点相关的未了结异常ID,按上报时间升序;相关 = 归属该节点 ∪ 整单异常")
private List<String> relatedExceptionIds = new ArrayList<>();
@Schema(description = "阻断该节点的异常处置ID(司机端「异常详情」跳转用);未被阻断或异常已不存在时为空")
@JsonSerialize(using = ToStringSerializer.class)
private Long blockedExceptionId;
@Schema(description = "不可打卡的原因,供司机端展示")
private String lockReason;
@Schema(description = "是否展示该卡(在途可能因频次/时段隐藏)")
private Boolean visible;
@@ -69,4 +69,8 @@ public class DriverVO extends Driver {
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate warningDate;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
}
@@ -44,29 +44,89 @@ public class DriverVehicleCardVO implements Serializable {
@Schema(description = "车牌号")
private String plateNo;
@Schema(description = "所有人(行驶证登记的单位名称,不是司机)")
private String ownerName;
@Schema(description = "车牌颜色")
private String plateColor;
@Schema(description = "车辆类型")
private String vehicleType;
@Schema(description = "行驶证主页照片")
private String licenseFrontUrl;
@Schema(description = "行驶证副页照片")
private String licenseBackUrl;
@Schema(description = "道路运输证号")
private String roadTransportNo;
@Schema(description = "道路运输证照片")
private String roadTransportUrl;
@Schema(description = "车架号")
@Schema(description = "车辆识别代号(车架号),选填")
private String vin;
@Schema(description = "发动机号")
private String engineNo;
@Schema(description = "外廓长度,毫米")
private Integer outerLength;
@Schema(description = "外廓宽度,毫米")
private Integer outerWidth;
@Schema(description = "外廓高度,毫米")
private Integer outerHeight;
@Schema(description = "核定载质量,KG")
private Integer approvedLoadKg;
@Schema(description = "准牵引总质量,KG")
private Integer tractionMassKg;
@Schema(description = "能源类型")
private String energyType;
@Schema(description = "业务关系:自有 / 管理挂靠 / 业务合作")
private String businessRelation;
@Schema(description = "强制报废日期")
private String compulsoryScrapDate;
@Schema(description = "强制报废长期有效:0否 1是")
private Integer compulsoryScrapLongTerm;
@Schema(description = "行驶证档案编号")
private String drivingLicenseNo;
@Schema(description = "行驶证主页正面照片")
private String drivingLicenseImage;
@Schema(description = "行驶证主页反面照片")
private String drivingLicenseMainBack;
@Schema(description = "行驶证副页正面照片")
private String drivingLicenseViceFront;
@Schema(description = "行驶证副页反面照片")
private String drivingLicenseViceBack;
@Schema(description = "行驶证有效期起")
private String drivingLicenseStartDate;
@Schema(description = "行驶证有效期止")
private String licenseValidEnd;
private String drivingLicenseEndDate;
@Schema(description = "行驶证长期有效:0否 1是")
private Integer drivingLicenseLongTerm;
@Schema(description = "道路运输证号")
private String roadTransportCertNo;
@Schema(description = "道路运输证照片")
private String roadTransportCertImage;
@Schema(description = "道路运输证有效期起")
private String roadTransportCertStartDate;
@Schema(description = "道路运输证有效期止")
private String roadTransportCertEndDate;
@Schema(description = "道路运输证长期有效:0否 1是")
private Integer roadTransportCertLongTerm;
@Schema(description = "机动车登记编号")
private String registrationNo;
@Schema(description = "机动车登记本照片")
private String registrationImage;
@Schema(description = "认证状态:0认证中 1认证通过 2认证驳回")
private Integer certificationStatus;
@@ -25,5 +25,8 @@ public class EquipmentLedgerVO extends EquipmentLedger {
@Serial
private static final long serialVersionUID = 1L;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
@TableField(exist = false)
private String updateUserName;
}
@@ -26,6 +26,10 @@ public class EtcRecordVO extends EtcRecord {
@Serial
private static final long serialVersionUID = 1L;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
@TableField(exist = false)
@Schema(description = "更新人姓名")
private String updateUserName;
@@ -9,6 +9,7 @@
package org.springblade.transport.pojo.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.transport.pojo.entity.FormalSettlement;
@@ -18,6 +19,7 @@ import org.springblade.transport.pojo.entity.FormalSettlementInvoice;
import org.springblade.transport.pojo.entity.FormalSettlementSource;
import org.springblade.transport.pojo.entity.FormalSettlementPayment;
import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
@@ -39,6 +41,15 @@ public class FormalSettlementVO extends FormalSettlement {
@TableField(exist = false) private String ids;
@TableField(exist = false) private String approvalStatusName;
@TableField(exist = false) private String settlementTypeName;
@TableField(exist = false) private Integer attachmentCount;
@TableField(exist = false) private String attachmentSyncStatus;
@TableField(exist = false) private String attachmentSyncStatusName;
@TableField(exist = false) @Schema(description = "金蝶锁状态:locked-锁定中/failed-锁定失败;unlocked为终态不返回,null=无锁或已解锁")
private String kingdeeLockStatus;
@TableField(exist = false) @Schema(description = "金蝶锁失败原因(仅锁定失败时有值)")
private String kingdeeLockFailReason;
@TableField(exist = false) @Schema(description = "金蝶锁重试次数(达上限5后不再自动补偿,需重推同步金蝶)")
private Integer kingdeeTryCount;
@TableField(exist = false) private String preSettlementNos;
@TableField(exist = false) private String preSettlementNo;
@TableField(exist = false) private List<FormalSettlementSource> sources;
@@ -46,6 +57,8 @@ public class FormalSettlementVO extends FormalSettlement {
@TableField(exist = false) private List<FormalSettlementSummaryFee> summaryFees;
@TableField(exist = false) private List<FormalSettlementPayment> payments;
@TableField(exist = false) private List<FormalSettlementInvoice> invoices;
@TableField(exist = false) @Schema(description = "开票申请经金蝶回写的发票结果(含文件地址,只读展示)")
private List<InvoiceApplicationInvoice> issuedInvoices;
@TableField(exist = false) private List<PaymentApplicationVO> paymentApplications;
@TableField(exist = false) private List<SettlementAdjustmentVO> adjustments;
@TableField(exist = false) private List<FormalSettlementChangeRecord> changeRecords;
@@ -63,6 +63,13 @@ public class InsuranceRecordVO extends InsuranceRecord {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
/**
* 创建人姓名
*/
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
/**
* 更新人姓名
*/
@@ -49,6 +49,12 @@ public class InvoiceApplicationVO extends InvoiceApplication {
@TableField(exist = false) private String settlementNos;
@TableField(exist = false) private String approvalStatusName;
@TableField(exist = false) private String kingdeeStatusName;
@TableField(exist = false) private Integer attachmentCount;
@TableField(exist = false) private String attachmentSyncStatus;
@TableField(exist = false) private String attachmentSyncStatusName;
@TableField(exist = false) private String kingdeeLockStatus;
@TableField(exist = false) private String kingdeeLockFailReason;
@TableField(exist = false) private Integer kingdeeTryCount;
@TableField(exist = false) private String createUserName;
@TableField(exist = false) private List<InvoiceApplicationSettlement> settlements;
@TableField(exist = false) private List<InvoiceApplicationSheetVO> sheets;
@@ -49,6 +49,9 @@ public class InvoiceReceiptVO extends InvoiceReceipt {
@TableField(exist = false) private String settlementNos;
@TableField(exist = false) private String approvalStatusName;
@TableField(exist = false) private String kingdeePushStatusName;
@TableField(exist = false) private Integer attachmentCount;
@TableField(exist = false) private String attachmentSyncStatus;
@TableField(exist = false) private String attachmentSyncStatusName;
@TableField(exist = false) private String createUserName;
@TableField(exist = false) private String updateUserName;
@TableField(exist = false) private List<InvoiceReceiptSettlement> settlements;
@@ -62,6 +62,13 @@ public class MaintenancePlanVO extends MaintenancePlan {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
/**
* 创建人姓名
*/
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
/**
* 更新人姓名
*/
@@ -62,6 +62,13 @@ public class MaintenanceRecordVO extends MaintenanceRecord {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
/**
* 创建人姓名
*/
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
/**
* 更新人姓名
*/
@@ -41,6 +41,11 @@ public class MasterOrderVO extends MasterOrder {
private List<Waybill> boundWaybills;
@TableField(exist = false)
private BigDecimal totalQuantity;
/**
* 各品类已调度量(按货物名称+类型聚合,取各分段已调度量的最大值)
*/
@TableField(exist = false)
private Map<String, BigDecimal> dispatchedGoods;
@TableField(exist = false)
private String createUserName;
@TableField(exist = false)
@@ -66,6 +66,10 @@ public class MileageRecordVO extends MileageRecord {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
@TableField(exist = false)
@Schema(description = "更新人姓名")
private String updateUserName;
@@ -76,6 +76,10 @@ public class OilElectricRecordVO extends OilElectricRecord {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
@TableField(exist = false)
@Schema(description = "更新人姓名")
private String updateUserName;
@@ -49,6 +49,10 @@ public class OtherExpenseRecordVO extends OtherExpenseRecord {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
@TableField(exist = false)
@Schema(description = "更新人姓名")
private String updateUserName;
@@ -43,6 +43,12 @@ public class PaymentApplicationVO extends PaymentApplication {
@TableField(exist = false) private String paymentTypeName;
@TableField(exist = false) private String approvalStatusName;
@TableField(exist = false) private String kingdeeStatusName;
@TableField(exist = false) private Integer attachmentCount;
@TableField(exist = false) private String attachmentSyncStatus;
@TableField(exist = false) private String attachmentSyncStatusName;
@TableField(exist = false) private String kingdeeLockStatus;
@TableField(exist = false) private String kingdeeLockFailReason;
@TableField(exist = false) private Integer kingdeeTryCount;
@TableField(exist = false) private List<PaymentApplicationInvoice> invoices;
@TableField(exist = false) private List<PaymentApplicationRecord> paymentRecords;
@TableField(exist = false) private List<PaymentApplicationSettlement> settlements;
@@ -76,6 +76,11 @@ public class ReceiptClaimRecordVO extends ReceiptClaim {
private String claimStatusName;
@TableField(exist = false)
private String kingdeeBillStatusName;
/**
* 关联流水推送状态(none未推送/pushed已推送/failed推送失败,查询条件)
*/
@TableField(exist = false)
private String pushStatus;
@TableField(exist = false)
private List<ReceiptClaimSettlement> settlements;
@TableField(exist = false)
@@ -58,6 +58,10 @@ public class ReceiptFlowVO extends KingdeeReceiptFlow {
@TableField(exist = false)
private String kingdeeClaimStatusName;
@TableField(exist = false)
private String pushStatusName;
@TableField(exist = false)
private String kingdeeBillStatusName;
@TableField(exist = false)
private BigDecimal remainingAmount;
@TableField(exist = false)
private String createUserName;
@@ -75,6 +79,16 @@ public class ReceiptFlowVO extends KingdeeReceiptFlow {
@TableField(exist = false)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime transactionEndTime;
/**
* 收款金额区间-最小值(查询条件)
*/
@TableField(exist = false)
private BigDecimal receiptAmountStart;
/**
* 收款金额区间-最大值(查询条件)
*/
@TableField(exist = false)
private BigDecimal receiptAmountEnd;
@TableField(exist = false)
private List<ReceiptFlowRecord> claimRecords = new ArrayList<>();
}
@@ -63,6 +63,13 @@ public class TireReplacementRecordVO extends TireReplacementRecord {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
/**
* 创建人姓名
*/
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
/**
* 更新人姓名
*/
@@ -0,0 +1,100 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.transport.pojo.entity.TransportCertRecord;
import java.io.Serial;
/**
* 认证记录视图实体类
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "认证记录")
public class TransportCertRecordVO extends TransportCertRecord {
@Serial
private static final long serialVersionUID = 1L;
/**
* 创建人姓名
*/
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
/**
* 更新人姓名
*/
@TableField(exist = false)
@Schema(description = "更新人姓名")
private String updateUserName;
/**
* 提交人姓名
*/
@TableField(exist = false)
@Schema(description = "提交人姓名")
private String submitUserName;
/**
* 审核人姓名
*/
@TableField(exist = false)
@Schema(description = "审核人姓名")
private String auditUserName;
/**
* 状态中文文案:审核中 / 已通过 / 已驳回 / 已作废
* <p>
* 由服务层统一翻译后下发,避免小程序、Web、接口三处各造一套说法。
*/
@TableField(exist = false)
@Schema(description = "状态中文文案")
private String statusText;
/**
* 司机姓名(认证对象档案的**当前**姓名,不是提交时的历史值)
* <p>
* 司机侧取认证对象本身;车辆侧取提交该记录的司机。
* 解析不到档案时为空——存量订正记录里没有账号的那些司机补不出人,如实留空。
*/
@TableField(exist = false)
@Schema(description = "司机姓名")
private String driverName;
/**
* 司机手机号(认证对象档案的**当前**手机号,明文下发)
* <p>
* 审核员靠它联系司机核实材料,故不脱敏;认证快照里从来没有手机号,
* 它只有司机档案这一个来源。
*/
@TableField(exist = false)
@Schema(description = "司机手机号")
private String driverMobile;
}
@@ -57,6 +57,10 @@ public class TransportChangeRecordVO extends TransportChangeRecord {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
@TableField(exist = false)
@Schema(description = "更新人姓名")
private String updateUserName;
@@ -62,4 +62,8 @@ public class TransportShipVO extends TransportShip {
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate warningDate;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
}
@@ -76,4 +76,8 @@ public class TransportVehicleVO extends TransportVehicle {
@Schema(description = "绑定司机")
private String boundDriver;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
}
@@ -63,6 +63,13 @@ public class ViolationRecordVO extends ViolationRecord {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTimeEnd;
/**
* 创建人姓名
*/
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
/**
* 更新人姓名
*/
@@ -25,6 +25,7 @@ public class VoucherFolderVO implements Serializable {
private String folderName;
private Long waybillId;
private String waybillNo;
private String loadingNo;
private Integer matched;
private String processStatus;
private String fileName;
@@ -0,0 +1,26 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/** 运单批量导入单行校验错误。 */
@Data
@Schema(description = "运单批量导入单行校验错误")
public class WaybillImportRowErrorVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "行号(从0开始,相对当前批次)")
private Integer rowIndex;
@Schema(description = "错误信息")
private String errorMessage;
}
@@ -0,0 +1,28 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/** 运单批量导入校验结果。 */
@Data
@Schema(description = "运单批量导入校验结果")
public class WaybillImportValidateResultVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "是否全部通过")
private Boolean success;
@Schema(description = "校验错误列表")
private List<WaybillImportRowErrorVO> errors = new ArrayList<>();
}
@@ -66,6 +66,10 @@ public class WaybillVO extends Waybill {
@Schema(description = "业务状态名称")
private String businessStatusName;
@TableField(exist = false)
@Schema(description = "运输方式名称")
private String transportTypeName;
@TableField(exist = false)
@Schema(description = "是否需要司机确认接单(过程配置接单节点)")
private Boolean requireAccept;
@@ -0,0 +1,114 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.cache;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.SpringUtil;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.pojo.entity.MpUser;
/**
* 小程序用户缓存
*
* @author Chill
*/
@Slf4j
public class MpUserCache {
public static final String MP_USER_CACHE = "blade:mp_user";
private static final String MP_USER_CACHE_ID = "mp-user:id:";
private static IMpUserClient mpUserClient;
private static IMpUserClient getMpUserClient() {
if (mpUserClient == null) {
mpUserClient = SpringUtil.getBean(IMpUserClient.class);
}
return mpUserClient;
}
/**
* 按主键获取小程序用户
*/
public static MpUser getUser(Long userId) {
if (userId == null) {
return null;
}
try {
return CacheUtil.get(MP_USER_CACHE, MP_USER_CACHE_ID, userId, () -> loadUserById(userId));
} catch (RuntimeException exception) {
log.warn("小程序用户缓存读取失败,改为直连查询, userId={}", userId, exception);
return loadUserById(userId);
}
}
/**
* 获取小程序用户真实姓名(优先 realName → name → account → id)
*/
public static String getUserRealName(Long userId) {
if (userId == null) {
return StringPool.EMPTY;
}
try {
MpUser user = getUser(userId);
if (user == null) {
return Func.toStr(userId);
}
if (StringUtil.isNotBlank(user.getRealName())) {
return user.getRealName();
}
if (StringUtil.isNotBlank(user.getName())) {
return user.getName();
}
if (StringUtil.isNotBlank(user.getAccount())) {
return user.getAccount();
}
return Func.toStr(userId);
} catch (RuntimeException exception) {
log.warn("翻译小程序用户姓名失败,回退为用户ID, userId={}", userId, exception);
return Func.toStr(userId);
}
}
/**
* 清理小程序用户缓存
*/
public static void clear() {
CacheUtil.clear(MP_USER_CACHE);
CacheUtil.clear(MP_USER_CACHE, Boolean.FALSE);
}
private static MpUser loadUserById(Long userId) {
R<MpUser> result = getMpUserClient().userById(userId);
if (result == null || !result.isSuccess()) {
return null;
}
return result.getData();
}
}
@@ -25,6 +25,7 @@
*/
package org.springblade.system.cache;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
@@ -42,6 +43,7 @@ import static org.springblade.core.launch.constant.FlowConstant.TASK_USR_PREFIX;
*
* @author Chill
*/
@Slf4j
public class UserCache {
private static final String USER_CACHE_ID = "user:id:";
private static final String USER_CACHE_ACCOUNT = "user:account:";
@@ -73,10 +75,18 @@ public class UserCache {
* @return
*/
public static User getUser(Long userId) {
return CacheUtil.get(USER_CACHE, USER_CACHE_ID, userId, () -> {
R<User> result = getUserClient().userInfoById(userId);
return result.getData();
});
try {
return CacheUtil.get(USER_CACHE, USER_CACHE_ID, userId, () -> loadUserById(userId));
} catch (RuntimeException exception) {
log.warn("用户缓存读取失败,清理损坏缓存后重试, userId={}", userId, exception);
clearBrokenUserCache();
try {
return CacheUtil.get(USER_CACHE, USER_CACHE_ID, userId, () -> loadUserById(userId));
} catch (RuntimeException retryException) {
log.warn("用户缓存重试仍失败,改为直连查询, userId={}", userId, retryException);
return loadUserById(userId);
}
}
}
/**
@@ -89,20 +99,25 @@ public class UserCache {
if (userId == null) {
return StringPool.EMPTY;
}
User user = getUser(userId);
if (user == null) {
try {
User user = getUser(userId);
if (user == null) {
return Func.toStr(userId);
}
if (StringUtil.isNotBlank(user.getRealName())) {
return user.getRealName();
}
if (StringUtil.isNotBlank(user.getName())) {
return user.getName();
}
if (StringUtil.isNotBlank(user.getAccount())) {
return user.getAccount();
}
return Func.toStr(userId);
} catch (RuntimeException exception) {
log.warn("翻译用户姓名失败,回退为用户ID, userId={}", userId, exception);
return Func.toStr(userId);
}
if (StringUtil.isNotBlank(user.getRealName())) {
return user.getRealName();
}
if (StringUtil.isNotBlank(user.getName())) {
return user.getName();
}
if (StringUtil.isNotBlank(user.getAccount())) {
return user.getAccount();
}
return Func.toStr(userId);
}
/**
@@ -113,10 +128,34 @@ public class UserCache {
* @return
*/
public static User getUser(String tenantId, String account) {
return CacheUtil.get(USER_CACHE, USER_CACHE_ACCOUNT, tenantId + StringPool.DASH + account, () -> {
R<User> result = getUserClient().userByAccount(tenantId, account);
return result.getData();
});
String accountKey = tenantId + StringPool.DASH + account;
try {
return CacheUtil.get(USER_CACHE, USER_CACHE_ACCOUNT, accountKey, () -> loadUserByAccount(tenantId, account));
} catch (RuntimeException exception) {
log.warn("用户缓存读取失败,清理损坏缓存后重试, accountKey={}", accountKey, exception);
clearBrokenUserCache();
try {
return CacheUtil.get(USER_CACHE, USER_CACHE_ACCOUNT, accountKey, () -> loadUserByAccount(tenantId, account));
} catch (RuntimeException retryException) {
log.warn("用户缓存重试仍失败,改为直连查询, accountKey={}", accountKey, retryException);
return loadUserByAccount(tenantId, account);
}
}
}
private static User loadUserById(Long userId) {
R<User> result = getUserClient().userInfoById(userId);
return result.getData();
}
private static User loadUserByAccount(String tenantId, String account) {
R<User> result = getUserClient().userByAccount(tenantId, account);
return result.getData();
}
private static void clearBrokenUserCache() {
CacheUtil.clear(USER_CACHE);
CacheUtil.clear(USER_CACHE, Boolean.FALSE);
}
}
@@ -0,0 +1,74 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.cache;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.User;
/**
* 用户展示名解析(系统用户优先,未命中再查小程序用户)
* <p>
* 小程序链路写入的审计人/提交人 ID 落在 blade_mp_user,需用本工具翻译姓名。
*
* @author Chill
*/
public final class UserDisplayCache {
private UserDisplayCache() {
}
/**
* 翻译用户姓名:先 blade_user,再 blade_mp_user,都没有则回退 ID
*/
public static String getUserRealName(Long userId) {
if (userId == null) {
return StringPool.EMPTY;
}
User user = UserCache.getUser(userId);
if (user != null && Func.isNotEmpty(user.getId())) {
return resolveName(user.getRealName(), user.getName(), user.getAccount(), userId);
}
MpUser mpUser = MpUserCache.getUser(userId);
if (mpUser != null && Func.isNotEmpty(mpUser.getId())) {
return resolveName(mpUser.getRealName(), mpUser.getName(), mpUser.getAccount(), userId);
}
return Func.toStr(userId);
}
private static String resolveName(String realName, String name, String account, Long userId) {
if (StringUtil.isNotBlank(realName)) {
return realName;
}
if (StringUtil.isNotBlank(name)) {
return name;
}
if (StringUtil.isNotBlank(account)) {
return account;
}
return Func.toStr(userId);
}
}
@@ -0,0 +1,121 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.feign;
import org.springblade.core.launch.constant.AppConstant;
import org.springblade.core.tool.api.R;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 小程序用户 Feign 接口
*
* @author Chill
*/
@FeignClient(
value = AppConstant.APPLICATION_SYSTEM_NAME
)
public interface IMpUserClient {
String API_PREFIX = "/feign/client/mp-user";
String USER_BY_ID = API_PREFIX + "/user-by-id";
String USER_BY_PHONE = API_PREFIX + "/user-by-phone";
String USER_BY_ACCOUNT = API_PREFIX + "/user-by-account";
String USER_INFO_BY_ID = API_PREFIX + "/info-by-id";
String USER_INFO_BY_PHONE = API_PREFIX + "/info-by-phone";
String USER_INFO_BY_ACCOUNT = API_PREFIX + "/info-by-account";
String SAVE_USER = API_PREFIX + "/save-user";
String UPDATE_USER = API_PREFIX + "/update-user";
String BIND_OPENID = API_PREFIX + "/bind-openid";
String SET_PASSWORD = API_PREFIX + "/set-password";
/**
* 按主键查询
*/
@GetMapping(USER_BY_ID)
R<MpUser> userById(@RequestParam("userId") Long userId);
/**
* 按租户 + 手机号查询
*/
@GetMapping(USER_BY_PHONE)
R<MpUser> userByPhone(@RequestParam("tenantId") String tenantId, @RequestParam("phone") String phone);
/**
* 按租户 + 账号查询
*/
@GetMapping(USER_BY_ACCOUNT)
R<MpUser> userByAccount(@RequestParam("tenantId") String tenantId, @RequestParam("account") String account);
/**
* 按主键查询登录信息
*/
@GetMapping(USER_INFO_BY_ID)
R<MpUserInfo> userInfoById(@RequestParam("userId") Long userId);
/**
* 按租户 + 手机号查询登录信息
*/
@GetMapping(USER_INFO_BY_PHONE)
R<MpUserInfo> userInfoByPhone(@RequestParam("tenantId") String tenantId, @RequestParam("phone") String phone);
/**
* 按租户 + 账号查询登录信息
*/
@GetMapping(USER_INFO_BY_ACCOUNT)
R<MpUserInfo> userInfoByAccount(@RequestParam("tenantId") String tenantId, @RequestParam("account") String account);
/**
* 新增小程序用户
*/
@PostMapping(SAVE_USER)
R<Boolean> saveUser(@RequestBody MpUser user);
/**
* 更新小程序用户
*/
@PostMapping(UPDATE_USER)
R<Boolean> updateUser(@RequestBody MpUser user);
/**
* 绑定/更新微信小程序 openid
*/
@PostMapping(BIND_OPENID)
R<Boolean> bindOpenId(@RequestParam("tenantId") String tenantId,
@RequestParam("userId") Long userId,
@RequestParam("openid") String openid);
/**
* 设置小程序用户密码
*/
@PostMapping(SET_PASSWORD)
R<Boolean> setPassword(@RequestParam("userId") Long userId,
@RequestParam("password") String password,
@RequestParam("password2") String password2);
}
@@ -0,0 +1,131 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import org.springblade.core.tool.jackson.Sensitive;
import org.springblade.core.tool.sensitive.SensitiveType;
import java.io.Serial;
import java.util.Date;
/**
* 小程序用户实体(仅用于小程序登录,与 blade_user 分离)
*
* @author Chill
*/
@Data
@TableName("blade_mp_user")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "小程序用户")
public class MpUser extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户编号
*/
@Schema(description = "用户编号")
private String code;
/**
* 账号
*/
@Schema(description = "账号")
private String account;
/**
* 密码
*/
@Schema(description = "密码")
private String password;
/**
* 昵称
*/
@Schema(description = "昵称")
private String name;
/**
* 真名
*/
@Schema(description = "真名")
private String realName;
/**
* 头像
*/
@Schema(description = "头像")
private String avatar;
/**
* 邮箱
*/
@Sensitive(type = SensitiveType.EMAIL)
@Schema(description = "邮箱")
private String email;
/**
* 手机
*/
@Sensitive(type = SensitiveType.MOBILE)
@Schema(description = "手机")
private String phone;
/**
* 生日
*/
@Schema(description = "生日")
private Date birthday;
/**
* 性别
*/
@Schema(description = "性别")
private Integer sex;
/**
* 微信小程序 openid
*/
@Schema(description = "微信小程序openid")
private String openid;
/**
* 小程序身份,逗号分隔:driver,admin
*/
@Schema(description = "小程序身份")
private String identities;
/**
* 部门id
*/
@Schema(description = "部门id")
private String deptId;
/**
* 岗位id
*/
@Schema(description = "岗位id")
private String postId;
/**
* 备注
*/
@Schema(description = "备注")
private String remark;
}
@@ -0,0 +1,69 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.pojo.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springblade.core.tool.support.Kv;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 小程序用户登录信息
*
* @author Chill
*/
@Data
@Schema(description = "小程序用户信息")
public class MpUserInfo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户基础信息
*/
@Schema(description = "小程序用户")
private MpUser user;
/**
* 身份编码集合
*/
@Schema(description = "身份集合")
private List<String> identities;
/**
* 权限标识集合
*/
@Schema(description = "权限集合")
private List<String> permissions;
/**
* 拓展信息
*/
@Schema(description = "拓展信息")
private Kv detail;
}
@@ -25,6 +25,7 @@
*/
package org.springblade.system.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@@ -131,6 +132,11 @@ public class User extends TenantEntity implements IPhone {
* 是否是oa的账号
*/
private Integer isOa;
/**
* OA上次更新时间(OA 的 modified 字段),仅同步比对使用
*/
@TableField(updateStrategy = FieldStrategy.NOT_NULL)
private String oaModified;
/**
* 是否推送mk
*/
@@ -0,0 +1,148 @@
/**
* 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>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.system.pojo.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 小程序用户身份枚举(可叠加,后续可扩展)
*
* @author Chill
*/
@Getter
@AllArgsConstructor
public enum MpUserIdentity {
/**
* 司机
*/
DRIVER("driver", "司机"),
/**
* 管理员(调度端)
*/
ADMIN("admin", "管理员"),
;
/**
* 身份编码
*/
private final String code;
/**
* 身份名称
*/
private final String label;
/**
* 管理员身份对应的小程序权限码(兼容原 mp_auth)
*/
public static final String PERMISSION_MP_AUTH = "mp_auth";
/**
* detail 中标记小程序用户的 type 值
*/
public static final String DETAIL_TYPE = "mp";
/**
* 解析身份编码
*/
public static MpUserIdentity of(String code) {
if (Func.isBlank(code)) {
return null;
}
String normalized = code.trim().toLowerCase(Locale.ROOT);
return Arrays.stream(values())
.filter(item -> item.code.equals(normalized))
.findFirst()
.orElse(null);
}
/**
* 拆分身份串为编码列表(去重、保序)
*/
public static List<String> split(String identities) {
if (Func.isBlank(identities)) {
return Collections.emptyList();
}
Set<String> identitySet = new LinkedHashSet<>();
for (String part : Func.toStrList(identities)) {
if (Func.isBlank(part)) {
continue;
}
String code = part.trim().toLowerCase(Locale.ROOT);
if (of(code) != null || StringUtil.isNotBlank(code)) {
identitySet.add(code);
}
}
return new ArrayList<>(identitySet);
}
/**
* 合并身份,确保包含指定身份
*/
public static String merge(String identities, String... appendCodes) {
Set<String> identitySet = new LinkedHashSet<>(split(identities));
if (appendCodes != null) {
for (String code : appendCodes) {
if (Func.isNotBlank(code)) {
identitySet.add(code.trim().toLowerCase(Locale.ROOT));
}
}
}
return identitySet.stream().collect(Collectors.joining(","));
}
/**
* 是否包含指定身份
*/
public static boolean has(String identities, MpUserIdentity identity) {
if (identity == null) {
return false;
}
return split(identities).contains(identity.getCode());
}
/**
* 根据身份生成小程序权限码(不再依赖系统角色菜单)
*/
public static List<String> toPermissions(String identities) {
List<String> permissions = new ArrayList<>();
if (has(identities, ADMIN)) {
permissions.add(PERMISSION_MP_AUTH);
}
return permissions;
}
}

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