Merge branch 'master' into feature/20261104-bfi-project-contract-ref-v2
This commit is contained in:
+6
@@ -87,6 +87,12 @@ public class DeptVO extends Dept implements INode<DeptVO> {
|
|||||||
*/
|
*/
|
||||||
private String parentName;
|
private String parentName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所属公司名称(按 belongCompanyCode 解析)
|
||||||
|
*/
|
||||||
|
@Schema(description = "所属公司名称")
|
||||||
|
private String belongCompanyName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 机构类型名称
|
* 机构类型名称
|
||||||
*/
|
*/
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.pojo.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从业资格证 OCR 识别结果。
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "从业资格证OCR识别结果")
|
||||||
|
public class QualificationCertificateOcrVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按第三方返回 key 聚合的字段,例如 qualification_category、certificate_no、valid_from、valid_until。
|
||||||
|
*/
|
||||||
|
@Schema(description = "识别字段,键为第三方 OCR 的 key")
|
||||||
|
private Map<String, String> fields = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
}
|
||||||
+28
@@ -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 WaybillImportDuplicateVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "行号(从0开始,相对当前批次)")
|
||||||
|
private Integer rowIndex;
|
||||||
|
|
||||||
|
@Schema(description = "命中的已有运单号")
|
||||||
|
private List<String> waybillNos = new ArrayList<>();
|
||||||
|
}
|
||||||
+2
-2
@@ -274,8 +274,8 @@ public class DeptController extends BladeController {
|
|||||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||||
@GetMapping("/business-dept-select")
|
@GetMapping("/business-dept-select")
|
||||||
@ApiOperationSupport(order = 12)
|
@ApiOperationSupport(order = 12)
|
||||||
@Operation(summary = "业务部门下拉", description = "返回是否业务部门=是的部门列表")
|
@Operation(summary = "业务部门下拉", description = "返回是否业务部门=是的部门列表(含上级组织名称)")
|
||||||
public R<List<Dept>> businessDeptSelect() {
|
public R<List<DeptVO>> businessDeptSelect() {
|
||||||
return R.data(deptService.listBusinessDept());
|
return R.data(deptService.listBusinessDept());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -25,6 +25,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.springblade.system.controller;
|
package org.springblade.system.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -37,6 +38,7 @@ import jakarta.validation.Valid;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import org.springblade.core.boot.ctrl.BladeController;
|
import org.springblade.core.boot.ctrl.BladeController;
|
||||||
import org.springblade.core.cache.utils.CacheUtil;
|
import org.springblade.core.cache.utils.CacheUtil;
|
||||||
|
import org.springblade.core.mp.support.Query;
|
||||||
import org.springblade.core.secure.BladeUser;
|
import org.springblade.core.secure.BladeUser;
|
||||||
import org.springblade.core.secure.annotation.IsAdmin;
|
import org.springblade.core.secure.annotation.IsAdmin;
|
||||||
import org.springblade.core.secure.annotation.PreAuth;
|
import org.springblade.core.secure.annotation.PreAuth;
|
||||||
@@ -85,7 +87,7 @@ public class RoleController extends BladeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 列表
|
* 分页列表(按树形根节点分页)
|
||||||
*/
|
*/
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
@Parameters({
|
@Parameters({
|
||||||
@@ -93,9 +95,9 @@ public class RoleController extends BladeController {
|
|||||||
@Parameter(name = "roleAlias", description = "角色别名", in = ParameterIn.QUERY, schema = @Schema(type = "string"))
|
@Parameter(name = "roleAlias", description = "角色别名", in = ParameterIn.QUERY, schema = @Schema(type = "string"))
|
||||||
})
|
})
|
||||||
@ApiOperationSupport(order = 2)
|
@ApiOperationSupport(order = 2)
|
||||||
@Operation(summary = "列表", description = "传入role")
|
@Operation(summary = "分页", description = "传入role")
|
||||||
public R<List<RoleVO>> list(@Parameter(hidden = true) @RequestParam Map<String, Object> role) {
|
public R<IPage<RoleVO>> list(@Parameter(hidden = true) @RequestParam Map<String, Object> role, Query query) {
|
||||||
return R.data(roleService.selectList(role));
|
return R.data(roleService.selectPage(role, query));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+2
-2
@@ -109,9 +109,9 @@ public interface IDeptService extends IService<Dept> {
|
|||||||
/**
|
/**
|
||||||
* 业务部门下拉(是否业务部门=是)
|
* 业务部门下拉(是否业务部门=是)
|
||||||
*
|
*
|
||||||
* @return 业务部门列表
|
* @return 业务部门列表(含上级组织名称,便于同名部门区分)
|
||||||
*/
|
*/
|
||||||
List<Dept> listBusinessDept();
|
List<DeptVO> listBusinessDept();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据部门名称精确匹配获取部门ID集合
|
* 根据部门名称精确匹配获取部门ID集合
|
||||||
|
|||||||
+10
@@ -27,6 +27,7 @@ package org.springblade.system.service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import org.springblade.core.mp.support.Query;
|
||||||
import org.springblade.system.pojo.entity.Role;
|
import org.springblade.system.pojo.entity.Role;
|
||||||
import org.springblade.system.pojo.vo.RoleVO;
|
import org.springblade.system.pojo.vo.RoleVO;
|
||||||
|
|
||||||
@@ -58,6 +59,15 @@ public interface IRoleService extends IService<Role> {
|
|||||||
*/
|
*/
|
||||||
List<RoleVO> selectList(Map<String, Object> role);
|
List<RoleVO> selectList(Map<String, Object> role);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色分页(按树形根节点分页,子节点随根节点返回)
|
||||||
|
*
|
||||||
|
* @param role 查询条件 Map
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 角色分页结果,非超管时仅命中当前会话租户
|
||||||
|
*/
|
||||||
|
IPage<RoleVO> selectPage(Map<String, Object> role, Query query);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义角色分页查询
|
* 自定义角色分页查询
|
||||||
*
|
*
|
||||||
|
|||||||
+34
@@ -42,6 +42,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE;
|
import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE;
|
||||||
@@ -88,6 +89,39 @@ public class BusinessDeptConfigServiceImpl extends BaseServiceImpl<BusinessDeptC
|
|||||||
if (Func.isEmpty(platformCompanyId) || Func.isEmpty(businessDeptId)) {
|
if (Func.isEmpty(platformCompanyId) || Func.isEmpty(businessDeptId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
BusinessDeptConfig config = findByPlatformAndDept(platformCompanyId, businessDeptId);
|
||||||
|
if (config != null) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
// 兼容项目仍挂旧组织 ID / 同名非平台公司节点:按平台公司名称回退匹配现行配置
|
||||||
|
Dept platformCompany = deptService.getById(platformCompanyId);
|
||||||
|
String platformCompanyName = platformCompany == null ? null : trimToNull(platformCompany.getDeptName());
|
||||||
|
if (Func.isEmpty(platformCompanyName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Dept> activePlatformList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||||
|
.eq(Dept::getIsPlatformCompany, 1)
|
||||||
|
.eq(Dept::getDeptName, platformCompanyName)
|
||||||
|
.orderByAsc(Dept::getId));
|
||||||
|
for (Dept activePlatform : activePlatformList) {
|
||||||
|
if (activePlatform == null || Func.isEmpty(activePlatform.getId())
|
||||||
|
|| Objects.equals(activePlatform.getId(), platformCompanyId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
config = findByPlatformAndDept(activePlatform.getId(), businessDeptId);
|
||||||
|
if (config != null) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getOne(Wrappers.<BusinessDeptConfig>lambdaQuery()
|
||||||
|
.eq(BusinessDeptConfig::getPlatformCompanyName, platformCompanyName)
|
||||||
|
.eq(BusinessDeptConfig::getBusinessDeptId, businessDeptId)
|
||||||
|
.eq(BusinessDeptConfig::getIsDeleted, 0)
|
||||||
|
.orderByDesc(BusinessDeptConfig::getUpdateTime)
|
||||||
|
.last("LIMIT 1"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BusinessDeptConfig findByPlatformAndDept(Long platformCompanyId, Long businessDeptId) {
|
||||||
return getOne(Wrappers.<BusinessDeptConfig>lambdaQuery()
|
return getOne(Wrappers.<BusinessDeptConfig>lambdaQuery()
|
||||||
.eq(BusinessDeptConfig::getPlatformCompanyId, platformCompanyId)
|
.eq(BusinessDeptConfig::getPlatformCompanyId, platformCompanyId)
|
||||||
.eq(BusinessDeptConfig::getBusinessDeptId, businessDeptId)
|
.eq(BusinessDeptConfig::getBusinessDeptId, businessDeptId)
|
||||||
|
|||||||
+42
-2
@@ -321,7 +321,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Dept> listBusinessDept() {
|
public List<DeptVO> listBusinessDept() {
|
||||||
LambdaQueryWrapper<Dept> queryWrapper = Wrappers.<Dept>lambdaQuery()
|
LambdaQueryWrapper<Dept> queryWrapper = Wrappers.<Dept>lambdaQuery()
|
||||||
.eq(Dept::getIsBusinessDept, 1)
|
.eq(Dept::getIsBusinessDept, 1)
|
||||||
.orderByAsc(Dept::getSort)
|
.orderByAsc(Dept::getSort)
|
||||||
@@ -329,7 +329,47 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
|||||||
if (!AuthUtil.isAdministrator()) {
|
if (!AuthUtil.isAdministrator()) {
|
||||||
queryWrapper.eq(Dept::getTenantId, AuthUtil.getTenantId());
|
queryWrapper.eq(Dept::getTenantId, AuthUtil.getTenantId());
|
||||||
}
|
}
|
||||||
return list(queryWrapper);
|
List<Dept> deptList = list(queryWrapper);
|
||||||
|
if (CollectionUtil.isEmpty(deptList)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
// 批量补齐上级名称、所属公司名称:扁平下拉靠公司/上级区分同名业务部门
|
||||||
|
Set<Long> parentIdSet = deptList.stream()
|
||||||
|
.map(Dept::getParentId)
|
||||||
|
.filter(Func::isNotEmpty)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
Map<Long, String> parentNameMap = Collections.emptyMap();
|
||||||
|
if (CollectionUtil.isNotEmpty(parentIdSet)) {
|
||||||
|
parentNameMap = listByIds(parentIdSet).stream()
|
||||||
|
.filter(parent -> Func.isNotEmpty(parent.getId()))
|
||||||
|
.collect(Collectors.toMap(Dept::getId, Dept::getDeptName, (left, right) -> left));
|
||||||
|
}
|
||||||
|
Set<String> companyCodeSet = deptList.stream()
|
||||||
|
.map(Dept::getBelongCompanyCode)
|
||||||
|
.filter(Func::isNotEmpty)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
Map<String, String> companyNameMap = Collections.emptyMap();
|
||||||
|
if (CollectionUtil.isNotEmpty(companyCodeSet)) {
|
||||||
|
companyNameMap = list(Wrappers.<Dept>lambdaQuery().in(Dept::getDeptCode, companyCodeSet)).stream()
|
||||||
|
.filter(company -> Func.isNotEmpty(company.getDeptCode()))
|
||||||
|
.collect(Collectors.toMap(Dept::getDeptCode, Dept::getDeptName, (left, right) -> left));
|
||||||
|
}
|
||||||
|
Map<Long, String> finalParentNameMap = parentNameMap;
|
||||||
|
Map<String, String> finalCompanyNameMap = companyNameMap;
|
||||||
|
return deptList.stream().map(dept -> {
|
||||||
|
DeptVO deptVO = BeanUtil.copyProperties(dept, DeptVO.class);
|
||||||
|
if (deptVO == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(dept.getParentId())) {
|
||||||
|
deptVO.setParentName(finalParentNameMap.getOrDefault(dept.getParentId(), StringPool.EMPTY));
|
||||||
|
}
|
||||||
|
if (Func.isNotEmpty(dept.getBelongCompanyCode())) {
|
||||||
|
deptVO.setBelongCompanyName(
|
||||||
|
finalCompanyNameMap.getOrDefault(dept.getBelongCompanyCode(), StringPool.EMPTY));
|
||||||
|
}
|
||||||
|
return deptVO;
|
||||||
|
}).filter(Objects::nonNull).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+22
@@ -33,6 +33,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.mp.support.Condition;
|
import org.springblade.core.mp.support.Condition;
|
||||||
|
import org.springblade.core.mp.support.Query;
|
||||||
import org.springblade.core.secure.utils.AuthUtil;
|
import org.springblade.core.secure.utils.AuthUtil;
|
||||||
import org.springblade.core.tenant.TenantGuard;
|
import org.springblade.core.tenant.TenantGuard;
|
||||||
import org.springblade.core.tool.constant.BladeConstant;
|
import org.springblade.core.tool.constant.BladeConstant;
|
||||||
@@ -92,9 +93,30 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements IR
|
|||||||
public List<RoleVO> selectList(Map<String, Object> role) {
|
public List<RoleVO> selectList(Map<String, Object> role) {
|
||||||
QueryWrapper<Role> queryWrapper = Condition.getQueryWrapper(role, Role.class);
|
QueryWrapper<Role> queryWrapper = Condition.getQueryWrapper(role, Role.class);
|
||||||
applyTenantScope(queryWrapper);
|
applyTenantScope(queryWrapper);
|
||||||
|
queryWrapper.lambda().orderByAsc(Role::getSort).orderByAsc(Role::getId);
|
||||||
return RoleWrapper.build().listNodeVO(list(queryWrapper));
|
return RoleWrapper.build().listNodeVO(list(queryWrapper));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IPage<RoleVO> selectPage(Map<String, Object> role, Query query) {
|
||||||
|
List<RoleVO> tree = selectList(role);
|
||||||
|
IPage<RoleVO> page = Condition.getPage(query);
|
||||||
|
long total = tree.size();
|
||||||
|
page.setTotal(total);
|
||||||
|
if (total == 0L || page.getSize() < 1L) {
|
||||||
|
page.setRecords(Collections.emptyList());
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
int fromIndex = (int) ((page.getCurrent() - 1L) * page.getSize());
|
||||||
|
if (fromIndex >= tree.size()) {
|
||||||
|
page.setRecords(Collections.emptyList());
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
int toIndex = (int) Math.min(fromIndex + page.getSize(), tree.size());
|
||||||
|
page.setRecords(new ArrayList<>(tree.subList(fromIndex, toIndex)));
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<RoleVO> selectRolePage(IPage<RoleVO> page, RoleVO role) {
|
public IPage<RoleVO> selectRolePage(IPage<RoleVO> page, RoleVO role) {
|
||||||
return page.setRecords(baseMapper.selectRolePage(page, role));
|
return page.setRecords(baseMapper.selectRolePage(page, role));
|
||||||
|
|||||||
+17
@@ -37,8 +37,10 @@ import org.springblade.core.tool.api.R;
|
|||||||
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||||
import org.springblade.transport.ocr.service.IBaiduOcrService;
|
import org.springblade.transport.ocr.service.IBaiduOcrService;
|
||||||
import org.springblade.transport.ocr.service.IBusinessLicenseOcrService;
|
import org.springblade.transport.ocr.service.IBusinessLicenseOcrService;
|
||||||
|
import org.springblade.transport.ocr.service.IQualificationCertificateOcrService;
|
||||||
import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
|
import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
|
||||||
import org.springblade.transport.pojo.vo.BusinessLicenseOcrVO;
|
import org.springblade.transport.pojo.vo.BusinessLicenseOcrVO;
|
||||||
|
import org.springblade.transport.pojo.vo.QualificationCertificateOcrVO;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -63,6 +65,7 @@ public class BaiduOcrController extends BladeController {
|
|||||||
|
|
||||||
private final IBaiduOcrService baiduOcrService;
|
private final IBaiduOcrService baiduOcrService;
|
||||||
private final IBusinessLicenseOcrService businessLicenseOcrService;
|
private final IBusinessLicenseOcrService businessLicenseOcrService;
|
||||||
|
private final IQualificationCertificateOcrService qualificationCertificateOcrService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 识别上传的图片。
|
* 识别上传的图片。
|
||||||
@@ -126,6 +129,20 @@ public class BaiduOcrController extends BladeController {
|
|||||||
return R.data(businessLicenseOcrService.recognizeUrl(imageUrl));
|
return R.data(businessLicenseOcrService.recognizeUrl(imageUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从业资格证图片地址识别(第三方 /Biaozhunbaodan)。
|
||||||
|
*
|
||||||
|
* @param imageUrl 图片地址
|
||||||
|
* @return 识别结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/qualification-certificate")
|
||||||
|
@ApiOperationSupport(order = 4)
|
||||||
|
@Operation(summary = "从业资格证OCR识别", description = "按图片地址调用第三方道路运输从业人员从业资格证识别接口")
|
||||||
|
public R<QualificationCertificateOcrVO> recognizeQualificationCertificate(
|
||||||
|
@Parameter(description = "图片地址", required = true) @RequestParam String imageUrl) {
|
||||||
|
return R.data(qualificationCertificateOcrService.recognizeUrl(imageUrl));
|
||||||
|
}
|
||||||
|
|
||||||
private BaiduOcrType resolveType(String type) {
|
private BaiduOcrType resolveType(String type) {
|
||||||
try {
|
try {
|
||||||
return BaiduOcrType.from(type);
|
return BaiduOcrType.from(type);
|
||||||
|
|||||||
+4
-3
@@ -135,9 +135,10 @@ public class VoucherManageController extends BladeController {
|
|||||||
@ApiOperationSupport(order = 7)
|
@ApiOperationSupport(order = 7)
|
||||||
@Operation(summary = "可关联运输批次")
|
@Operation(summary = "可关联运输批次")
|
||||||
public R<IPage<Map<String, Object>>> waybillBatches(Query query, @RequestParam(required = false) String batchNo,
|
public R<IPage<Map<String, Object>>> waybillBatches(Query query, @RequestParam(required = false) String batchNo,
|
||||||
@RequestParam(required = false) String createUser, @RequestParam(required = false) Integer waybillCount,
|
@RequestParam(required = false) Long projectId, @RequestParam(required = false) String createUser,
|
||||||
@RequestParam(required = false) String createTimeStart, @RequestParam(required = false) String createTimeEnd) {
|
@RequestParam(required = false) Integer waybillCount, @RequestParam(required = false) String createTimeStart,
|
||||||
return R.data(voucherManageService.selectableWaybillBatches(Condition.getPage(query), batchNo, createUser, waybillCount, createTimeStart, createTimeEnd));
|
@RequestParam(required = false) String createTimeEnd) {
|
||||||
|
return R.data(voucherManageService.selectableWaybillBatches(Condition.getPage(query), batchNo, projectId, createUser, waybillCount, createTimeStart, createTimeEnd));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/audit-pass")
|
@PostMapping("/audit-pass")
|
||||||
|
|||||||
+8
@@ -57,6 +57,7 @@ import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
|
|||||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||||
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
||||||
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
|
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
|
||||||
|
import org.springblade.transport.pojo.vo.WaybillImportDuplicateVO;
|
||||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||||
import org.springblade.transport.pojo.vo.WaybillLocateVO;
|
import org.springblade.transport.pojo.vo.WaybillLocateVO;
|
||||||
import org.springblade.transport.pojo.vo.WaybillTrackVO;
|
import org.springblade.transport.pojo.vo.WaybillTrackVO;
|
||||||
@@ -230,6 +231,13 @@ public class WaybillController extends BladeController {
|
|||||||
waybillImportBatchService.exportFailures(request, response);
|
waybillImportBatchService.exportFailures(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/import-batch/duplicates")
|
||||||
|
@ApiOperationSupport(order = 6)
|
||||||
|
@Operation(summary = "查询与已有运单重复的导入行")
|
||||||
|
public R<List<WaybillImportDuplicateVO>> checkImportDuplicates(@RequestBody WaybillImportBatchRequest request) {
|
||||||
|
return R.data(waybillImportBatchService.checkDuplicates(request));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/import-batch/confirm")
|
@PostMapping("/import-batch/confirm")
|
||||||
@ApiOperationSupport(order = 7)
|
@ApiOperationSupport(order = 7)
|
||||||
@Operation(summary = "确认运单批量导入")
|
@Operation(summary = "确认运单批量导入")
|
||||||
|
|||||||
+3
-2
@@ -13,8 +13,9 @@ import org.apache.ibatis.annotations.Delete;
|
|||||||
public interface VoucherWaybillBatchMapper extends BaseMapper<VoucherWaybillBatch> {
|
public interface VoucherWaybillBatchMapper extends BaseMapper<VoucherWaybillBatch> {
|
||||||
@Delete("DELETE FROM blade_voucher_waybill_batch WHERE voucher_id = #{voucherId}")
|
@Delete("DELETE FROM blade_voucher_waybill_batch WHERE voucher_id = #{voucherId}")
|
||||||
int deletePhysicalByVoucherId(@Param("voucherId") Long voucherId);
|
int deletePhysicalByVoucherId(@Param("voucherId") Long voucherId);
|
||||||
IPage<Map<String, Object>> selectVoucherWaybillBatchPage(IPage<?> page, @Param("tenantId") String tenantId, @Param("batchNo") String batchNo, @Param("createUser") String createUser,
|
IPage<Map<String, Object>> selectVoucherWaybillBatchPage(IPage<?> page, @Param("tenantId") String tenantId, @Param("batchNo") String batchNo,
|
||||||
@Param("waybillCount") Integer waybillCount, @Param("createTimeStart") String createTimeStart, @Param("createTimeEnd") String createTimeEnd,
|
@Param("projectId") Long projectId, @Param("createUser") String createUser, @Param("waybillCount") Integer waybillCount,
|
||||||
|
@Param("createTimeStart") String createTimeStart, @Param("createTimeEnd") String createTimeEnd,
|
||||||
@Param("restrictCarrier") boolean restrictCarrier, @Param("carrierName") String carrierName);
|
@Param("restrictCarrier") boolean restrictCarrier, @Param("carrierName") String carrierName);
|
||||||
List<Map<String, Object>> selectWaybillBatchesByIds(@Param("tenantId") String tenantId, @Param("ids") List<Long> ids,
|
List<Map<String, Object>> selectWaybillBatchesByIds(@Param("tenantId") String tenantId, @Param("ids") List<Long> ids,
|
||||||
@Param("restrictCarrier") boolean restrictCarrier, @Param("carrierName") String carrierName);
|
@Param("restrictCarrier") boolean restrictCarrier, @Param("carrierName") String carrierName);
|
||||||
|
|||||||
+2
@@ -4,12 +4,14 @@
|
|||||||
<select id="selectVoucherWaybillBatchPage" resultType="java.util.LinkedHashMap">
|
<select id="selectVoucherWaybillBatchPage" resultType="java.util.LinkedHashMap">
|
||||||
SELECT MIN(w.id) AS id, MAX(w.create_time) AS createTime, MAX(w.create_user) AS createUser,
|
SELECT MIN(w.id) AS id, MAX(w.create_time) AS createTime, MAX(w.create_user) AS createUser,
|
||||||
MAX(u.real_name) AS createUserName, COUNT(*) AS waybillCount,
|
MAX(u.real_name) AS createUserName, COUNT(*) AS waybillCount,
|
||||||
|
CASE WHEN COUNT(DISTINCT COALESCE(w.project_id, -1)) > 1 THEN '多个项目' ELSE MAX(w.project_name) END AS projectName,
|
||||||
COALESCE(NULLIF(w.batch_no, ''), '未分批运单') AS batchNo
|
COALESCE(NULLIF(w.batch_no, ''), '未分批运单') AS batchNo
|
||||||
FROM blade_waybill w
|
FROM blade_waybill w
|
||||||
LEFT JOIN blade_user u ON u.id = w.create_user
|
LEFT JOIN blade_user u ON u.id = w.create_user
|
||||||
WHERE w.is_deleted = 0 AND w.tenant_id = #{tenantId}
|
WHERE w.is_deleted = 0 AND w.tenant_id = #{tenantId}
|
||||||
<if test="restrictCarrier">AND w.carrier_name = #{carrierName}</if>
|
<if test="restrictCarrier">AND w.carrier_name = #{carrierName}</if>
|
||||||
<if test="batchNo != null and batchNo != ''">AND w.batch_no LIKE CONCAT('%', #{batchNo}, '%')</if>
|
<if test="batchNo != null and batchNo != ''">AND w.batch_no LIKE CONCAT('%', #{batchNo}, '%')</if>
|
||||||
|
<if test="projectId != null">AND w.project_id = #{projectId}</if>
|
||||||
<if test="createUser != null and createUser != ''">AND w.create_user = #{createUser}</if>
|
<if test="createUser != null and createUser != ''">AND w.create_user = #{createUser}</if>
|
||||||
<if test="createTimeStart != null and createTimeStart != ''">AND w.create_time <![CDATA[>=]]> #{createTimeStart}</if>
|
<if test="createTimeStart != null and createTimeStart != ''">AND w.create_time <![CDATA[>=]]> #{createTimeStart}</if>
|
||||||
<if test="createTimeEnd != null and createTimeEnd != ''">AND w.create_time <![CDATA[<=]]> #{createTimeEnd}</if>
|
<if test="createTimeEnd != null and createTimeEnd != ''">AND w.create_time <![CDATA[<=]]> #{createTimeEnd}</if>
|
||||||
|
|||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* 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.ocr.service;
|
||||||
|
|
||||||
|
import org.springblade.transport.pojo.vo.QualificationCertificateOcrVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从业资格证 OCR 服务。
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
public interface IQualificationCertificateOcrService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图片地址识别道路运输从业人员从业资格证。
|
||||||
|
*
|
||||||
|
* @param imageUrl 图片地址
|
||||||
|
* @return 识别结果
|
||||||
|
*/
|
||||||
|
QualificationCertificateOcrVO recognizeUrl(String imageUrl);
|
||||||
|
|
||||||
|
}
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* 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.ocr.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
|
import org.springblade.transport.ocr.service.IQualificationCertificateOcrService;
|
||||||
|
import org.springblade.transport.pojo.vo.QualificationCertificateOcrVO;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从业资格证 OCR 服务实现,调用第三方 /Biaozhunbaodan 接口。
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class QualificationCertificateOcrServiceImpl implements IQualificationCertificateOcrService {
|
||||||
|
|
||||||
|
private static final String PATH = "/Biaozhunbaodan";
|
||||||
|
private static final int SUCCESS_CODE = 200;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从业资格证 OCR 服务地址,Nacos:thirdParty.ocr.qualificationCertificateBaseUrl
|
||||||
|
*/
|
||||||
|
@Value("${thirdParty.ocr.qualificationCertificateBaseUrl:http://124.71.22.123:8400}")
|
||||||
|
private String qualificationCertificateBaseUrl;
|
||||||
|
|
||||||
|
public QualificationCertificateOcrServiceImpl(ObjectMapper objectMapper,
|
||||||
|
@Qualifier("baiduOcrHttpClient") HttpClient httpClient) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public QualificationCertificateOcrVO recognizeUrl(String imageUrl) {
|
||||||
|
if (StringUtil.isBlank(imageUrl)) {
|
||||||
|
throw new ServiceException("OCR图片地址不能为空");
|
||||||
|
}
|
||||||
|
String normalizedUrl = imageUrl.trim();
|
||||||
|
validateImageUrl(normalizedUrl);
|
||||||
|
if (StringUtil.isBlank(qualificationCertificateBaseUrl)) {
|
||||||
|
throw new ServiceException("从业资格证OCR服务地址未配置(thirdParty.ocr.qualificationCertificateBaseUrl)");
|
||||||
|
}
|
||||||
|
String requestUrl = qualificationCertificateBaseUrl.replaceAll("/+$", "") + PATH;
|
||||||
|
Map<String, String> body = new LinkedHashMap<>();
|
||||||
|
body.put("img_path", normalizedUrl);
|
||||||
|
JsonNode response = sendRequest(requestUrl, body);
|
||||||
|
int code = response.path("code").asInt(Integer.MIN_VALUE);
|
||||||
|
if (code != SUCCESS_CODE) {
|
||||||
|
String message = firstNonBlank(response.path("msg").asText(null), response.path("message").asText(null), "未知错误");
|
||||||
|
log.warn("从业资格证OCR识别失败,code={},msg={},imageUrl={}", code, message, normalizedUrl);
|
||||||
|
throw new ServiceException("从业资格证OCR识别失败:" + message);
|
||||||
|
}
|
||||||
|
QualificationCertificateOcrVO result = new QualificationCertificateOcrVO();
|
||||||
|
result.setFields(extractFields(response.path("result").path("key_info")));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode sendRequest(String requestUrl, Map<String, String> body) {
|
||||||
|
try {
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(body);
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(requestUrl))
|
||||||
|
.timeout(Duration.ofSeconds(60))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
|
log.error("从业资格证OCR接口响应异常,status={},url={}", response.statusCode(), requestUrl);
|
||||||
|
throw new ServiceException("从业资格证OCR接口调用失败,HTTP状态码:" + response.statusCode());
|
||||||
|
}
|
||||||
|
JsonNode result = objectMapper.readTree(response.body());
|
||||||
|
if (result == null || !result.isObject()) {
|
||||||
|
throw new ServiceException("从业资格证OCR响应格式异常");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (ServiceException exception) {
|
||||||
|
throw exception;
|
||||||
|
} catch (InterruptedException exception) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.error("调用从业资格证OCR接口被中断,url={}", requestUrl, exception);
|
||||||
|
throw new ServiceException("从业资格证OCR接口调用被中断");
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
log.error("从业资格证OCR接口地址配置不正确,url={}", requestUrl);
|
||||||
|
throw new ServiceException("从业资格证OCR接口地址配置不正确");
|
||||||
|
} catch (IOException exception) {
|
||||||
|
log.error("调用从业资格证OCR接口失败,url={}", requestUrl, exception);
|
||||||
|
throw new ServiceException("从业资格证OCR接口调用失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> extractFields(JsonNode keyInfoNode) {
|
||||||
|
Map<String, String> fields = new LinkedHashMap<>();
|
||||||
|
if (keyInfoNode == null || !keyInfoNode.isArray()) {
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
for (JsonNode item : keyInfoNode) {
|
||||||
|
if (item == null || !item.isObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String key = item.path("key").asText(null);
|
||||||
|
String value = item.path("value").asText(null);
|
||||||
|
if (StringUtil.isBlank(key) || value == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String trimmed = value.trim();
|
||||||
|
if (StringUtil.isBlank(trimmed)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fields.put(key.trim(), trimmed);
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateImageUrl(String imageUrl) {
|
||||||
|
if (imageUrl.getBytes(StandardCharsets.UTF_8).length > 1024) {
|
||||||
|
throw new ServiceException("OCR图片地址长度不能超过1024字节");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
URI imageUri = URI.create(imageUrl);
|
||||||
|
String scheme = imageUri.getScheme();
|
||||||
|
if (StringUtil.isBlank(scheme) || StringUtil.isBlank(imageUri.getHost())
|
||||||
|
|| imageUri.getUserInfo() != null
|
||||||
|
|| !("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme))) {
|
||||||
|
throw new ServiceException("OCR图片地址必须是有效的HTTP或HTTPS地址");
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
throw new ServiceException("OCR图片地址格式不正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstNonBlank(String... values) {
|
||||||
|
if (values == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String value : values) {
|
||||||
|
if (StringUtil.isNotBlank(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -30,7 +30,7 @@ public interface IVoucherManageService extends BaseService<VoucherManage> {
|
|||||||
void processUploadedVoucher(Long voucherId);
|
void processUploadedVoucher(Long voucherId);
|
||||||
void reprocessUploadedVoucher(Long voucherId);
|
void reprocessUploadedVoucher(Long voucherId);
|
||||||
void removeVoucher(Long id);
|
void removeVoucher(Long id);
|
||||||
IPage<Map<String, Object>> selectableWaybillBatches(IPage<?> page, String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd);
|
IPage<Map<String, Object>> selectableWaybillBatches(IPage<?> page, String batchNo, Long projectId, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd);
|
||||||
void auditPass(Long id);
|
void auditPass(Long id);
|
||||||
void auditReject(Long id, String rejectReason);
|
void auditReject(Long id, String rejectReason);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -7,8 +7,11 @@ import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
|
|||||||
import org.springblade.transport.pojo.entity.WaybillImportBatch;
|
import org.springblade.transport.pojo.entity.WaybillImportBatch;
|
||||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||||
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
|
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
|
||||||
|
import org.springblade.transport.pojo.vo.WaybillImportDuplicateVO;
|
||||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/** 运单批次服务。 */
|
/** 运单批次服务。 */
|
||||||
public interface IWaybillImportBatchService extends BaseService<WaybillImportBatch> {
|
public interface IWaybillImportBatchService extends BaseService<WaybillImportBatch> {
|
||||||
WaybillImportBatch saveDraft(WaybillImportBatchRequest request);
|
WaybillImportBatch saveDraft(WaybillImportBatchRequest request);
|
||||||
@@ -18,4 +21,7 @@ public interface IWaybillImportBatchService extends BaseService<WaybillImportBat
|
|||||||
IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request);
|
IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request);
|
||||||
BusinessRemoveResultVO removeBatches(String ids);
|
BusinessRemoveResultVO removeBatches(String ids);
|
||||||
String nextBatchNo();
|
String nextBatchNo();
|
||||||
|
|
||||||
|
/** 逐行比对导入行与项目下已有运单,返回命中的运单号。 */
|
||||||
|
List<WaybillImportDuplicateVO> checkDuplicates(WaybillImportBatchRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -907,14 +907,19 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
|||||||
throw new ServiceException("合同负责部门不存在");
|
throw new ServiceException("合同负责部门不存在");
|
||||||
}
|
}
|
||||||
Dept platformCompany = SysCache.getDept(platformCompanyId);
|
Dept platformCompany = SysCache.getDept(platformCompanyId);
|
||||||
if (platformCompany == null) {
|
// 合同编号前缀改由「平台公司 + 业务部门」业务部门配置维护;系统侧会按同名现行平台公司回退匹配
|
||||||
throw new ServiceException("平台公司不存在");
|
|
||||||
}
|
|
||||||
// 合同编号前缀改由「平台公司 + 业务部门」业务部门配置维护,不再读取组织机构 Dept.contractCodePrefix
|
|
||||||
BusinessDeptConfig config = SysCache.getBusinessDeptConfig(platformCompanyId, organizationId);
|
BusinessDeptConfig config = SysCache.getBusinessDeptConfig(platformCompanyId, organizationId);
|
||||||
String prefix = config == null ? null : TransportBusinessSupport.trimToNull(config.getContractCodePrefix());
|
String prefix = config == null ? null : TransportBusinessSupport.trimToNull(config.getContractCodePrefix());
|
||||||
if (Func.isEmpty(prefix)) {
|
if (Func.isEmpty(prefix)) {
|
||||||
throw new ServiceException("业务部门配置中未维护该平台公司与业务部门的合同编码前缀,无法自动生成,请先在业务部门配置中维护或手动输入合同编号");
|
if (platformCompany == null) {
|
||||||
|
throw new ServiceException("平台公司不存在或已删除,且未匹配到同名平台公司的合同编码前缀配置,请重新选择平台公司或手动输入合同编号");
|
||||||
|
}
|
||||||
|
String platformName = TransportBusinessSupport.trimToNull(platformCompany.getDeptName());
|
||||||
|
String deptName = TransportBusinessSupport.trimToNull(dept.getDeptName());
|
||||||
|
throw new ServiceException(String.format(
|
||||||
|
"业务部门配置中未维护平台公司「%s」与业务部门「%s」的合同编码前缀,无法自动生成;请确认项目平台公司与配置中的平台公司为同一组织后维护,或手动输入合同编号",
|
||||||
|
Func.toStr(platformName, String.valueOf(platformCompanyId)),
|
||||||
|
Func.toStr(deptName, String.valueOf(organizationId))));
|
||||||
}
|
}
|
||||||
int year = LocalDate.now().getYear();
|
int year = LocalDate.now().getYear();
|
||||||
String head = prefix + "(" + year + ")";
|
String head = prefix + "(" + year + ")";
|
||||||
|
|||||||
+32
-13
@@ -1193,6 +1193,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
|||||||
|
|
||||||
/** 将 Excel 填写值回填到评分明细:选项型匹配选项,分值型写入 scoreInput/selfScore */
|
/** 将 Excel 填写值回填到评分明细:选项型匹配选项,分值型写入 scoreInput/selfScore */
|
||||||
private void applyImportedScoreDetail(CustomerCreditScoreDetailVO detail, String fillValue, String selfScoreText) {
|
private void applyImportedScoreDetail(CustomerCreditScoreDetailVO detail, String fillValue, String selfScoreText) {
|
||||||
|
BigDecimal importedSelfScore = parseImportedSelfScore(selfScoreText);
|
||||||
if ("score".equalsIgnoreCase(detail.getOptionType())) {
|
if ("score".equalsIgnoreCase(detail.getOptionType())) {
|
||||||
BigDecimal scoreInput;
|
BigDecimal scoreInput;
|
||||||
try {
|
try {
|
||||||
@@ -1201,12 +1202,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
|||||||
throw new ServiceException("填写值必须为数字");
|
throw new ServiceException("填写值必须为数字");
|
||||||
}
|
}
|
||||||
detail.setScoreInput(scoreInput);
|
detail.setScoreInput(scoreInput);
|
||||||
if (Func.isNotEmpty(selfScoreText)) {
|
if (importedSelfScore != null) {
|
||||||
try {
|
detail.setSelfScore(importedSelfScore);
|
||||||
detail.setSelfScore(new BigDecimal(selfScoreText));
|
|
||||||
} catch (NumberFormatException exception) {
|
|
||||||
throw new ServiceException("自评得分必须为数字");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1223,15 +1220,21 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
|||||||
}
|
}
|
||||||
detail.setSelectedOption(optionValue == null ? fillValue : String.valueOf(optionValue));
|
detail.setSelectedOption(optionValue == null ? fillValue : String.valueOf(optionValue));
|
||||||
BigDecimal optionScore = resolveOptionScore(matched);
|
BigDecimal optionScore = resolveOptionScore(matched);
|
||||||
detail.setSelfScore(optionScore == null ? BigDecimal.ZERO : optionScore);
|
detail.setSelfScore(importedSelfScore != null ? importedSelfScore : (optionScore == null ? BigDecimal.ZERO : optionScore));
|
||||||
if (Func.isNotEmpty(selfScoreText)) {
|
}
|
||||||
|
|
||||||
|
/** 解析线下 Excel 自评得分;空值或哨兵值 -1 视为未填写 */
|
||||||
|
private BigDecimal parseImportedSelfScore(String selfScoreText) {
|
||||||
|
if (Func.isEmpty(selfScoreText) || "-1".equals(selfScoreText.trim())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
detail.setSelfScore(new BigDecimal(selfScoreText));
|
BigDecimal selfScore = new BigDecimal(selfScoreText.trim());
|
||||||
|
return selfScore.compareTo(BigDecimal.ONE.negate()) == 0 ? null : selfScore;
|
||||||
} catch (NumberFormatException exception) {
|
} catch (NumberFormatException exception) {
|
||||||
throw new ServiceException("自评得分必须为数字");
|
throw new ServiceException("自评得分必须为数字");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> matchScoreOption(String optionsJson, String fillValue) {
|
private Map<String, Object> matchScoreOption(String optionsJson, String fillValue) {
|
||||||
if (Func.isEmpty(optionsJson) || Func.isEmpty(fillValue)) {
|
if (Func.isEmpty(optionsJson) || Func.isEmpty(fillValue)) {
|
||||||
@@ -1720,6 +1723,9 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
|||||||
scoreVO.setMaxCreditLimit(normalizeSentinelAmount(scoreVO.getMaxCreditLimit()));
|
scoreVO.setMaxCreditLimit(normalizeSentinelAmount(scoreVO.getMaxCreditLimit()));
|
||||||
scoreVO.setApplyCreditLimit(normalizeSentinelAmount(scoreVO.getApplyCreditLimit()));
|
scoreVO.setApplyCreditLimit(normalizeSentinelAmount(scoreVO.getApplyCreditLimit()));
|
||||||
scoreVO.setTempApplyCreditLimit(normalizeOptionalScoreAmount(scoreVO.getTempApplyCreditLimit()));
|
scoreVO.setTempApplyCreditLimit(normalizeOptionalScoreAmount(scoreVO.getTempApplyCreditLimit()));
|
||||||
|
scoreVO.setSelfScore(normalizeSentinelAmount(scoreVO.getSelfScore()));
|
||||||
|
scoreVO.setReviewScore(normalizeSentinelAmount(scoreVO.getReviewScore()));
|
||||||
|
scoreVO.setFinalScore(normalizeSentinelAmount(scoreVO.getFinalScore()));
|
||||||
scoreVO.setProofAttachments(normalizeScoreProofAttachments(scoreVO));
|
scoreVO.setProofAttachments(normalizeScoreProofAttachments(scoreVO));
|
||||||
CustomerCreditScore score = Objects.requireNonNull(BeanUtil.copyProperties(scoreVO, CustomerCreditScore.class));
|
CustomerCreditScore score = Objects.requireNonNull(BeanUtil.copyProperties(scoreVO, CustomerCreditScore.class));
|
||||||
score.setId(scoreId);
|
score.setId(scoreId);
|
||||||
@@ -1740,6 +1746,9 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
|||||||
if (detailVO.getScore() == null) {
|
if (detailVO.getScore() == null) {
|
||||||
detailVO.setScore(resolveFullScore(detailVO.getOptionsJson()));
|
detailVO.setScore(resolveFullScore(detailVO.getOptionsJson()));
|
||||||
}
|
}
|
||||||
|
detailVO.setSelfScore(normalizeSentinelAmount(detailVO.getSelfScore()));
|
||||||
|
detailVO.setReviewScore(normalizeSentinelAmount(detailVO.getReviewScore()));
|
||||||
|
detailVO.setScoreInput(normalizeSentinelAmount(detailVO.getScoreInput()));
|
||||||
CustomerCreditScoreDetail detail = Objects.requireNonNull(BeanUtil.copyProperties(detailVO, CustomerCreditScoreDetail.class));
|
CustomerCreditScoreDetail detail = Objects.requireNonNull(BeanUtil.copyProperties(detailVO, CustomerCreditScoreDetail.class));
|
||||||
detail.setId(IdWorker.getId());
|
detail.setId(IdWorker.getId());
|
||||||
detail.setTenantId(score.getTenantId());
|
detail.setTenantId(score.getTenantId());
|
||||||
@@ -1903,8 +1912,17 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
|||||||
.collect(Collectors.groupingBy(CustomerCreditScoreDetailVO::getScoreId));
|
.collect(Collectors.groupingBy(CustomerCreditScoreDetailVO::getScoreId));
|
||||||
return scores.stream().map(score -> {
|
return scores.stream().map(score -> {
|
||||||
CustomerCreditScoreVO scoreVO = Objects.requireNonNull(BeanUtil.copyProperties(score, CustomerCreditScoreVO.class));
|
CustomerCreditScoreVO scoreVO = Objects.requireNonNull(BeanUtil.copyProperties(score, CustomerCreditScoreVO.class));
|
||||||
|
scoreVO.setSelfScore(normalizeSentinelAmount(scoreVO.getSelfScore()));
|
||||||
|
scoreVO.setReviewScore(normalizeSentinelAmount(scoreVO.getReviewScore()));
|
||||||
|
scoreVO.setFinalScore(normalizeSentinelAmount(scoreVO.getFinalScore()));
|
||||||
scoreVO.setAttachments(parseScoreAttachments(score.getProofAttachments()));
|
scoreVO.setAttachments(parseScoreAttachments(score.getProofAttachments()));
|
||||||
scoreVO.setDetails(detailMap.getOrDefault(score.getId(), new ArrayList<>()));
|
List<CustomerCreditScoreDetailVO> details = detailMap.getOrDefault(score.getId(), new ArrayList<>());
|
||||||
|
details.forEach(detail -> {
|
||||||
|
detail.setSelfScore(normalizeSentinelAmount(detail.getSelfScore()));
|
||||||
|
detail.setReviewScore(normalizeSentinelAmount(detail.getReviewScore()));
|
||||||
|
detail.setScoreInput(normalizeSentinelAmount(detail.getScoreInput()));
|
||||||
|
});
|
||||||
|
scoreVO.setDetails(details);
|
||||||
scoreVO.setStandards(loadStandards(score.getQuantificationId()));
|
scoreVO.setStandards(loadStandards(score.getQuantificationId()));
|
||||||
return scoreVO;
|
return scoreVO;
|
||||||
}).toList();
|
}).toList();
|
||||||
@@ -2024,8 +2042,9 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
|||||||
: resolveFullScore(itemOptions);
|
: resolveFullScore(itemOptions);
|
||||||
detail.setScore(fullScore == null ? BigDecimal.ZERO : fullScore);
|
detail.setScore(fullScore == null ? BigDecimal.ZERO : fullScore);
|
||||||
detail.setOptionsJson(buildOptionsJson(itemOptions));
|
detail.setOptionsJson(buildOptionsJson(itemOptions));
|
||||||
detail.setSelfScore(BigDecimal.ZERO);
|
// 模板明细自评/复评默认空,避免前端线下评估回显 -1/0 哨兵值
|
||||||
detail.setReviewScore(BigDecimal.ZERO);
|
detail.setSelfScore(null);
|
||||||
|
detail.setReviewScore(null);
|
||||||
details.add(detail);
|
details.add(detail);
|
||||||
}
|
}
|
||||||
return details;
|
return details;
|
||||||
|
|||||||
+29
-7
@@ -72,6 +72,7 @@ import org.springblade.transport.service.IPreSettlementService;
|
|||||||
import org.springblade.transport.service.IProjectApplyService;
|
import org.springblade.transport.service.IProjectApplyService;
|
||||||
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
|
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
|
||||||
import org.springblade.transport.service.IWaybillService;
|
import org.springblade.transport.service.IWaybillService;
|
||||||
|
import org.springblade.transport.support.ContractPartySupport;
|
||||||
import org.springblade.transport.support.TransportBusinessSupport;
|
import org.springblade.transport.support.TransportBusinessSupport;
|
||||||
import org.springblade.transport.wrapper.PreSettlementWrapper;
|
import org.springblade.transport.wrapper.PreSettlementWrapper;
|
||||||
import org.springblade.system.cache.UserCache;
|
import org.springblade.system.cache.UserCache;
|
||||||
@@ -280,10 +281,14 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
|||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
Map<Long, ProjectApply> projectById = projectApplyService.listByIds(projectIds).stream()
|
Map<Long, ProjectApply> projectById = projectApplyService.listByIds(projectIds).stream()
|
||||||
.collect(Collectors.toMap(ProjectApply::getId, Function.identity()));
|
.collect(Collectors.toMap(ProjectApply::getId, Function.identity()));
|
||||||
|
// 多方客商汇总名(顿号拼接)需拆开后再匹配客商档案全称,避免变更通过后合同被误过滤
|
||||||
Set<String> partyNames = contracts.stream()
|
Set<String> partyNames = contracts.stream()
|
||||||
.flatMap(contract -> Stream.of(contract.getPartyA(), contract.getPartyB()))
|
.flatMap(contract -> Stream.concat(
|
||||||
|
ContractPartySupport.splitCustomerNames(contract.getPartyA()).stream(),
|
||||||
|
ContractPartySupport.splitCustomerNames(contract.getPartyB()).stream()))
|
||||||
.filter(Func::isNotEmpty).collect(Collectors.toSet());
|
.filter(Func::isNotEmpty).collect(Collectors.toSet());
|
||||||
Set<String> approvedCustomers = customerArchiveService.list(Wrappers.<CustomerArchive>lambdaQuery()
|
Set<String> approvedCustomers = partyNames.isEmpty() ? Set.of() : customerArchiveService.list(
|
||||||
|
Wrappers.<CustomerArchive>lambdaQuery()
|
||||||
.eq(CustomerArchive::getIsDeleted, 0)
|
.eq(CustomerArchive::getIsDeleted, 0)
|
||||||
.eq(CustomerArchive::getStatus, 1)
|
.eq(CustomerArchive::getStatus, 1)
|
||||||
.eq(CustomerArchive::getApprovalStatus, "approved")
|
.eq(CustomerArchive::getApprovalStatus, "approved")
|
||||||
@@ -294,8 +299,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
|||||||
ProjectApply project = projectById.get(contract.getProjectId());
|
ProjectApply project = projectById.get(contract.getProjectId());
|
||||||
return project != null && !Objects.equals(project.getIsDeleted(), 1)
|
return project != null && !Objects.equals(project.getIsDeleted(), 1)
|
||||||
&& List.of("approved", "change_approved").contains(project.getApprovalStatus())
|
&& List.of("approved", "change_approved").contains(project.getApprovalStatus())
|
||||||
&& approvedCustomers.contains(contract.getPartyA())
|
&& isApprovedPartySummary(contract.getPartyA(), approvedCustomers)
|
||||||
&& approvedCustomers.contains(contract.getPartyB());
|
&& isApprovedPartySummary(contract.getPartyB(), approvedCustomers);
|
||||||
})
|
})
|
||||||
.map(contract -> {
|
.map(contract -> {
|
||||||
ProjectApply project = projectById.get(contract.getProjectId());
|
ProjectApply project = projectById.get(contract.getProjectId());
|
||||||
@@ -1424,12 +1429,29 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isApprovedCustomer(String customerName) {
|
private boolean isApprovedCustomer(String customerName) {
|
||||||
if (Func.isEmpty(customerName)) return false;
|
List<String> names = ContractPartySupport.splitCustomerNames(customerName);
|
||||||
return customerArchiveService.count(Wrappers.<CustomerArchive>lambdaQuery()
|
if (names.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String name : names) {
|
||||||
|
long count = customerArchiveService.count(Wrappers.<CustomerArchive>lambdaQuery()
|
||||||
.eq(CustomerArchive::getIsDeleted, 0)
|
.eq(CustomerArchive::getIsDeleted, 0)
|
||||||
.eq(CustomerArchive::getStatus, 1)
|
.eq(CustomerArchive::getStatus, 1)
|
||||||
.eq(CustomerArchive::getApprovalStatus, "approved")
|
.eq(CustomerArchive::getApprovalStatus, "approved")
|
||||||
.eq(CustomerArchive::getFullName, customerName)) > 0;
|
.eq(CustomerArchive::getFullName, name));
|
||||||
|
if (count <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isApprovedPartySummary(String partySummary, Set<String> approvedCustomers) {
|
||||||
|
List<String> names = ContractPartySupport.splitCustomerNames(partySummary);
|
||||||
|
if (names.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return names.stream().allMatch(approvedCustomers::contains);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateContractRelations(ContractManage contract) {
|
private void validateContractRelations(ContractManage contract) {
|
||||||
|
|||||||
+27
-4
@@ -1329,6 +1329,7 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 应付客商取运单选择的承运商,避免写成项目内其他承运商合同的乙方。
|
* 应付客商取运单选择的承运商,避免写成项目内其他承运商合同的乙方。
|
||||||
|
* 应收客商优先取运单客户;多方汇总名称时改用命中计费方案绑定的甲方,避免写入顿号拼接串。
|
||||||
*/
|
*/
|
||||||
private String resolveCounterpartyName(Waybill waybill, ContractManage contract, String settlementType) {
|
private String resolveCounterpartyName(Waybill waybill, ContractManage contract, String settlementType) {
|
||||||
if ("payable".equals(settlementType)) {
|
if ("payable".equals(settlementType)) {
|
||||||
@@ -1338,10 +1339,31 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
}
|
}
|
||||||
return contract == null ? null : contract.getPartyB();
|
return contract == null ? null : contract.getPartyB();
|
||||||
}
|
}
|
||||||
if (contract != null && Func.isNotEmpty(contract.getPartyA())) {
|
String waybillCustomer = trimToNull(waybill == null ? null : waybill.getCustomerName());
|
||||||
return contract.getPartyA();
|
if (waybillCustomer != null && !waybillCustomer.contains("、")) {
|
||||||
|
return waybillCustomer;
|
||||||
}
|
}
|
||||||
return waybill == null ? null : waybill.getCustomerName();
|
if (contract != null && waybill != null) {
|
||||||
|
List<Map<String, Object>> plans = billingPlansForWaybill(contract, waybill);
|
||||||
|
Map<String, Object> plan = resolveDefaultBillingPlan(plans, waybill.getTransportType());
|
||||||
|
if (plan != null) {
|
||||||
|
String planCustomer = trimToNull(stringValue(plan, "customerName"));
|
||||||
|
if (planCustomer != null) {
|
||||||
|
return planCustomer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String firstParty = ContractPartySupport.firstPartyName(contract.getPartyAJson(), null);
|
||||||
|
if (firstParty != null) {
|
||||||
|
return firstParty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contract != null) {
|
||||||
|
String partyA = trimToNull(contract.getPartyA());
|
||||||
|
if (partyA != null && !partyA.contains("、")) {
|
||||||
|
return partyA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return waybillCustomer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1486,7 +1508,8 @@ public class ReceivablePayableDetailServiceImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String matchedPlanId(Waybill waybill, ContractManage contract) {
|
private String matchedPlanId(Waybill waybill, ContractManage contract) {
|
||||||
List<Map<String, Object>> plans = parseList(contract.getBillingPlanJson());
|
// 与费用计算共用客商过滤,避免多方汇总客户名先判定命中、再按客商滤空后静默跳过
|
||||||
|
List<Map<String, Object>> plans = billingPlansForWaybill(contract, waybill);
|
||||||
if (plans.isEmpty()) return null;
|
if (plans.isEmpty()) return null;
|
||||||
Map<String, Object> plan = resolveDefaultBillingPlan(plans, waybill.getTransportType());
|
Map<String, Object> plan = resolveDefaultBillingPlan(plans, waybill.getTransportType());
|
||||||
if (plan == null) return null;
|
if (plan == null) return null;
|
||||||
|
|||||||
+2
-2
@@ -1331,8 +1331,8 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<Map<String, Object>> selectableWaybillBatches(IPage<?> page, String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd) {
|
public IPage<Map<String, Object>> selectableWaybillBatches(IPage<?> page, String batchNo, Long projectId, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd) {
|
||||||
return voucherWaybillBatchMapper.selectVoucherWaybillBatchPage(page, AuthUtil.getTenantId(), batchNo, createUser, waybillCount, createTimeStart, createTimeEnd,
|
return voucherWaybillBatchMapper.selectVoucherWaybillBatchPage(page, AuthUtil.getTenantId(), batchNo, projectId, createUser, waybillCount, createTimeStart, createTimeEnd,
|
||||||
!TransportBusinessSupport.isAdministrator(), currentCarrierName());
|
!TransportBusinessSupport.isAdministrator(), currentCarrierName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+91
@@ -12,6 +12,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springblade.common.excel.ImportFailureExcelUtil;
|
import org.springblade.common.excel.ImportFailureExcelUtil;
|
||||||
|
import org.springblade.common.excel.LenientDateParser;
|
||||||
import org.springblade.core.log.exception.ServiceException;
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||||
import org.springblade.core.secure.utils.AuthUtil;
|
import org.springblade.core.secure.utils.AuthUtil;
|
||||||
@@ -39,6 +40,7 @@ import org.springblade.transport.pojo.entity.Waybill;
|
|||||||
import org.springblade.transport.pojo.entity.WaybillImportBatch;
|
import org.springblade.transport.pojo.entity.WaybillImportBatch;
|
||||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||||
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
|
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
|
||||||
|
import org.springblade.transport.pojo.vo.WaybillImportDuplicateVO;
|
||||||
import org.springblade.transport.pojo.vo.WaybillImportRowErrorVO;
|
import org.springblade.transport.pojo.vo.WaybillImportRowErrorVO;
|
||||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||||
import org.springblade.transport.service.ICustomerArchiveService;
|
import org.springblade.transport.service.ICustomerArchiveService;
|
||||||
@@ -50,6 +52,7 @@ import org.springblade.transport.service.ITransportPlanService;
|
|||||||
import org.springblade.transport.service.IWaybillImportBatchService;
|
import org.springblade.transport.service.IWaybillImportBatchService;
|
||||||
import org.springblade.transport.service.IWaybillService;
|
import org.springblade.transport.service.IWaybillService;
|
||||||
import org.springblade.transport.support.TransportBusinessSupport;
|
import org.springblade.transport.support.TransportBusinessSupport;
|
||||||
|
import org.springblade.transport.support.VehiclePlateSupport;
|
||||||
import org.springblade.transport.support.WaybillProcessSupport;
|
import org.springblade.transport.support.WaybillProcessSupport;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -173,6 +176,94 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
return toVO(batch);
|
return toVO(batch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<WaybillImportDuplicateVO> checkDuplicates(WaybillImportBatchRequest request) {
|
||||||
|
List<WaybillImportDuplicateVO> duplicates = new ArrayList<>();
|
||||||
|
if (request == null || Func.isEmpty(request.getRows()) || Func.isEmpty(request.getProjectId())) {
|
||||||
|
return duplicates;
|
||||||
|
}
|
||||||
|
List<Long> carrierIds = resolveCarrierIds(request);
|
||||||
|
String carrierName = resolveCarrierNames(carrierIds, request.getCarrierName());
|
||||||
|
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery()
|
||||||
|
.eq(Waybill::getIsDeleted, 0)
|
||||||
|
.eq(Waybill::getProjectId, request.getProjectId());
|
||||||
|
if (Func.isNotEmpty(request.getId())) {
|
||||||
|
// 追加导入时,本批次已落库的运单不算历史运单。
|
||||||
|
wrapper.ne(Waybill::getImportBatchId, request.getId());
|
||||||
|
}
|
||||||
|
List<Waybill> candidates = waybillService.list(wrapper);
|
||||||
|
if (Func.isEmpty(candidates)) {
|
||||||
|
return duplicates;
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> rows = request.getRows();
|
||||||
|
for (int index = 0; index < rows.size(); index++) {
|
||||||
|
List<String> waybillNos = new ArrayList<>();
|
||||||
|
for (Waybill waybill : candidates) {
|
||||||
|
if (!isDuplicateRow(rows.get(index), waybill, carrierIds, carrierName)) continue;
|
||||||
|
String waybillNo = waybill.getWaybillNo();
|
||||||
|
if (Func.isNotEmpty(waybillNo) && !waybillNos.contains(waybillNo)) {
|
||||||
|
waybillNos.add(waybillNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (waybillNos.isEmpty()) continue;
|
||||||
|
WaybillImportDuplicateVO duplicate = new WaybillImportDuplicateVO();
|
||||||
|
duplicate.setRowIndex(index);
|
||||||
|
duplicate.setWaybillNos(waybillNos);
|
||||||
|
duplicates.add(duplicate);
|
||||||
|
}
|
||||||
|
return duplicates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判重口径为导入必填项逐一相同:承运商、车牌、运输方式、起止地址、货物名称与类型、
|
||||||
|
* 数量及单位、实际发货与实际完成时间。历史运单这些字段常为空,双方都空按相同处理。
|
||||||
|
*/
|
||||||
|
private boolean isDuplicateRow(Map<String, Object> row, Waybill waybill, List<Long> carrierIds, String carrierName) {
|
||||||
|
if (row == null || waybill == null) return false;
|
||||||
|
if (!carrierMatches(waybill, carrierIds, carrierName)) return false;
|
||||||
|
if (!samePlate(vehicleNoFromRow(row), waybill.getVehicleNo())) return false;
|
||||||
|
String transportType = resolveTransportTypeKey(
|
||||||
|
stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"));
|
||||||
|
if (!sameText(transportType, waybill.getTransportType())) return false;
|
||||||
|
if (!sameText(stringValue(row, "departureAddress"), waybill.getDepartureAddress())) return false;
|
||||||
|
if (!sameText(stringValue(row, "arrivalAddress"), waybill.getArrivalAddress())) return false;
|
||||||
|
if (!sameText(stringValue(row, "cargoName"), waybill.getCargoName())) return false;
|
||||||
|
if (!sameText(stringValue(row, "cargoType"), waybill.getCargoType())) return false;
|
||||||
|
BigDecimal quantity = defaultQuantity(decimalValue(row, "quantity", "数量", "重量"));
|
||||||
|
if (quantity.compareTo(defaultQuantity(waybill.getQuantity())) != 0) return false;
|
||||||
|
String quantityUnit = stringValue(row, "quantityUnit", "数量单位");
|
||||||
|
if (!sameText(Func.isEmpty(quantityUnit) ? "吨" : quantityUnit, waybill.getQuantityUnit())) return false;
|
||||||
|
if (!Objects.equals(duplicateDate(row.get("actualStartDate")), waybill.getStartDate())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Objects.equals(duplicateDate(row.get("actualEndDate")), waybill.getEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 承运商优先按档案 ID 比对;历史手工运单只有名称,退化为名称比对。 */
|
||||||
|
private boolean carrierMatches(Waybill waybill, List<Long> carrierIds, String carrierName) {
|
||||||
|
if (Func.isNotEmpty(carrierIds) && waybill.getCarrierId() != null) {
|
||||||
|
return carrierIds.contains(waybill.getCarrierId());
|
||||||
|
}
|
||||||
|
return sameText(waybill.getCarrierName(), carrierName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameText(String left, String right) {
|
||||||
|
String a = left == null ? "" : left.trim();
|
||||||
|
String b = right == null ? "" : right.trim();
|
||||||
|
return a.equalsIgnoreCase(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean samePlate(String left, String right) {
|
||||||
|
return VehiclePlateSupport.normalize(left).equals(VehiclePlateSupport.normalize(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判重时的日期解析不阻断查询:无法识别的日期返回 null,按「与空值相同」处理。 */
|
||||||
|
private LocalDate duplicateDate(Object value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String text = String.valueOf(value).trim();
|
||||||
|
return text.isEmpty() ? null : LenientDateParser.parseDateLenientlyOrNull(text);
|
||||||
|
}
|
||||||
|
|
||||||
/** 草稿与确认导入共用落库流程,差异仅在于运单是否走校验以及是否生成应收应付明细。 */
|
/** 草稿与确认导入共用落库流程,差异仅在于运单是否走校验以及是否生成应收应付明细。 */
|
||||||
private WaybillImportBatch persist(WaybillImportBatchRequest request, String importStatus) {
|
private WaybillImportBatch persist(WaybillImportBatchRequest request, String importStatus) {
|
||||||
boolean draft = STATUS_DRAFT.equals(importStatus);
|
boolean draft = STATUS_DRAFT.equals(importStatus);
|
||||||
|
|||||||
+9
-1
@@ -2174,7 +2174,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
|||||||
if (isRoadTransport(waybill.getTransportType())) {
|
if (isRoadTransport(waybill.getTransportType())) {
|
||||||
if ("承运商".equals(waybill.getCarrierType())) {
|
if ("承运商".equals(waybill.getCarrierType())) {
|
||||||
TransportBusinessSupport.validateRequired(waybill.getCarrierName(), "承运商不能为空");
|
TransportBusinessSupport.validateRequired(waybill.getCarrierName(), "承运商不能为空");
|
||||||
} else if (!isBatchImport(waybill)) {
|
} else if (!isBatchImport(waybill) && !isDriverOptionalSource(waybill)) {
|
||||||
TransportBusinessSupport.validateRequired(waybill.getDriverName(), "司机不能为空");
|
TransportBusinessSupport.validateRequired(waybill.getDriverName(), "司机不能为空");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2350,6 +2350,14 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
|||||||
return waybill != null && TransportBusinessSupport.DATA_SOURCE_BATCH_IMPORT.equals(waybill.getDataSource());
|
return waybill != null && TransportBusinessSupport.DATA_SOURCE_BATCH_IMPORT.equals(waybill.getDataSource());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计划调度生成的运单允许司机为空:调度明细里司机与手机号对全部承运类型都是选填。
|
||||||
|
* 数据来源已由 prepare 归一,多联总单调度同样归为计划调度。
|
||||||
|
*/
|
||||||
|
private boolean isDriverOptionalSource(Waybill waybill) {
|
||||||
|
return waybill != null && TransportBusinessSupport.DATA_SOURCE_PLAN_DISPATCH.equals(waybill.getDataSource());
|
||||||
|
}
|
||||||
|
|
||||||
private void fillJsonFromFlatFields(Waybill waybill) {
|
private void fillJsonFromFlatFields(Waybill waybill) {
|
||||||
if (Func.isEmpty(waybill.getTaskEntryMode())) {
|
if (Func.isEmpty(waybill.getTaskEntryMode())) {
|
||||||
waybill.setTaskEntryMode("simple");
|
waybill.setTaskEntryMode("simple");
|
||||||
|
|||||||
+64
-5
@@ -279,10 +279,39 @@ public final class ContractPartySupport {
|
|||||||
if (plans.isEmpty()) {
|
if (plans.isEmpty()) {
|
||||||
return plans;
|
return plans;
|
||||||
}
|
}
|
||||||
|
List<Map<String, Object>> matched = matchBillingPlans(plans, customerId, customerName, partyRowId);
|
||||||
|
if (!matched.isEmpty()) {
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
// 运单客户名为合同多方汇总(顿号拼接)时,按各客商名称分别匹配绑定方案
|
||||||
|
if (Func.isEmpty(partyRowId) && customerId == null && Func.isNotEmpty(customerName) && customerName.contains("、")) {
|
||||||
|
Map<String, Map<String, Object>> deduped = new LinkedHashMap<>();
|
||||||
|
for (String part : customerName.split("、")) {
|
||||||
|
String segment = trimToNull(part);
|
||||||
|
if (Func.isEmpty(segment)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (Map<String, Object> plan : matchBillingPlans(plans, null, segment, null)) {
|
||||||
|
deduped.putIfAbsent(billingPlanKey(plan), plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!deduped.isEmpty()) {
|
||||||
|
return new ArrayList<>(deduped.values());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 历史方案未绑定客商时,不强制过滤,避免老数据无法计费
|
||||||
|
if (plans.stream().noneMatch(ContractPartySupport::hasCustomerBinding)) {
|
||||||
|
return plans;
|
||||||
|
}
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Map<String, Object>> matchBillingPlans(List<Map<String, Object>> plans, Long customerId,
|
||||||
|
String customerName, String partyRowId) {
|
||||||
String normalizedName = normalizePartyName(customerName);
|
String normalizedName = normalizePartyName(customerName);
|
||||||
String normalizedPartyRowId = trimToNull(partyRowId);
|
String normalizedPartyRowId = trimToNull(partyRowId);
|
||||||
String customerIdText = customerId == null ? null : String.valueOf(customerId);
|
String customerIdText = customerId == null ? null : String.valueOf(customerId);
|
||||||
List<Map<String, Object>> matched = plans.stream()
|
return plans.stream()
|
||||||
.filter(plan -> {
|
.filter(plan -> {
|
||||||
if (Func.isNotEmpty(normalizedPartyRowId)
|
if (Func.isNotEmpty(normalizedPartyRowId)
|
||||||
&& Objects.equals(trimToNull(plan.get("partyRowId")), normalizedPartyRowId)) {
|
&& Objects.equals(trimToNull(plan.get("partyRowId")), normalizedPartyRowId)) {
|
||||||
@@ -296,11 +325,23 @@ public final class ContractPartySupport {
|
|||||||
&& Objects.equals(normalizePartyName(trimToNull(plan.get("customerName"))), normalizedName);
|
&& Objects.equals(normalizePartyName(trimToNull(plan.get("customerName"))), normalizedName);
|
||||||
})
|
})
|
||||||
.collect(Collectors.toCollection(ArrayList::new));
|
.collect(Collectors.toCollection(ArrayList::new));
|
||||||
// 历史方案未绑定客商时,不强制过滤,避免老数据无法计费
|
|
||||||
if (matched.isEmpty() && plans.stream().noneMatch(ContractPartySupport::hasCustomerBinding)) {
|
|
||||||
return plans;
|
|
||||||
}
|
}
|
||||||
return matched;
|
|
||||||
|
private static String billingPlanKey(Map<String, Object> plan) {
|
||||||
|
if (plan == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String id = trimToNull(plan.get("id"));
|
||||||
|
if (Func.isNotEmpty(id)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
String planId = trimToNull(plan.get("planId"));
|
||||||
|
if (Func.isNotEmpty(planId)) {
|
||||||
|
return planId;
|
||||||
|
}
|
||||||
|
return String.valueOf(trimToNull(plan.get("customerName"))) + "|"
|
||||||
|
+ String.valueOf(trimToNull(plan.get("name"))) + "|"
|
||||||
|
+ String.valueOf(trimToNull(plan.get("planName")));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<LoadingCarrierContractVO> expandCarrierOptions(Collection<ContractManage> contracts) {
|
public static List<LoadingCarrierContractVO> expandCarrierOptions(Collection<ContractManage> contracts) {
|
||||||
@@ -507,6 +548,24 @@ public final class ContractPartySupport {
|
|||||||
return names.isEmpty() ? null : String.join("、", names);
|
return names.isEmpty() ? null : String.join("、", names);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拆分合同甲/乙/丙方汇总名称(顿号拼接的多方客商)。
|
||||||
|
*/
|
||||||
|
public static List<String> splitCustomerNames(String joinedNames) {
|
||||||
|
String text = trimToNull(joinedNames);
|
||||||
|
if (Func.isEmpty(text)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
for (String part : text.split("、")) {
|
||||||
|
String name = trimToNull(part);
|
||||||
|
if (Func.isNotEmpty(name) && !names.contains(name)) {
|
||||||
|
names.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
private static String toJsonOrNull(List<Map<String, Object>> rows) {
|
private static String toJsonOrNull(List<Map<String, Object>> rows) {
|
||||||
return rows == null || rows.isEmpty() ? null : JsonUtil.toJson(rows);
|
return rows == null || rows.isEmpty() ? null : JsonUtil.toJson(rows);
|
||||||
}
|
}
|
||||||
|
|||||||
+163
@@ -0,0 +1,163 @@
|
|||||||
|
package org.springblade.transport.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springblade.system.feign.ISysClient;
|
||||||
|
import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
|
||||||
|
import org.springblade.transport.pojo.entity.Waybill;
|
||||||
|
import org.springblade.transport.pojo.vo.WaybillImportDuplicateVO;
|
||||||
|
import org.springblade.transport.service.ICustomerArchiveService;
|
||||||
|
import org.springblade.transport.service.IDriverService;
|
||||||
|
import org.springblade.transport.service.ILoadingManageService;
|
||||||
|
import org.springblade.transport.service.IProjectApplyService;
|
||||||
|
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||||
|
import org.springblade.transport.service.ITransportPlanService;
|
||||||
|
import org.springblade.transport.service.IWaybillService;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量导入行与历史运单的判重测试。
|
||||||
|
* <p>
|
||||||
|
* 口径为导入必填项逐一相同;历史手工运单的承运商档案 ID 与时间列常为空,
|
||||||
|
* 这类字段双方都空时按相同处理,否则手工运单永远判不出重复。
|
||||||
|
*/
|
||||||
|
class WaybillImportDuplicateTest {
|
||||||
|
|
||||||
|
private static final Long PROJECT_ID = 1001L;
|
||||||
|
private static final Long CARRIER_ID = 2002L;
|
||||||
|
private static final String CARRIER_NAME = "广西驰远干线运输有限公司";
|
||||||
|
private static final String VEHICLE_NO = "桂A5D61N";
|
||||||
|
private static final String CARGO_NAME = "混煤";
|
||||||
|
private static final String DEPARTURE = "鄂尔多斯市达拉特旗树林召镇";
|
||||||
|
private static final String ARRIVAL = "南宁市江南区沙井大道";
|
||||||
|
|
||||||
|
private IWaybillService waybillService;
|
||||||
|
private WaybillImportBatchServiceImpl service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
waybillService = mock(IWaybillService.class);
|
||||||
|
service = new WaybillImportBatchServiceImpl(
|
||||||
|
waybillService,
|
||||||
|
mock(ICustomerArchiveService.class),
|
||||||
|
mock(IProjectApplyService.class),
|
||||||
|
mock(ITransportPlanService.class),
|
||||||
|
mock(IReceivablePayableDetailService.class),
|
||||||
|
mock(ILoadingManageService.class),
|
||||||
|
mock(ISysClient.class),
|
||||||
|
mock(IDriverService.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("必填项全一致:返回命中的已有运单号")
|
||||||
|
void reportsDuplicateWaybillNo() {
|
||||||
|
stubCandidates(waybill("YD-20261023-0012"));
|
||||||
|
|
||||||
|
WaybillImportBatchRequest req = request(importRow());
|
||||||
|
List<WaybillImportDuplicateVO> duplicates = service.checkDuplicates(req);
|
||||||
|
|
||||||
|
assertEquals(1, duplicates.size(), "rows=" + req.getRows() + " projectId=" + req.getProjectId());
|
||||||
|
assertEquals(0, duplicates.get(0).getRowIndex());
|
||||||
|
assertEquals(List.of("YD-20261023-0012"), duplicates.get(0).getWaybillNos());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("数量不同:不判为重复")
|
||||||
|
void ignoresDifferentQuantity() {
|
||||||
|
stubCandidates(waybill("YD-20261023-0012"));
|
||||||
|
Map<String, Object> row = importRow();
|
||||||
|
row.put("quantity", "21");
|
||||||
|
|
||||||
|
assertTrue(service.checkDuplicates(request(row)).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("车牌大小写与空格不同:仍判为重复")
|
||||||
|
void ignoresPlateFormatting() {
|
||||||
|
stubCandidates(waybill("YD-20261023-0012"));
|
||||||
|
Map<String, Object> row = importRow();
|
||||||
|
row.put("vehicleNo", " 桂a5d61n ");
|
||||||
|
|
||||||
|
assertEquals(1, service.checkDuplicates(request(row)).size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("历史运单没有承运商档案 ID:退化为承运商名称比对")
|
||||||
|
void matchesByCarrierNameWhenHistoryHasNoId() {
|
||||||
|
Waybill waybill = waybill("YD-20261023-0012");
|
||||||
|
waybill.setCarrierId(null);
|
||||||
|
stubCandidates(waybill);
|
||||||
|
|
||||||
|
assertEquals(1, service.checkDuplicates(request(importRow())).size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("项目为空:直接返回空,不查库")
|
||||||
|
void returnsEmptyWithoutProject() {
|
||||||
|
WaybillImportBatchRequest request = request(importRow());
|
||||||
|
request.setProjectId(null);
|
||||||
|
|
||||||
|
assertTrue(service.checkDuplicates(request).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stubCandidates(Waybill... waybills) {
|
||||||
|
when(waybillService.list(any(Wrapper.class))).thenReturn(List.of(waybills));
|
||||||
|
}
|
||||||
|
|
||||||
|
private WaybillImportBatchRequest request(Map<String, Object> row) {
|
||||||
|
WaybillImportBatchRequest request = new WaybillImportBatchRequest();
|
||||||
|
request.setProjectId(PROJECT_ID);
|
||||||
|
request.setCarrierId(CARRIER_ID);
|
||||||
|
request.setCarrierName(CARRIER_NAME);
|
||||||
|
request.setRows(List.of(row));
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> importRow() {
|
||||||
|
Map<String, Object> row = new HashMap<>();
|
||||||
|
row.put("vehicleNo", VEHICLE_NO);
|
||||||
|
// 无 Spring 上下文时字典读不到,运输方式按“名称即键”兜底,故此处直接给内部键。
|
||||||
|
row.put("transportType", "road");
|
||||||
|
row.put("departureAddress", DEPARTURE);
|
||||||
|
row.put("arrivalAddress", ARRIVAL);
|
||||||
|
row.put("cargoName", CARGO_NAME);
|
||||||
|
row.put("cargoType", "煤制品");
|
||||||
|
row.put("quantity", "20");
|
||||||
|
row.put("quantityUnit", "吨");
|
||||||
|
row.put("actualStartDate", "2026-10-13");
|
||||||
|
row.put("actualEndDate", "2026-10-14");
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Waybill waybill(String waybillNo) {
|
||||||
|
Waybill waybill = new Waybill();
|
||||||
|
waybill.setWaybillNo(waybillNo);
|
||||||
|
waybill.setProjectId(PROJECT_ID);
|
||||||
|
waybill.setCarrierId(CARRIER_ID);
|
||||||
|
waybill.setCarrierName(CARRIER_NAME);
|
||||||
|
waybill.setVehicleNo(VEHICLE_NO);
|
||||||
|
waybill.setTransportType("road");
|
||||||
|
waybill.setDepartureAddress(DEPARTURE);
|
||||||
|
waybill.setArrivalAddress(ARRIVAL);
|
||||||
|
waybill.setCargoName(CARGO_NAME);
|
||||||
|
waybill.setCargoType("煤制品");
|
||||||
|
waybill.setQuantity(new BigDecimal("20.00"));
|
||||||
|
waybill.setQuantityUnit("吨");
|
||||||
|
waybill.setStartDate(LocalDate.of(2026, 10, 13));
|
||||||
|
waybill.setEndDate(LocalDate.of(2026, 10, 14));
|
||||||
|
return waybill;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,6 +98,8 @@ thirdParty:
|
|||||||
idCardBaseUrl: http://124.71.22.123:8500
|
idCardBaseUrl: http://124.71.22.123:8500
|
||||||
# 营业执照 OCR 服务地址(POST /businessLicense,JSON:img_path、data_type)
|
# 营业执照 OCR 服务地址(POST /businessLicense,JSON:img_path、data_type)
|
||||||
businessLicenseBaseUrl: http://124.71.22.123:8888
|
businessLicenseBaseUrl: http://124.71.22.123:8888
|
||||||
|
# 从业资格证 OCR 服务地址(POST /Biaozhunbaodan,JSON:img_path)
|
||||||
|
qualificationCertificateBaseUrl: http://124.71.22.123:8400
|
||||||
wps:
|
wps:
|
||||||
# WPS开放接口地址
|
# WPS开放接口地址
|
||||||
baseUrl: http://127.0.0.1:8080
|
baseUrl: http://127.0.0.1:8080
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ thirdParty:
|
|||||||
idCardBaseUrl: http://124.71.22.123:8500
|
idCardBaseUrl: http://124.71.22.123:8500
|
||||||
# 营业执照 OCR 服务地址(POST /businessLicense,JSON:img_path、data_type)
|
# 营业执照 OCR 服务地址(POST /businessLicense,JSON:img_path、data_type)
|
||||||
businessLicenseBaseUrl: http://124.71.22.123:8888
|
businessLicenseBaseUrl: http://124.71.22.123:8888
|
||||||
|
# 从业资格证 OCR 服务地址(POST /Biaozhunbaodan,JSON:img_path)
|
||||||
|
qualificationCertificateBaseUrl: http://124.71.22.123:8400
|
||||||
wps:
|
wps:
|
||||||
# WPS开放接口地址
|
# WPS开放接口地址
|
||||||
baseUrl: http://127.0.0.1:8080
|
baseUrl: http://127.0.0.1:8080
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ thirdParty:
|
|||||||
idCardBaseUrl: ${OCR_ID_CARD_BASE_URL:http://124.71.22.123:8500}
|
idCardBaseUrl: ${OCR_ID_CARD_BASE_URL:http://124.71.22.123:8500}
|
||||||
# 营业执照 OCR 服务地址(POST /businessLicense,JSON:img_path、data_type)
|
# 营业执照 OCR 服务地址(POST /businessLicense,JSON:img_path、data_type)
|
||||||
businessLicenseBaseUrl: ${OCR_BUSINESS_LICENSE_BASE_URL:http://124.71.22.123:8888}
|
businessLicenseBaseUrl: ${OCR_BUSINESS_LICENSE_BASE_URL:http://124.71.22.123:8888}
|
||||||
|
# 从业资格证 OCR 服务地址(POST /Biaozhunbaodan,JSON:img_path)
|
||||||
|
qualificationCertificateBaseUrl: ${OCR_QUALIFICATION_CERTIFICATE_BASE_URL:http://124.71.22.123:8400}
|
||||||
wps:
|
wps:
|
||||||
# WPS开放接口地址
|
# WPS开放接口地址
|
||||||
baseUrl: ${WPS_BASE_URL:http://127.0.0.1:8080}
|
baseUrl: ${WPS_BASE_URL:http://127.0.0.1:8080}
|
||||||
|
|||||||
Reference in New Issue
Block a user