Merge branch 'master' into feature/20261103-bfi-warehouse-material
# Conflicts: # blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java # doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md # doc/sql/transport/lqj/20261102/06_blade_invoice_receipt_supplier_code_menu_20261031.sql # doc/sql/transport/lqj/20261102/19_blade_dict_fee_category_warehouse_20261103.sql
This commit is contained in:
+23
@@ -110,6 +110,11 @@ public class BfiProperties {
|
||||
*/
|
||||
private ArResult arResult = new ArResult();
|
||||
|
||||
/**
|
||||
* 金蝶冲销状态回写配置(A102-4/A302-4)
|
||||
*/
|
||||
private WriteOffResult writeOffResult = new WriteOffResult();
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定补偿配置
|
||||
*/
|
||||
@@ -364,6 +369,24 @@ public class BfiProperties {
|
||||
private long lockLeaseSeconds = 1800;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶冲销状态回写配置项(A102-4/A302-4):定时回写开关、防重键批量查询与结算单批量更新的分批大小,
|
||||
* 防止单批过大超出金蝶查询接口与批量落库承载能力
|
||||
*/
|
||||
@Data
|
||||
public static class WriteOffResult {
|
||||
|
||||
/**
|
||||
* 冲销状态回写任务开关,默认关闭(安全兜底),由 Nacos 按环境显式开启
|
||||
*/
|
||||
private boolean syncEnabled = false;
|
||||
|
||||
/**
|
||||
* 冲销状态批量查询/批量更新分批大小,默认 1000
|
||||
*/
|
||||
private int queryBatchSize = 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶单据锁定补偿配置项
|
||||
*/
|
||||
|
||||
+6
@@ -171,6 +171,12 @@ public final class KingdeeBfiConstant {
|
||||
/** 金蝶同步状态:生成失败(推送异常/金蝶返回失败,可重推)。 */
|
||||
public static final String KINGDEE_STATUS_FAILED = "failed";
|
||||
|
||||
/** 金蝶冲销状态:未冲销(旧应付/应收单在金蝶尚未冲销)。 */
|
||||
public static final String KINGDEE_WRITEOFF_UNWRITTEN = "unwrittenoff";
|
||||
|
||||
/** 金蝶冲销状态:已冲销(旧应付/应收单已在金蝶冲销,部分冲销视同已冲销)。 */
|
||||
public static final String KINGDEE_WRITEOFF_WRITTEN = "writtenoff";
|
||||
|
||||
/** 收票单推送状态:未推送(新建单据默认,票池绑定层的 kingdee_status 与此无关)。 */
|
||||
public static final String RECEIVED_PUSH_UNSYNCED = "unsynced";
|
||||
|
||||
|
||||
+17
@@ -36,7 +36,9 @@ import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.ocr.constant.BaiduOcrType;
|
||||
import org.springblade.transport.ocr.service.IBaiduOcrService;
|
||||
import org.springblade.transport.ocr.service.IBusinessLicenseOcrService;
|
||||
import org.springblade.transport.pojo.vo.BaiduOcrResultVO;
|
||||
import org.springblade.transport.pojo.vo.BusinessLicenseOcrVO;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -60,6 +62,7 @@ import java.io.IOException;
|
||||
public class BaiduOcrController extends BladeController {
|
||||
|
||||
private final IBaiduOcrService baiduOcrService;
|
||||
private final IBusinessLicenseOcrService businessLicenseOcrService;
|
||||
|
||||
/**
|
||||
* 识别上传的图片。
|
||||
@@ -109,6 +112,20 @@ public class BaiduOcrController extends BladeController {
|
||||
return R.data(baiduOcrService.recognizeUrl(resolveType(type), side, imageUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* 营业执照图片地址识别(第三方 /businessLicense)。
|
||||
*
|
||||
* @param imageUrl 图片地址
|
||||
* @return 识别结果
|
||||
*/
|
||||
@PostMapping("/business-license")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "营业执照OCR识别", description = "按图片地址调用第三方营业执照识别接口")
|
||||
public R<BusinessLicenseOcrVO> recognizeBusinessLicense(
|
||||
@Parameter(description = "图片地址", required = true) @RequestParam String imageUrl) {
|
||||
return R.data(businessLicenseOcrService.recognizeUrl(imageUrl));
|
||||
}
|
||||
|
||||
private BaiduOcrType resolveType(String type) {
|
||||
try {
|
||||
return BaiduOcrType.from(type);
|
||||
|
||||
+38
-4
@@ -36,6 +36,7 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.dto.CertAuditRequest;
|
||||
import org.springblade.transport.pojo.vo.CertAuditCapabilityVO;
|
||||
import org.springblade.transport.pojo.vo.CertAuditDetailVO;
|
||||
import org.springblade.transport.pojo.vo.TransportCertRecordVO;
|
||||
@@ -43,10 +44,13 @@ import org.springblade.transport.service.ICertAuditService;
|
||||
import org.springblade.transport.support.CertAuditPermissions;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 认证审核(管理端)
|
||||
* <p>
|
||||
@@ -92,13 +96,26 @@ public class CertificationAuditController extends BladeController {
|
||||
|
||||
@PostMapping("/audit")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "审核认证记录", description = "通过使该记录成为生效快照;驳回只终结该记录,生效快照不变")
|
||||
@Operation(summary = "审核认证记录", description = "通过使该记录成为生效快照;驳回只终结该记录,生效快照不变。通过时可携带修订后的全量认证内容,它将成为新的生效内容并原样留痕")
|
||||
@PreAuth(CertAuditPermissions.AUDIT_ACTION)
|
||||
public R<Boolean> audit(
|
||||
@Parameter(description = "认证记录主键", required = true) @RequestParam Long id,
|
||||
@Parameter(description = "是否通过", required = true) @RequestParam Boolean pass,
|
||||
@Parameter(description = "审核请求体(推荐):id、pass、rejectReason、auditedSnapshotJson")
|
||||
@RequestBody(required = false) CertAuditRequest body,
|
||||
@Parameter(description = "认证记录主键(兼容老调用)") @RequestParam(required = false) Long id,
|
||||
@Parameter(description = "是否通过(兼容老调用)") @RequestParam(required = false) Boolean pass,
|
||||
@Parameter(description = "驳回原因,驳回时必填") @RequestParam(required = false) String rejectReason) {
|
||||
return R.status(certAuditService.audit(id, Boolean.TRUE.equals(pass), rejectReason));
|
||||
// 同一字段 body 与 query 都可传,两处都给时以 body 为准。
|
||||
Long recordId = preferBody(body, CertAuditRequest::getId, id);
|
||||
Boolean approved = preferBody(body, CertAuditRequest::getPass, pass);
|
||||
String reason = preferBody(body, CertAuditRequest::getRejectReason, rejectReason);
|
||||
String revision = preferBody(body, CertAuditRequest::getAuditedSnapshotJson, null);
|
||||
if (recordId == null) {
|
||||
return R.fail("缺少认证记录ID");
|
||||
}
|
||||
if (approved == null) {
|
||||
return R.fail("缺少审核结果");
|
||||
}
|
||||
return R.status(certAuditService.audit(recordId, Boolean.TRUE.equals(approved), reason, revision));
|
||||
}
|
||||
|
||||
// 本接口不挂 @PreAuth;无权限时返回 false。
|
||||
@@ -109,4 +126,21 @@ public class CertificationAuditController extends BladeController {
|
||||
return R.data(certAuditService.capability());
|
||||
}
|
||||
|
||||
/**
|
||||
* 取一个审核参数:body 里给了就用 body 的,否则用 query 的
|
||||
*
|
||||
* @param body 审核请求体,可为 null
|
||||
* @param getter 从请求体取该字段
|
||||
* @param queryValue 该字段的 query 参数值
|
||||
* @param <T> 字段类型
|
||||
* @return body 字段值;body 为 null 或该字段为 null 时返回 query 参数值
|
||||
*/
|
||||
private static <T> T preferBody(CertAuditRequest body, Function<CertAuditRequest, T> getter, T queryValue) {
|
||||
if (body == null) {
|
||||
return queryValue;
|
||||
}
|
||||
T bodyValue = getter.apply(body);
|
||||
return bodyValue == null ? queryValue : bodyValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-3
@@ -188,10 +188,11 @@ public class ContractManageController extends BladeController {
|
||||
|
||||
@GetMapping("/next-contract-no")
|
||||
@ApiOperationSupport(order = 16)
|
||||
@Operation(summary = "预览下一合同编号", description = "按合同负责部门编码前缀+年份+三位流水预览")
|
||||
@Operation(summary = "预览下一合同编号", description = "按平台公司+业务部门配置的前缀+年份+三位流水预览")
|
||||
public R<String> nextContractNo(
|
||||
@Parameter(description = "合同负责部门ID", required = true) @RequestParam Long organizationId) {
|
||||
return R.data(contractManageService.nextContractNo(organizationId));
|
||||
@Parameter(description = "合同负责部门ID", required = true) @RequestParam Long organizationId,
|
||||
@Parameter(description = "平台公司ID", required = true) @RequestParam Long platformCompanyId) {
|
||||
return R.data(contractManageService.nextContractNo(organizationId, platformCompanyId));
|
||||
}
|
||||
|
||||
@GetMapping("/export-contract-manage")
|
||||
|
||||
+3
-2
@@ -54,9 +54,10 @@ public class DriverAppController extends BladeController {
|
||||
|
||||
@GetMapping("/mine")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "当前登录司机档案", description = "按手机号匹配 blade_transport_driver.mobile;可传 mobile,未传则从登录态解析")
|
||||
@Operation(summary = "当前登录司机档案", description = "按登录用户的 user_id 匹配 blade_transport_driver.user_id")
|
||||
// mobile:旧版小程序仍会带这个参数,已不参与解析,保留只为不让老客户端报错
|
||||
public R<DriverVO> mine(@RequestParam(required = false) String mobile) {
|
||||
return R.data(driverAppService.currentByPhone(mobile));
|
||||
return R.data(driverAppService.current());
|
||||
}
|
||||
|
||||
@GetMapping("/vehicles")
|
||||
|
||||
+7
-7
@@ -65,14 +65,14 @@ public class DriverWaybillController extends BladeController {
|
||||
|
||||
@GetMapping("/current-task")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "首页:当前运输中任务", description = "当前用户手机号匹配运单 driverPhone 且 businessStatus=running 的最新一条运单")
|
||||
@Operation(summary = "首页:当前运输中任务", description = "当前司机档案(按登录用户解析)名下、businessStatus=running 的最新一条运单;归属判据是运单 driver_id")
|
||||
public R<DriverWaybillCardVO> currentTask() {
|
||||
return R.data(driverWaybillService.currentTask());
|
||||
}
|
||||
|
||||
@GetMapping("/pending-preview")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "首页:待接运单预览", description = "当前用户手机号匹配运单 driverPhone 且 businessStatus=pending 的预览列表与总数")
|
||||
@Operation(summary = "首页:待接运单预览", description = "当前司机档案名下、businessStatus=pending 的预览列表与总数;归属判据是运单 driver_id")
|
||||
public R<DriverWaybillPreviewVO> pendingPreview(
|
||||
@Parameter(description = "预览条数,默认 2") @RequestParam(required = false) Integer size) {
|
||||
return R.data(driverWaybillService.pendingPreview(size));
|
||||
@@ -80,14 +80,14 @@ public class DriverWaybillController extends BladeController {
|
||||
|
||||
@GetMapping("/counts")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "运单 Tab 统计", description = "仅统计当前用户手机号匹配运单 driverPhone 的运单:全部 / 待接单 / 进行中 / 已完成")
|
||||
@Operation(summary = "运单 Tab 统计", description = "仅统计当前司机档案名下(运单 driver_id)的运单:全部 / 待接单 / 进行中 / 已完成")
|
||||
public R<DriverWaybillTabCountsVO> counts() {
|
||||
return R.data(driverWaybillService.tabCounts());
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "运单分页列表", description = "仅返回运单 driverPhone 等于当前用户手机号的数据;status:空=全部,0待接单,1运输中,2已完成")
|
||||
@Operation(summary = "运单分页列表", description = "仅返回运单 driver_id 等于当前司机档案的数据;status:空=全部,0待接单,1运输中,2已完成")
|
||||
public R<IPage<DriverWaybillCardVO>> page(
|
||||
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
|
||||
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
|
||||
@@ -107,14 +107,14 @@ public class DriverWaybillController extends BladeController {
|
||||
|
||||
@GetMapping("/task/current")
|
||||
@ApiOperationSupport(order = 21)
|
||||
@Operation(summary = "首页:当前运输中运次", description = "当前用户手机号可见的运输中或待确认(待回单确认)运次(有配载单的一趟车算一个)最新一条")
|
||||
@Operation(summary = "首页:当前运输中运次", description = "当前司机档案可见的运输中或待确认(待回单确认)运次(有配载单的一趟车算一个)最新一条;配载段按配载单 driver_id,未配载段按运单 driver_id")
|
||||
public R<DriverWaybillCardVO> tripCurrentTask() {
|
||||
return R.data(driverWaybillService.tripCurrentTask());
|
||||
}
|
||||
|
||||
@GetMapping("/task/pending-preview")
|
||||
@ApiOperationSupport(order = 22)
|
||||
@Operation(summary = "首页:待接运次预览", description = "当前用户手机号可见的待接运次预览与总数(按运次计)")
|
||||
@Operation(summary = "首页:待接运次预览", description = "当前司机档案可见的待接运次预览与总数(按运次计)")
|
||||
public R<DriverWaybillPreviewVO> tripPendingPreview(
|
||||
@Parameter(description = "预览条数,默认 2") @RequestParam(required = false) Integer size) {
|
||||
return R.data(driverWaybillService.tripPendingPreview(size));
|
||||
@@ -166,7 +166,7 @@ public class DriverWaybillController extends BladeController {
|
||||
|
||||
@PostMapping("/track")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "司机端行驶轨迹", description = "按当前用户手机号校验归属后,根据打卡记录/计划时间自动推导起止日期并查询 LBS 轨迹(WGS-84)")
|
||||
@Operation(summary = "司机端行驶轨迹", description = "按当前司机档案校验归属后,根据打卡记录/计划时间自动推导起止日期并查询 LBS 轨迹(WGS-84)")
|
||||
public R<WaybillTrackVO> track(@Parameter(description = "运单ID", required = true) @RequestParam Long id) {
|
||||
return R.data(driverWaybillService.track(id));
|
||||
}
|
||||
|
||||
+9
@@ -36,6 +36,7 @@ import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRejectRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRouteAdjustRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionMaintenanceRecordRequest;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||
import org.springblade.transport.service.IExceptionDisposalService;
|
||||
@@ -140,4 +141,12 @@ public class ExceptionDisposalController extends BladeController {
|
||||
return R.success("已调整路线");
|
||||
}
|
||||
|
||||
@PostMapping("/maintenance-record")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "录入维修记录", description = "车辆故障维修类异常的子流程:写一条关联维修记录并追加跟进记录,不结案;一条异常至多一条;仅需登录态")
|
||||
public R submitMaintenanceRecord(@RequestBody ExceptionMaintenanceRecordRequest request) {
|
||||
exceptionDisposalService.submitMaintenanceRecord(request);
|
||||
return R.success("已保存维修记录");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
@@ -38,6 +38,7 @@ import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.hcs.config.HcsProperties;
|
||||
import org.springblade.hcs.dto.HcsContractLegalResultDTO;
|
||||
import org.springblade.hcs.dto.HcsCurrencyVO;
|
||||
import org.springblade.hcs.dto.HcsLegalResultDTO;
|
||||
@@ -70,7 +71,9 @@ import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* HCS 合规推送端点(设计文档 §5.0 手动入口)
|
||||
@@ -99,6 +102,20 @@ public class HcsSyncController {
|
||||
private final HcsInvoiceRegSyncSupport hcsInvoiceRegSyncSupport;
|
||||
private final HcsReceiptBillSyncSupport hcsReceiptBillSyncSupport;
|
||||
private final HcsMasterDataSupport hcsMasterDataSupport;
|
||||
private final HcsProperties hcsProperties;
|
||||
|
||||
/**
|
||||
* 推送开关配置(供项目管理/合同管理列表控制「推送合规」「批量推送合规」显隐)
|
||||
*/
|
||||
@GetMapping("/push-config")
|
||||
@ApiOperationSupport(order = 0)
|
||||
@Operation(summary = "查询HCS推送开关", description = "返回 thirdParty.hcs.push.immediate-enabled,前端据此隐藏即时推送入口")
|
||||
public R<Map<String, Object>> pushConfig() {
|
||||
boolean immediateEnabled = hcsProperties.getPush() != null && hcsProperties.getPush().isImmediateEnabled();
|
||||
Map<String, Object> config = new HashMap<>(2);
|
||||
config.put("immediateEnabled", immediateEnabled);
|
||||
return R.data(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条重推(bizType:project=项目立项 / contract=合同 / payment=付款申请 / invoice-application=开票申请 / invoice-receipt=收票单)
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ public class LoadingManageController extends BladeController {
|
||||
|
||||
@GetMapping("/carrier-contracts")
|
||||
@ApiOperationSupport(order = 13)
|
||||
@Operation(summary = "可选承运商合同", description = "查询已审核生效的承运商合同;传入 waybillIds 时按子运单客户合同甲方匹配后对乙方取交集")
|
||||
@Operation(summary = "可选承运商合同", description = "查询已审核生效的承运商合同;传入 waybillIds 时按子运单客户合同乙方=承运商合同甲方匹配后对乙方取交集")
|
||||
public R<List<LoadingCarrierContractVO>> carrierContracts(
|
||||
@RequestParam List<Long> projectIds,
|
||||
@RequestParam(required = false) List<Long> waybillIds) {
|
||||
|
||||
+94
@@ -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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.vo.ArchiveManageStatsVO;
|
||||
import org.springblade.transport.pojo.vo.DeptTreeNodeVO;
|
||||
import org.springblade.transport.service.IManageArchiveService;
|
||||
import org.springblade.transport.support.MpUserDataScopeSupport;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 调度端档案(小程序管理端)
|
||||
* <p>
|
||||
* 对外完整路径:{@code /api/blade-transport/archive/manage/**}
|
||||
* (网关 StripPrefix 去掉 {@code blade-transport} 后落入 {@code /archive/manage/**})。
|
||||
* 同时兼容未去前缀直连({@code /blade-transport/archive/manage/**})。
|
||||
* 仅需登录态,不挂管理端菜单鉴权。
|
||||
* <p>
|
||||
* 为什么单独一个接口:管理端 {@code /driver/expiry-stat}、{@code /transport-vehicle/expiry-stat}
|
||||
* 挂在 {@code @PreAuth(menu = "driver")} / {@code @PreAuth(menu = "transport_vehicle")} 下,
|
||||
* 小程序登录的令牌 roleId 是占位值 "0"、查不到 blade_role_menu,必然被拒(401 请求未授权)。
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping({"/archive/manage", "/blade-transport/archive/manage"})
|
||||
@Tag(name = "调度端档案", description = "小程序调度端资料页档案统计")
|
||||
public class ManageArchiveController extends BladeController {
|
||||
|
||||
/**
|
||||
* 权限不足的业务码。小程序请求层把 401 当会话失效,接口一律不得回 401。
|
||||
*/
|
||||
private static final int FORBIDDEN_CODE = 403;
|
||||
|
||||
private static final String FORBIDDEN_MSG = "仅调度端管理员可选择组织";
|
||||
|
||||
private final IManageArchiveService manageArchiveService;
|
||||
private final MpUserDataScopeSupport mpUserDataScopeSupport;
|
||||
|
||||
@GetMapping("/stats")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "档案统计", description = "司机档案与车辆档案的总数、30 天内到期数、已到期数;不按组织收口")
|
||||
public R<ArchiveManageStatsVO> stats() {
|
||||
return R.data(manageArchiveService.stats());
|
||||
}
|
||||
|
||||
// 本接口不挂 @PreAuth;无权限返回 false,不抛 403,供小程序决定空态。
|
||||
@GetMapping("/capability")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "档案管理能力判定", description = "当前登录是否为调度端管理员;false 时小程序显示无权限空态,不踢登录")
|
||||
public R<Boolean> capability() {
|
||||
return R.data(mpUserDataScopeSupport.isMpDispatcher());
|
||||
}
|
||||
|
||||
@GetMapping("/dept/tree")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "组织树", description = "小程序组织选择器用:启用中的部门树,已剔除「外部组织」整支;非管理员回业务 403")
|
||||
public R<List<DeptTreeNodeVO>> deptTree() {
|
||||
if (!mpUserDataScopeSupport.isMpDispatcher()) {
|
||||
return R.fail(FORBIDDEN_CODE, FORBIDDEN_MSG);
|
||||
}
|
||||
return R.data(manageArchiveService.deptTree());
|
||||
}
|
||||
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 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.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.vo.DriverVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.support.MpUserDataScopeSupport;
|
||||
import org.springblade.transport.wrapper.DriverWrapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 调度端司机档案管理(小程序)
|
||||
* <p>
|
||||
* 对外完整路径:{@code /api/blade-transport/archive/manage/driver/**}
|
||||
* (网关 StripPrefix 去掉 {@code blade-transport} 后落入 {@code /archive/manage/driver/**}),
|
||||
* 同时兼容未去前缀直连。
|
||||
* <p>
|
||||
* 为什么不复用管理端的 {@code /driver/**}:那些接口挂在 {@code @PreAuth(menu = "driver")} 下,
|
||||
* 小程序令牌的 roleId 是占位值 "0"、查不到 blade_role_menu,对任何小程序账号都必然被拒。
|
||||
* 这里不挂 {@code @PreAuth},改成方法内显式判定管理员身份,且**权限不足必须回业务 403**:
|
||||
* 小程序请求层把 401 一律当会话失效(清令牌 + 回登录页),回 401 会把越权访问变成被踢下线。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping({"/archive/manage/driver", "/blade-transport/archive/manage/driver"})
|
||||
@Tag(name = "调度端司机档案", description = "小程序调度端司机管理")
|
||||
public class ManageArchiveDriverController extends BladeController {
|
||||
|
||||
private static final int DEFAULT_CURRENT = 1;
|
||||
private static final int DEFAULT_SIZE = 10;
|
||||
private static final int MAX_SIZE = 100;
|
||||
|
||||
/**
|
||||
* 权限不足的业务码。小程序请求层把 401 当会话失效,接口一律不得回 401。
|
||||
*/
|
||||
private static final int FORBIDDEN_CODE = 403;
|
||||
|
||||
private static final String FORBIDDEN_MSG = "仅调度端管理员可管理司机档案";
|
||||
|
||||
private final IDriverService driverService;
|
||||
private final MpUserDataScopeSupport mpUserDataScopeSupport;
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "司机档案分页", description = "keyword 同时模糊匹配姓名与手机号;status 不传为启用停用全量;expireStatus 取 within30 / expired")
|
||||
public R<IPage<DriverVO>> page(DriverVO driver, Query query) {
|
||||
if (!canManage()) {
|
||||
return R.fail(FORBIDDEN_CODE, FORBIDDEN_MSG);
|
||||
}
|
||||
fillExpiryDate(driver);
|
||||
return R.data(driverService.selectDriverPage(Condition.getPage(normalizeQuery(query)), driver));
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "司机档案详情")
|
||||
public R<DriverVO> detail(@Parameter(description = "司机主键", required = true) @RequestParam Long id) {
|
||||
if (!canManage()) {
|
||||
return R.fail(FORBIDDEN_CODE, FORBIDDEN_MSG);
|
||||
}
|
||||
Driver query = new Driver();
|
||||
query.setId(id);
|
||||
return R.data(DriverWrapper.build().entityVO(driverService.getOne(Condition.getQueryWrapper(query))));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增或修改
|
||||
*/
|
||||
@PostMapping("/submit")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "新增或修改司机档案", description = "直接写档案、立即生效,不生成待审快照")
|
||||
public R submit(@Valid @RequestBody Driver driver) {
|
||||
if (!canManage()) {
|
||||
return R.fail(FORBIDDEN_CODE, FORBIDDEN_MSG);
|
||||
}
|
||||
// 生效指针只由认证审核流水写(通过才建立生效快照),不接受管理端写入
|
||||
driver.setEffectiveCertRecordId(null);
|
||||
boolean saved = driverService.submit(driver);
|
||||
// submit 内部把空串 trimToNull,update-strategy=not_null 会跳过该字段,导致删不掉大头照;这里显式清空
|
||||
if (saved && driver.getId() != null && Func.isBlank(driver.getHeadPhoto())) {
|
||||
driverService.update(Wrappers.<Driver>lambdaUpdate()
|
||||
.eq(Driver::getId, driver.getId())
|
||||
.set(Driver::getHeadPhoto, null));
|
||||
}
|
||||
return R.status(saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启停用
|
||||
*/
|
||||
@PostMapping("/status")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "启用 / 停用司机", description = "status:1 启用,2 停用。停用司机不进调度检索,但仍可登录")
|
||||
public R status(@Parameter(description = "主键", required = true) @RequestParam Long id,
|
||||
@Parameter(description = "状态:1 启用,2 停用", required = true) @RequestParam Integer status) {
|
||||
if (!canManage()) {
|
||||
return R.fail(FORBIDDEN_CODE, FORBIDDEN_MSG);
|
||||
}
|
||||
return R.status(driverService.changeStatus(id, status));
|
||||
}
|
||||
|
||||
private boolean canManage() {
|
||||
return mpUserDataScopeSupport.isMpDispatcher();
|
||||
}
|
||||
|
||||
private Query normalizeQuery(Query query) {
|
||||
if (query == null) {
|
||||
query = new Query();
|
||||
}
|
||||
if (query.getCurrent() == null || query.getCurrent() < DEFAULT_CURRENT) {
|
||||
query.setCurrent(DEFAULT_CURRENT);
|
||||
}
|
||||
if (query.getSize() == null || query.getSize() <= 0) {
|
||||
query.setSize(DEFAULT_SIZE);
|
||||
}
|
||||
if (query.getSize() > MAX_SIZE) {
|
||||
query.setSize(MAX_SIZE);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private void fillExpiryDate(DriverVO driver) {
|
||||
if (driver.getToday() == null) {
|
||||
driver.setToday(LocalDate.now());
|
||||
}
|
||||
if (driver.getWarningDate() == null) {
|
||||
driver.setWarningDate(driver.getToday().plusDays(30));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 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.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.entity.TransportVehicle;
|
||||
import org.springblade.transport.pojo.vo.TransportVehicleVO;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.support.MpUserDataScopeSupport;
|
||||
import org.springblade.transport.wrapper.TransportVehicleWrapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 调度端车辆档案管理(小程序)
|
||||
* <p>
|
||||
* 对外完整路径:{@code /api/blade-transport/archive/manage/vehicle/**}
|
||||
* (网关 StripPrefix 去掉 {@code blade-transport} 后落入 {@code /archive/manage/vehicle/**}),
|
||||
* 同时兼容未去前缀直连。
|
||||
* <p>
|
||||
* 与管理端 {@code /transport-vehicle/**} 的关系、以及为什么必须回业务 403 而不是 401,
|
||||
* 见 {@link ManageArchiveDriverController} 的类注释。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping({"/archive/manage/vehicle", "/blade-transport/archive/manage/vehicle"})
|
||||
@Tag(name = "调度端车辆档案", description = "小程序调度端车辆管理")
|
||||
public class ManageArchiveVehicleController extends BladeController {
|
||||
|
||||
private static final int DEFAULT_CURRENT = 1;
|
||||
private static final int DEFAULT_SIZE = 10;
|
||||
private static final int MAX_SIZE = 100;
|
||||
|
||||
/**
|
||||
* 权限不足的业务码。小程序请求层把 401 当会话失效,接口一律不得回 401。
|
||||
*/
|
||||
private static final int FORBIDDEN_CODE = 403;
|
||||
|
||||
private static final String FORBIDDEN_MSG = "仅调度端管理员可管理车辆档案";
|
||||
|
||||
private final ITransportVehicleService transportVehicleService;
|
||||
private final MpUserDataScopeSupport mpUserDataScopeSupport;
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "车辆档案分页", description = "keyword 同时模糊匹配车牌、VIN 与所有人;status 不传为启用停用全量;expireStatus 取 within30 / expired")
|
||||
public R<IPage<TransportVehicleVO>> page(TransportVehicleVO vehicle, Query query) {
|
||||
if (!canManage()) {
|
||||
return R.fail(FORBIDDEN_CODE, FORBIDDEN_MSG);
|
||||
}
|
||||
fillExpiryDate(vehicle);
|
||||
return R.data(transportVehicleService.selectTransportVehiclePage(Condition.getPage(normalizeQuery(query)), vehicle));
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "车辆档案详情")
|
||||
public R<TransportVehicleVO> detail(@Parameter(description = "车辆主键", required = true) @RequestParam Long id) {
|
||||
if (!canManage()) {
|
||||
return R.fail(FORBIDDEN_CODE, FORBIDDEN_MSG);
|
||||
}
|
||||
TransportVehicle query = new TransportVehicle();
|
||||
query.setId(id);
|
||||
return R.data(TransportVehicleWrapper.build().entityVO(transportVehicleService.getOne(Condition.getQueryWrapper(query))));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增或修改
|
||||
*/
|
||||
@PostMapping("/submit")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "新增或修改车辆档案", description = "直接写档案、立即生效,不生成待审快照,也不改动认证状态")
|
||||
public R submit(@Valid @RequestBody TransportVehicle vehicle) {
|
||||
if (!canManage()) {
|
||||
return R.fail(FORBIDDEN_CODE, FORBIDDEN_MSG);
|
||||
}
|
||||
// 生效指针只由认证审核流水写(通过才建立生效快照),不接受管理端写入
|
||||
vehicle.setEffectiveCertRecordId(null);
|
||||
return R.status(transportVehicleService.submit(vehicle));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启停用
|
||||
*/
|
||||
@PostMapping("/status")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "启用 / 停用车辆", description = "status:1 启用,2 停用。停用车辆不进车辆检索、司机端车辆列表不再出现")
|
||||
public R status(@Parameter(description = "主键", required = true) @RequestParam Long id,
|
||||
@Parameter(description = "状态:1 启用,2 停用", required = true) @RequestParam Integer status) {
|
||||
if (!canManage()) {
|
||||
return R.fail(FORBIDDEN_CODE, FORBIDDEN_MSG);
|
||||
}
|
||||
return R.status(transportVehicleService.changeStatus(id, status));
|
||||
}
|
||||
|
||||
private boolean canManage() {
|
||||
return mpUserDataScopeSupport.isMpDispatcher();
|
||||
}
|
||||
|
||||
private Query normalizeQuery(Query query) {
|
||||
if (query == null) {
|
||||
query = new Query();
|
||||
}
|
||||
if (query.getCurrent() == null || query.getCurrent() < DEFAULT_CURRENT) {
|
||||
query.setCurrent(DEFAULT_CURRENT);
|
||||
}
|
||||
if (query.getSize() == null || query.getSize() <= 0) {
|
||||
query.setSize(DEFAULT_SIZE);
|
||||
}
|
||||
if (query.getSize() > MAX_SIZE) {
|
||||
query.setSize(MAX_SIZE);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private void fillExpiryDate(TransportVehicleVO vehicle) {
|
||||
if (vehicle.getToday() == null) {
|
||||
vehicle.setToday(LocalDate.now());
|
||||
}
|
||||
if (vehicle.getWarningDate() == null) {
|
||||
vehicle.setWarningDate(vehicle.getToday().plusDays(30));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+2
@@ -66,6 +66,8 @@ public class ContractManageExcel implements Serializable {
|
||||
private String partyA;
|
||||
@ExcelProperty("乙方")
|
||||
private String partyB;
|
||||
@ExcelProperty("丙方")
|
||||
private String partyC;
|
||||
@ExcelProperty("开始日期")
|
||||
private LocalDate startDate;
|
||||
@ExcelProperty("结束日期")
|
||||
|
||||
+5
@@ -147,6 +147,11 @@
|
||||
|
||||
<sql id="QueryCondition">
|
||||
is_deleted = 0
|
||||
<!-- 小程序列表的单框搜索:姓名或手机号命中其一即可。管理端不传 keyword,条件不生效。 -->
|
||||
<if test="driver.keyword != null and driver.keyword != ''">
|
||||
<bind name="driverKeywordLike" value="'%' + driver.keyword + '%'"/>
|
||||
AND (driver_name LIKE #{driverKeywordLike} OR mobile LIKE #{driverKeywordLike})
|
||||
</if>
|
||||
<if test="driver.driverName != null and driver.driverName != ''">
|
||||
<bind name="driverNameLike" value="'%' + driver.driverName + '%'"/>
|
||||
AND driver_name LIKE #{driverNameLike}
|
||||
|
||||
+41
-32
@@ -14,6 +14,7 @@
|
||||
<result column="is_deleted" property="isDeleted"/>
|
||||
<result column="vehicle_type" property="vehicleType"/>
|
||||
<result column="vehicle_no" property="vehicleNo"/>
|
||||
<result column="exception_id" property="exceptionId"/>
|
||||
<result column="maintainer" property="maintainer"/>
|
||||
<result column="maintenance_time" property="maintenanceTime"/>
|
||||
<result column="location" property="location"/>
|
||||
@@ -27,55 +28,63 @@
|
||||
<result column="mileage_unit" property="mileageUnit"/>
|
||||
<result column="attachments" property="attachments"/>
|
||||
<result column="remark" property="remark"/>
|
||||
<result column="waybill_no" property="waybillNo"/>
|
||||
<result column="exception_type" property="exceptionType"/>
|
||||
<result column="exception_reason" property="exceptionReason"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectMaintenanceRecordPage" resultMap="maintenanceRecordResultMap">
|
||||
SELECT
|
||||
id,
|
||||
tenant_id,
|
||||
create_user,
|
||||
create_dept,
|
||||
create_time,
|
||||
update_user,
|
||||
update_time,
|
||||
status,
|
||||
is_deleted,
|
||||
vehicle_type,
|
||||
vehicle_no,
|
||||
maintainer,
|
||||
maintenance_time,
|
||||
location,
|
||||
replaced_part,
|
||||
cost,
|
||||
company,
|
||||
contact,
|
||||
address,
|
||||
factory_time,
|
||||
CASE WHEN mileage = -1 THEN NULL WHEN mileage < 0 THEN 0 ELSE mileage END AS mileage,
|
||||
mileage_unit,
|
||||
attachments,
|
||||
remark
|
||||
r.id,
|
||||
r.tenant_id,
|
||||
r.create_user,
|
||||
r.create_dept,
|
||||
r.create_time,
|
||||
r.update_user,
|
||||
r.update_time,
|
||||
r.status,
|
||||
r.is_deleted,
|
||||
r.vehicle_type,
|
||||
r.vehicle_no,
|
||||
r.exception_id,
|
||||
r.maintainer,
|
||||
r.maintenance_time,
|
||||
r.location,
|
||||
r.replaced_part,
|
||||
r.cost,
|
||||
r.company,
|
||||
r.contact,
|
||||
r.address,
|
||||
r.factory_time,
|
||||
CASE WHEN r.mileage = -1 THEN NULL WHEN r.mileage < 0 THEN 0 ELSE r.mileage END AS mileage,
|
||||
r.mileage_unit,
|
||||
r.attachments,
|
||||
r.remark,
|
||||
d.waybill_no,
|
||||
d.exception_type,
|
||||
d.exception_reason
|
||||
FROM
|
||||
blade_vehicle_maintenance_record
|
||||
blade_vehicle_maintenance_record r
|
||||
LEFT JOIN blade_exception_disposal d ON d.id = r.exception_id AND d.is_deleted = 0
|
||||
WHERE
|
||||
is_deleted = 0
|
||||
r.is_deleted = 0
|
||||
<if test="maintenanceRecord.createDept != null">
|
||||
AND create_dept = #{maintenanceRecord.createDept}
|
||||
AND r.create_dept = #{maintenanceRecord.createDept}
|
||||
</if>
|
||||
<if test="maintenanceRecord.vehicleType != null and maintenanceRecord.vehicleType != ''">
|
||||
AND vehicle_type = #{maintenanceRecord.vehicleType}
|
||||
AND r.vehicle_type = #{maintenanceRecord.vehicleType}
|
||||
</if>
|
||||
<if test="maintenanceRecord.vehicleNo != null and maintenanceRecord.vehicleNo != ''">
|
||||
<bind name="vehicleNoLike" value="'%' + maintenanceRecord.vehicleNo + '%'"/>
|
||||
AND vehicle_no LIKE #{vehicleNoLike}
|
||||
AND r.vehicle_no LIKE #{vehicleNoLike}
|
||||
</if>
|
||||
<if test="maintenanceRecord.createTimeStart != null">
|
||||
AND create_time >= #{maintenanceRecord.createTimeStart}
|
||||
AND r.create_time >= #{maintenanceRecord.createTimeStart}
|
||||
</if>
|
||||
<if test="maintenanceRecord.createTimeEnd != null">
|
||||
AND create_time <= #{maintenanceRecord.createTimeEnd}
|
||||
AND r.create_time <= #{maintenanceRecord.createTimeEnd}
|
||||
</if>
|
||||
ORDER BY create_time DESC
|
||||
ORDER BY r.create_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package org.springblade.transport.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 组织树只读查询(小程序组织选择器)
|
||||
* <p>
|
||||
* 不复用 blade-system 的部门接口:那些接口全挂在 {@code @PreAuth(menu = "dept")} 下,
|
||||
* 小程序令牌的 roleId 是占位值 "0"、查不到 blade_role_menu,任何小程序账号都会被拒。
|
||||
* 这里直接按租户读表,只取组织选择需要的四列。
|
||||
*/
|
||||
public interface ManageArchiveDeptMapper {
|
||||
|
||||
/**
|
||||
* 租户内启用中的部门,按排序号升序(不含已删除)
|
||||
*/
|
||||
@Select("SELECT id, parent_id AS parentId, dept_name AS deptName, sort FROM blade_dept "
|
||||
+ "WHERE is_deleted = 0 AND status = 1 AND tenant_id = #{tenantId} "
|
||||
+ "ORDER BY sort, id")
|
||||
List<Dept> selectEnabledByTenant(@Param("tenantId") String tenantId);
|
||||
|
||||
}
|
||||
+5
@@ -116,6 +116,11 @@
|
||||
|
||||
<sql id="QueryCondition">
|
||||
is_deleted = 0
|
||||
<!-- 小程序列表的单框搜索:车牌、VIN 或所有人命中其一即可。管理端不传 keyword,条件不生效。 -->
|
||||
<if test="vehicle.keyword != null and vehicle.keyword != ''">
|
||||
<bind name="vehicleKeywordLike" value="'%' + vehicle.keyword + '%'"/>
|
||||
AND (plate_no LIKE #{vehicleKeywordLike} OR vin LIKE #{vehicleKeywordLike} OR owner_name LIKE #{vehicleKeywordLike})
|
||||
</if>
|
||||
<if test="vehicle.organizationName != null and vehicle.organizationName != ''">
|
||||
<!-- 「未分配组织」哨兵值定义在 TransportBusiConstant.UNASSIGNED_ORG_FILTER 与前端
|
||||
tms-erp-web/src/mixins/organization-search.js,改这里必须同步常量与 DriverMapper.xml 的同名分支。
|
||||
|
||||
+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.BusinessLicenseOcrVO;
|
||||
|
||||
/**
|
||||
* 营业执照 OCR 服务。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public interface IBusinessLicenseOcrService {
|
||||
|
||||
/**
|
||||
* 根据图片地址识别营业执照。
|
||||
*
|
||||
* @param imageUrl 图片地址
|
||||
* @return 识别结果
|
||||
*/
|
||||
BusinessLicenseOcrVO recognizeUrl(String imageUrl);
|
||||
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 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.IBusinessLicenseOcrService;
|
||||
import org.springblade.transport.pojo.vo.BusinessLicenseOcrVO;
|
||||
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 服务实现,调用第三方 /businessLicense 接口。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class BusinessLicenseOcrServiceImpl implements IBusinessLicenseOcrService {
|
||||
|
||||
private static final String PATH = "/businessLicense";
|
||||
private static final String DATA_TYPE_URL = "url";
|
||||
private static final int SUCCESS_CODE = 0;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
/**
|
||||
* 营业执照 OCR 服务地址,Nacos:thirdParty.ocr.businessLicenseBaseUrl
|
||||
*/
|
||||
@Value("${thirdParty.ocr.businessLicenseBaseUrl:http://124.71.22.123:8888}")
|
||||
private String businessLicenseBaseUrl;
|
||||
|
||||
public BusinessLicenseOcrServiceImpl(ObjectMapper objectMapper,
|
||||
@Qualifier("baiduOcrHttpClient") HttpClient httpClient) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BusinessLicenseOcrVO recognizeUrl(String imageUrl) {
|
||||
if (StringUtil.isBlank(imageUrl)) {
|
||||
throw new ServiceException("OCR图片地址不能为空");
|
||||
}
|
||||
String normalizedUrl = imageUrl.trim();
|
||||
validateImageUrl(normalizedUrl);
|
||||
if (StringUtil.isBlank(businessLicenseBaseUrl)) {
|
||||
throw new ServiceException("营业执照OCR服务地址未配置(thirdParty.ocr.businessLicenseBaseUrl)");
|
||||
}
|
||||
String requestUrl = businessLicenseBaseUrl.replaceAll("/+$", "") + PATH;
|
||||
Map<String, String> body = new LinkedHashMap<>();
|
||||
body.put("img_path", normalizedUrl);
|
||||
body.put("data_type", DATA_TYPE_URL);
|
||||
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);
|
||||
}
|
||||
BusinessLicenseOcrVO result = new BusinessLicenseOcrVO();
|
||||
result.setFields(extractFields(response.path("data")));
|
||||
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 dataNode) {
|
||||
Map<String, String> fields = new LinkedHashMap<>();
|
||||
if (dataNode == null || !dataNode.isArray()) {
|
||||
return fields;
|
||||
}
|
||||
for (JsonNode item : dataNode) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 组织树节点(小程序组织选择器)
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "组织树节点")
|
||||
public class DeptTreeNodeVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 部门主键:雪花 ID 超出 JS 安全整数,序列化成字符串,前端不做 Number()
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@Schema(description = "部门主键")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "部门名称")
|
||||
private String name;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@Schema(description = "子部门")
|
||||
private List<DeptTreeNodeVO> children = new ArrayList<>();
|
||||
|
||||
}
|
||||
+5
-4
@@ -74,12 +74,13 @@ public interface ICertAuditService {
|
||||
/**
|
||||
* 审核一条认证记录
|
||||
*
|
||||
* @param recordId 认证记录主键
|
||||
* @param pass true 通过,false 驳回
|
||||
* @param rejectReason 驳回原因;驳回时必填,≤200 字
|
||||
* @param recordId 认证记录主键
|
||||
* @param pass true 通过,false 驳回
|
||||
* @param rejectReason 驳回原因;驳回时必填,≤200 字
|
||||
* @param auditedSnapshotJson 审核员修订后的全量认证内容;空或空白表示不修订,驳回时忽略
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean audit(Long recordId, boolean pass, String rejectReason);
|
||||
boolean audit(Long recordId, boolean pass, String rejectReason, String auditedSnapshotJson);
|
||||
|
||||
/**
|
||||
* 当前登录用户的审核能力:能否查看、能否审核
|
||||
|
||||
+4
-3
@@ -75,12 +75,13 @@ public interface IContractManageService extends BaseService<ContractManage> {
|
||||
List<ContractManageExcel> exportContractManage(ContractManageVO contractManage, String ids);
|
||||
|
||||
/**
|
||||
* 预览下一合同编号(按合同负责部门前缀 + 年份 + 三位流水)
|
||||
* 预览下一合同编号(按平台公司 + 业务部门配置的前缀 + 年份 + 三位流水)
|
||||
*
|
||||
* @param organizationId 合同负责部门ID
|
||||
* @param organizationId 合同负责部门(业务部门)ID
|
||||
* @param platformCompanyId 平台公司ID
|
||||
* @return 预览编号
|
||||
*/
|
||||
String nextContractNo(Long organizationId);
|
||||
String nextContractNo(Long organizationId, Long platformCompanyId);
|
||||
|
||||
/**
|
||||
* 合同是否可用于生成结算单:已审批、未终止,且临时合同仍在临时效力期内。
|
||||
|
||||
+5
-6
@@ -34,11 +34,11 @@ import java.util.List;
|
||||
public interface IDriverAppService {
|
||||
|
||||
/**
|
||||
* 当前登录用户对应的司机档案(按手机号匹配 blade_transport_driver.mobile)
|
||||
* 当前登录用户对应的司机档案(按 user_id 匹配 blade_transport_driver.user_id)
|
||||
*
|
||||
* @param mobile 小程序可显式传入当前用户手机号;为空时从登录态解析
|
||||
* @return 司机档案;未登录抛异常,查不到返回 null
|
||||
*/
|
||||
DriverVO currentByPhone(String mobile);
|
||||
DriverVO current();
|
||||
|
||||
/**
|
||||
* 当前司机绑定的车辆列表(driver.driving_vehicle ↔ vehicle.plate_no)
|
||||
@@ -49,10 +49,9 @@ public interface IDriverAppService {
|
||||
* 当前登录用户对应的司机**实体**
|
||||
* <p>
|
||||
* 认证提交要写回司机表,且越权校验要读 {@code driving_vehicle},
|
||||
* 故这里返回实体而非视图对象。解析顺序:先按登录手机号匹配,
|
||||
* 匹配不到再按 {@code user_id} 兜底。
|
||||
* 故这里返回实体而非视图对象。解析口径与 {@link #current()} 一致。
|
||||
*
|
||||
* @return 司机实体;未登录或查不到时返回 null
|
||||
* @return 司机实体;未登录抛异常,查不到返回 null
|
||||
*/
|
||||
Driver currentDriverEntity();
|
||||
|
||||
|
||||
+12
@@ -111,4 +111,16 @@ public interface IDriverService extends BaseService<Driver> {
|
||||
*/
|
||||
List<DriverExcel> exportDriver(Wrapper<Driver> queryWrapper);
|
||||
|
||||
/**
|
||||
* 按手机号解析司机档案 ID
|
||||
* <p>
|
||||
* 归属主键是档案 ID,手机号只是派单时的号码快照:只有租户内唯一命中才认,
|
||||
* 0 条(承运商司机、未登录过小程序的司机)与多条(历史脏数据)都返回 null,不抛异常。
|
||||
*
|
||||
* @param tenantId 租户ID,为空时取当前登录租户
|
||||
* @param mobile 手机号
|
||||
* @return 司机档案ID;无法唯一确定时为 null
|
||||
*/
|
||||
Long resolveDriverIdByMobile(String tenantId, String mobile);
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -28,6 +28,7 @@ import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRejectRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRouteAdjustRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionMaintenanceRecordRequest;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||
|
||||
@@ -75,4 +76,10 @@ public interface IExceptionDisposalService extends BaseService<ExceptionDisposal
|
||||
*/
|
||||
void adjustRoute(ExceptionDisposalRouteAdjustRequest request);
|
||||
|
||||
/**
|
||||
* 录入关联维修记录:写一条维修记录并追加一条跟进记录,不结案。
|
||||
* 一条异常至多一条维修记录;已撤销的异常不允许录入。
|
||||
*/
|
||||
void submitMaintenanceRecord(ExceptionMaintenanceRecordRequest request);
|
||||
|
||||
}
|
||||
|
||||
+17
@@ -103,6 +103,23 @@ public interface IFormalSettlementService extends BaseService<FormalSettlement>
|
||||
String retryKingdeeFailed();
|
||||
|
||||
String syncKingdeeArResult();
|
||||
|
||||
/**
|
||||
* 金蝶冲销状态定时回写:扫描已同步金蝶的结算单,按防重键批量查询 A102-4/A302-4 的
|
||||
* hadwrittenoff(部分冲销视同已冲销),回写冲销状态供结算单列表展示
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String syncKingdeeWriteOffResult();
|
||||
|
||||
/**
|
||||
* 单笔刷新金蝶冲销状态(选单预检/重推闸门实时查询后回写,状态未变化不落库)
|
||||
*
|
||||
* @param settlementId 正式结算单id
|
||||
* @param writtenOff 金蝶侧应付/应收单是否已冲销
|
||||
*/
|
||||
void refreshKingdeeWriteOff(Long settlementId, boolean writtenOff);
|
||||
|
||||
List<FormalSettlementDetailFee> detailFees(Long detailId);
|
||||
void adjustDetail(PreSettlementDetailAdjustRequest request);
|
||||
void refreshPaymentSummary(Long settlementId);
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ public interface ILoadingManageService extends BaseService<LoadingManage> {
|
||||
/**
|
||||
* 查询项目下已审核生效的承运商合同。
|
||||
* <p>
|
||||
* 传入 waybillIds 时,按各子运单客户合同甲方匹配可用承运商合同,再对乙方名称取交集后返回。
|
||||
* 传入 waybillIds 时,按各子运单客户合同乙方=承运商合同甲方匹配可用承运商合同,再对乙方名称取交集后返回。
|
||||
*/
|
||||
List<LoadingCarrierContractVO> carrierContracts(List<Long> projectIds, List<Long> waybillIds);
|
||||
|
||||
|
||||
+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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.service;
|
||||
|
||||
import org.springblade.transport.pojo.vo.ArchiveManageStatsVO;
|
||||
import org.springblade.transport.pojo.vo.DeptTreeNodeVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 调度端档案统计
|
||||
*/
|
||||
public interface IManageArchiveService {
|
||||
|
||||
/**
|
||||
* 司机与车辆档案的证件有效期统计
|
||||
*/
|
||||
ArchiveManageStatsVO stats();
|
||||
|
||||
/**
|
||||
* 组织树:启用中的部门,已剔除「外部组织」整支
|
||||
*/
|
||||
List<DeptTreeNodeVO> deptTree();
|
||||
|
||||
}
|
||||
+1
-1
@@ -41,7 +41,7 @@ public interface ITripQueryService {
|
||||
* <p>
|
||||
* 库层只取回收口与行级筛选命中的运单行,分组、分档、筛选、排序与分页都在服务层算完再做。
|
||||
*
|
||||
* @param query 查询条件,收口由 driverPhone 或 allDrivers 给出
|
||||
* @param query 查询条件,收口由 driverId(司机档案)或 allDrivers 给出
|
||||
* @return 运次分页
|
||||
*/
|
||||
IPage<TripVO> page(TripQueryVO query);
|
||||
|
||||
+155
-19
@@ -26,6 +26,7 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -138,20 +139,24 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
detail.setAuditUserName(UserCache.getUserRealName(record.getAuditUser()));
|
||||
detail.setRejectReason(record.getRejectReason());
|
||||
detail.setPendingSnapshotJson(record.getSnapshotJson());
|
||||
// 审核员上次填过的修订版:详情页要能回显,否则重开页面就丢了
|
||||
detail.setAuditedSnapshotJson(record.getAuditedSnapshotJson());
|
||||
|
||||
// 生效值一侧:从未认证通过时为 null,前端按「无对比基准」处理
|
||||
detail.setEffectiveRecordId(effectiveId);
|
||||
detail.setCertified(CertificationRuleSupport.isCertified(effectiveId));
|
||||
if (Func.isNotEmpty(effectiveId)) {
|
||||
TransportCertRecord effective = certRecordService.getById(effectiveId);
|
||||
detail.setEffectiveSnapshotJson(effective == null ? null : effective.getSnapshotJson());
|
||||
// 生效一侧同样修订版优先:审核页是拿「新提交」和「当前生效内容」做 diff 的,
|
||||
// 这里取旧值会让审核员看到一份从未生效过的差异
|
||||
detail.setEffectiveSnapshotJson(CertificationRuleSupport.effectiveSnapshotJson(effective));
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean audit(Long recordId, boolean pass, String rejectReason) {
|
||||
public boolean audit(Long recordId, boolean pass, String rejectReason, String auditedSnapshotJson) {
|
||||
TransportCertRecord record = requireRecord(recordId);
|
||||
|
||||
// 终态不可再审。若允许,把指针指着的那条「已通过」改成「已驳回」,
|
||||
@@ -165,6 +170,17 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
+ TransportCertRecord.REJECT_REASON_MAX_LENGTH + "字)");
|
||||
}
|
||||
|
||||
// 修订版只有「通过」才可能生效:驳回不产生生效内容,传了也不该留痕,
|
||||
// 否则历史里会出现一条「已驳回却带着生效内容」的记录。
|
||||
String revision = pass ? blankToNull(auditedSnapshotJson) : null;
|
||||
// 校验放在写库之前——内容不可用或改动了身份字段就整体回滚,不能先落一条状态再报错。
|
||||
if (revision != null) {
|
||||
requireUsableRevision(record, revision);
|
||||
requireIdentityUnchanged(record, revision);
|
||||
}
|
||||
// 写回档案的是「本次生效内容」:有修订用修订版,没有就用司机原提交。
|
||||
String effectiveSnapshotJson = revision == null ? record.getSnapshotJson() : revision;
|
||||
|
||||
Long auditor = AuthUtil.getUserId();
|
||||
LocalDateTime auditTime = LocalDateTime.now();
|
||||
|
||||
@@ -177,11 +193,13 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
: CertificationRuleSupport.reasonForReject(rejectReason));
|
||||
patch.setAuditUser(auditor);
|
||||
patch.setAuditTime(auditTime);
|
||||
// 未修订时留 null:框架的 not_null 更新策略会跳过该列,库里保持 NULL 而不是空串
|
||||
patch.setAuditedSnapshotJson(revision);
|
||||
certRecordService.updateById(patch);
|
||||
|
||||
// 2) 认证对象:通过才切指针;驳回只刷展示缓存。两者合并为**一次** UPDATE,
|
||||
// 避免对同一行连续写两次(第二次会把第一次的指针覆盖掉的风险也随之消失)。
|
||||
updateCertTarget(record, pass, rejectReason);
|
||||
updateCertTarget(record, pass, rejectReason, effectiveSnapshotJson);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -212,11 +230,12 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
* </ul>
|
||||
* 分两次写同一行除了多一次往返,还会让「先切指针、后被展示缓存更新覆盖」成为可能。
|
||||
*
|
||||
* @param record 被审核的记录
|
||||
* @param pass 是否通过
|
||||
* @param rejectReason 驳回原因
|
||||
* @param record 被审核的记录
|
||||
* @param pass 是否通过
|
||||
* @param rejectReason 驳回原因
|
||||
* @param effectiveSnapshotJson 本次生效内容(修订版优先,否则司机原提交)
|
||||
*/
|
||||
private void updateCertTarget(TransportCertRecord record, boolean pass, String rejectReason) {
|
||||
private void updateCertTarget(TransportCertRecord record, boolean pass, String rejectReason, String effectiveSnapshotJson) {
|
||||
boolean vehicleTarget = TransportCertRecord.BIZ_TYPE_VEHICLE.equals(record.getBizType());
|
||||
Integer status = pass ? TransportCertRecord.STATUS_APPROVED : TransportCertRecord.STATUS_REJECTED;
|
||||
// 只有通过才切指针;驳回时保持 null,框架的 not_null 更新策略会跳过该列
|
||||
@@ -232,7 +251,7 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
// 不写回的话,审核通过之后管理端与司机端看到的仍是提交之前的旧值——
|
||||
// 状态显示「已通过」、内容却还是旧的,是最容易让人误判的一种不一致。
|
||||
// 与指针合并成同一次 UPDATE。
|
||||
applySnapshotToVehicle(vehicle, record.getSnapshotJson());
|
||||
applySnapshotToVehicle(vehicle, effectiveSnapshotJson);
|
||||
} else {
|
||||
vehicle.setCertificationRejectReason(CertificationRuleSupport.reasonForReject(rejectReason));
|
||||
}
|
||||
@@ -255,7 +274,7 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
// 不写回的话,司机档案里始终是管理端当初录入的旧值(甚至为空),
|
||||
// 而认证状态已是「已通过」——状态与内容不一致最容易让人误判。
|
||||
// 与指针合并成同一次 UPDATE。
|
||||
applySnapshotToDriver(driver, record.getSnapshotJson());
|
||||
applySnapshotToDriver(driver, effectiveSnapshotJson);
|
||||
} else {
|
||||
driver.setCertificationRejectReason(CertificationRuleSupport.reasonForReject(rejectReason));
|
||||
}
|
||||
@@ -332,6 +351,122 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验审核员填写的修订版内容,不满足时抛错
|
||||
* <p>
|
||||
* 修订版是审核员的「最终认定」,会直接写回档案。因此必须能反序列化成当前提交结构,
|
||||
* 且带上判定「这份内容有效」的关键字段,否则写回等于什么也没写:
|
||||
* 司机看身份证号与驾驶证号,车辆看行驶证号与核定载质量。
|
||||
* <p>
|
||||
* 不满足时抛错、整体回滚——审核员填错 JSON 是操作失误,不能悄悄降级成「不修订」,
|
||||
* 那样他会以为自己的修订已经生效。
|
||||
*
|
||||
* @param record 被审核的记录
|
||||
* @param revision 已去空白的修订版内容
|
||||
*/
|
||||
private void requireUsableRevision(TransportCertRecord record, String revision) {
|
||||
JSONObject parsed = parseRevision(revision);
|
||||
if (TransportCertRecord.BIZ_TYPE_VEHICLE.equals(record.getBizType())) {
|
||||
VehicleCertSubmitRequest snapshot = toSnapshot(parsed, VehicleCertSubmitRequest.class);
|
||||
if (snapshot == null
|
||||
|| (Func.isEmpty(snapshot.getDrivingLicenseNo()) && Func.isEmpty(snapshot.getApprovedLoadKg()))) {
|
||||
throw new ServiceException("修订内容缺少行驶证号与核定载质量,无法写入车辆档案");
|
||||
}
|
||||
return;
|
||||
}
|
||||
PersonCertSubmitRequest snapshot = toSnapshot(parsed, PersonCertSubmitRequest.class);
|
||||
if (snapshot == null
|
||||
|| (Func.isEmpty(snapshot.getIdCardNo()) && Func.isEmpty(snapshot.getDriverFileNo()))) {
|
||||
throw new ServiceException("修订内容缺少身份证号与驾驶证号,无法写入司机档案");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 身份字段不可修订:修订版改动了身份证号、驾驶证号(司机)或车牌号(车辆)时拒绝
|
||||
* <p>
|
||||
* 审核只认定证件内容,身份字段标识「是谁」,改了审核的就是另一个对象。
|
||||
* 原提交里该字段为空时(旧数据、空快照)没有可比对的原值,不比对,允许补全。
|
||||
*
|
||||
* @param record 被审核的记录
|
||||
* @param revision 已去空白的修订版内容
|
||||
*/
|
||||
private void requireIdentityUnchanged(TransportCertRecord record, String revision) {
|
||||
JSONObject parsed = parseRevision(revision);
|
||||
if (parsed == null) {
|
||||
return;
|
||||
}
|
||||
if (TransportCertRecord.BIZ_TYPE_VEHICLE.equals(record.getBizType())) {
|
||||
VehicleCertSubmitRequest original = parseSnapshotQuietly(record.getSnapshotJson(), VehicleCertSubmitRequest.class);
|
||||
if (original == null) {
|
||||
return;
|
||||
}
|
||||
VehicleCertSubmitRequest revised = toSnapshot(parsed, VehicleCertSubmitRequest.class);
|
||||
requireSameIdentity(original.getPlateNo(), revised.getPlateNo(), "修订内容不能修改车牌号");
|
||||
return;
|
||||
}
|
||||
PersonCertSubmitRequest original = parseSnapshotQuietly(record.getSnapshotJson(), PersonCertSubmitRequest.class);
|
||||
if (original == null) {
|
||||
return;
|
||||
}
|
||||
String message = "修订内容不能修改身份证号与驾驶证号";
|
||||
PersonCertSubmitRequest revised = toSnapshot(parsed, PersonCertSubmitRequest.class);
|
||||
requireSameIdentity(original.getIdCardNo(), revised.getIdCardNo(), message);
|
||||
requireSameIdentity(original.getDriverFileNo(), revised.getDriverFileNo(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原提交有值时,修订版必须与之一致;原提交为空时允许补全
|
||||
*
|
||||
* @param original 原提交里的身份字段值
|
||||
* @param revised 修订版里的身份字段值
|
||||
* @param message 不一致时抛出的文案
|
||||
*/
|
||||
private void requireSameIdentity(String original, String revised, String message) {
|
||||
if (Func.isEmpty(original)) {
|
||||
return;
|
||||
}
|
||||
if (!original.equals(revised)) {
|
||||
throw new ServiceException(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化审核员填写的修订版 JSON
|
||||
*
|
||||
* @param revision 已去空白的修订版内容
|
||||
* @return 解析结果;内容为字面量 null 时返回 null
|
||||
*/
|
||||
private JSONObject parseRevision(String revision) {
|
||||
try {
|
||||
return JSON.parseObject(revision);
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("修订内容格式不正确,请检查后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析原提交快照;为空或无法解析时返回 null(此时没有可比对的原值)
|
||||
*
|
||||
* @param snapshotJson 原提交内容
|
||||
* @param clazz 目标结构
|
||||
* @param <T> 提交结构类型
|
||||
* @return 解析结果;为空或解析失败时返回 null
|
||||
*/
|
||||
private <T> T parseSnapshotQuietly(String snapshotJson, Class<T> clazz) {
|
||||
if (Func.isEmpty(snapshotJson)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parseObject(snapshotJson, clazz);
|
||||
} catch (Exception exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T toSnapshot(JSONObject parsed, Class<T> clazz) {
|
||||
return parsed == null ? null : parsed.toJavaObject(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把生效快照里的司机证件字段搬进司机档案(就地写入同一个 patch 对象)
|
||||
* <p>
|
||||
@@ -345,33 +480,34 @@ public class CertAuditServiceImpl implements ICertAuditService {
|
||||
* 快照里为 null 的字段不会写(框架的 not_null 更新策略会跳过),
|
||||
* 因此不会把管理端填过、司机端没填的字段清空。
|
||||
* <p>
|
||||
* 快照缺失、无法解析或不是当前字段定义时**只跳过写回,不阻断审核**:
|
||||
* 审核本身是独立于写回的业务动作(切指针、置状态),不该因为一份读不懂的快照而失败;
|
||||
* 何况历史快照本来就存在,阻断会让这些对象再也审不过去。
|
||||
* 申报的身份证号已落在另一名司机名下时是例外——那会把两条档案写成同一个证件号,
|
||||
* 必须阻断。
|
||||
* 快照缺失、无法解析或不是当前字段定义时**直接拒绝本次通过并整体回滚**:
|
||||
* 「通过」的语义是「把这批证件内容认定为生效值」,没有可用内容却放行,
|
||||
* 等于给一条空记录盖上已通过的章,状态与内容互相矛盾。
|
||||
* 内容确实为空的历史记录,由审核员在审核时补全后重审即可。
|
||||
* 申报的身份证号已落在另一名司机名下时同样阻断——那会把两条档案写成同一个证件号。
|
||||
*
|
||||
* @param driver 待更新的司机 patch
|
||||
* @param snapshotJson 该条记录的全量提交内容
|
||||
*/
|
||||
private void applySnapshotToDriver(Driver driver, String snapshotJson) {
|
||||
if (Func.isEmpty(snapshotJson)) {
|
||||
return;
|
||||
throw new ServiceException("该认证记录没有提交内容,无法写入司机档案;请让司机重新提交一次认证");
|
||||
}
|
||||
PersonCertSubmitRequest snapshot;
|
||||
try {
|
||||
snapshot = JSON.parseObject(snapshotJson, PersonCertSubmitRequest.class);
|
||||
} catch (Exception exception) {
|
||||
return;
|
||||
throw new ServiceException("该认证记录的提交内容无法解析,未写入司机档案;请重新提交一次认证");
|
||||
}
|
||||
if (snapshot == null) {
|
||||
return;
|
||||
throw new ServiceException("该认证记录的提交内容无法解析,未写入司机档案;请重新提交一次认证");
|
||||
}
|
||||
// 字段名对不上时 JSON 会解析成「全是 null」而不报错(历史快照就是这种情况),
|
||||
// 那样写回等于什么也没写,还会把档案里已有的值当成「没填」而跳过。
|
||||
// 故用证件内容里的关键字段判定「这份快照是否是当前格式」,不是就不写。
|
||||
// 故用证件内容里的关键字段判定「这份快照是否是当前格式」,不是就拒绝。
|
||||
if (Func.isEmpty(snapshot.getIdCardNo()) && Func.isEmpty(snapshot.getDriverFileNo())) {
|
||||
return;
|
||||
throw new ServiceException("该认证记录的提交内容与当前字段定义不匹配(可能是早期格式的历史快照),"
|
||||
+ "未写入司机档案;请重新提交一次认证");
|
||||
}
|
||||
// 身份证号要落到档案上,先确认它没有落在别人名下:重复证件号一旦落库,
|
||||
// 建档查重与按证件号识别同一人都会失效
|
||||
|
||||
+1
-1
@@ -310,7 +310,7 @@ public class CommonAddressServiceImpl extends BaseServiceImpl<CommonAddressMappe
|
||||
|
||||
private void fillDisplay(CommonAddressVO address) {
|
||||
address.setSiteCodeDisplay(TYPE_NORMAL.equals(address.getAddressType()) ? NORMAL_SITE_CODE : address.getSiteCode());
|
||||
address.setReadonly(!TransportBusinessSupport.isAdministrator() && !Objects.equals(address.getDeptId(), currentDeptId()));
|
||||
address.setReadonly(TransportBusinessSupport.isDeptReadonly(address.getDeptId(), TransportBusinessSupport.MENU_COMMON_ADDRESS));
|
||||
address.setCreateUserName(UserCache.getUserRealName(address.getCreateUser()));
|
||||
address.setUpdateUserName(UserCache.getUserRealName(address.getUpdateUser()));
|
||||
}
|
||||
|
||||
+28
-5
@@ -44,6 +44,7 @@ import org.springblade.transport.pojo.vo.ContractChangeVO;
|
||||
import org.springblade.transport.pojo.vo.ContractManageVO;
|
||||
import org.springblade.transport.service.IContractChangeService;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.support.ContractPartySupport;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.wrapper.ContractChangeWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -316,9 +317,12 @@ public class ContractChangeServiceImpl extends BaseServiceImpl<ContractChangeMap
|
||||
} else if (!Objects.equals(contractChange.getAllDept(), 1)) {
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("合同变更");
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
queryWrapper.and(wrapper -> wrapper.eq(ContractChange::getOrganizationId, currentDeptId)
|
||||
.or().eq(ContractChange::getCreateUser, currentUserId)
|
||||
.or().eq(ContractChange::getHandlerUserId, currentUserId));
|
||||
queryWrapper.and(wrapper -> {
|
||||
wrapper.eq(ContractChange::getOrganizationId, currentDeptId)
|
||||
.or().eq(ContractChange::getCreateUser, currentUserId)
|
||||
.or().eq(ContractChange::getHandlerUserId, currentUserId);
|
||||
TransportBusinessSupport.orProjectVisibleByBusinessDept(wrapper, List.of(currentDeptId));
|
||||
});
|
||||
} else if (Func.isNotEmpty(contractChange.getOrganizationId())) {
|
||||
queryWrapper.eq(ContractChange::getOrganizationId, contractChange.getOrganizationId());
|
||||
}
|
||||
@@ -358,7 +362,20 @@ public class ContractChangeServiceImpl extends BaseServiceImpl<ContractChangeMap
|
||||
target.setSignType(source.getSignType());
|
||||
target.setContractNo(source.getContractNo());
|
||||
target.setContractName(source.getContractName());
|
||||
target.setPartyB(source.getPartyB());
|
||||
target.setPartyAJson(source.getPartyAJson());
|
||||
target.setPartyBJson(source.getPartyBJson());
|
||||
target.setPartyCJson(source.getPartyCJson());
|
||||
ContractManage partyHolder = new ContractManage();
|
||||
partyHolder.setPartyAJson(target.getPartyAJson());
|
||||
partyHolder.setPartyBJson(target.getPartyBJson());
|
||||
partyHolder.setPartyCJson(target.getPartyCJson());
|
||||
ContractPartySupport.normalizeAndFillSummary(partyHolder);
|
||||
target.setPartyAJson(partyHolder.getPartyAJson());
|
||||
target.setPartyBJson(partyHolder.getPartyBJson());
|
||||
target.setPartyCJson(partyHolder.getPartyCJson());
|
||||
target.setPartyA(partyHolder.getPartyA());
|
||||
target.setPartyB(partyHolder.getPartyB());
|
||||
target.setPartyC(partyHolder.getPartyC());
|
||||
target.setStartDate(source.getStartDate());
|
||||
target.setEndDate(source.getEndDate());
|
||||
target.setContractFormat(source.getContractFormat());
|
||||
@@ -391,7 +408,12 @@ public class ContractChangeServiceImpl extends BaseServiceImpl<ContractChangeMap
|
||||
data.put("contractNo", contractManage.getContractNo());
|
||||
data.put("contractName", contractManage.getContractName());
|
||||
data.put("signType", contractManage.getSignType());
|
||||
data.put("partyA", contractManage.getPartyA());
|
||||
data.put("partyB", contractManage.getPartyB());
|
||||
data.put("partyC", contractManage.getPartyC());
|
||||
data.put("partyAJson", contractManage.getPartyAJson());
|
||||
data.put("partyBJson", contractManage.getPartyBJson());
|
||||
data.put("partyCJson", contractManage.getPartyCJson());
|
||||
data.put("startDate", contractManage.getStartDate());
|
||||
data.put("endDate", contractManage.getEndDate());
|
||||
data.put("contractFormat", contractManage.getContractFormat());
|
||||
@@ -504,7 +526,8 @@ public class ContractChangeServiceImpl extends BaseServiceImpl<ContractChangeMap
|
||||
return true;
|
||||
}
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("合同变更");
|
||||
return currentDeptId != null && Objects.equals(contractChange.getOrganizationId(), currentDeptId);
|
||||
return currentDeptId != null && (Objects.equals(contractChange.getOrganizationId(), currentDeptId)
|
||||
|| TransportBusinessSupport.isProjectOwnedByBusinessDept(contractChange.getProjectId(), currentDeptId));
|
||||
}
|
||||
|
||||
private void fillReadonly(ContractChangeVO contractChangeVO) {
|
||||
|
||||
+83
-43
@@ -36,6 +36,7 @@ import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.SysCache;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.system.pojo.entity.BusinessDeptConfig;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.transport.constant.HcsSyncConstant;
|
||||
import org.springblade.transport.excel.ContractManageExcel;
|
||||
@@ -48,6 +49,7 @@ import org.springblade.transport.pojo.entity.HcsContractExeStage;
|
||||
import org.springblade.transport.pojo.vo.ContractManageVO;
|
||||
import org.springblade.transport.service.IContractExeStageService;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.support.ContractPartySupport;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
|
||||
import org.springblade.transport.support.hcs.HcsImmediatePushSupport;
|
||||
@@ -60,7 +62,6 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -449,7 +450,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
request.setSignType(change.getSignType());
|
||||
request.setContractNo(change.getContractNo());
|
||||
request.setContractName(change.getContractName());
|
||||
request.setPartyB(change.getPartyB());
|
||||
request.setPartyAJson(change.getPartyAJson());
|
||||
request.setPartyBJson(change.getPartyBJson());
|
||||
request.setPartyCJson(change.getPartyCJson());
|
||||
ContractPartySupport.normalizeAndFillSummary(request);
|
||||
request.setStartDate(change.getStartDate());
|
||||
request.setEndDate(change.getEndDate());
|
||||
request.setContractFormat(change.getContractFormat());
|
||||
@@ -510,13 +514,23 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
TransportBusinessSupport.validateRequired(request.getSettlementCurrency(), "请选择结算币种");
|
||||
ContractManage candidate = new ContractManage();
|
||||
BeanUtil.copyProperties(source, candidate);
|
||||
candidate.setPartyB(TransportBusinessSupport.trimToNull(request.getPartyB()));
|
||||
candidate.setPartyAJson(request.getPartyAJson());
|
||||
candidate.setPartyBJson(request.getPartyBJson());
|
||||
candidate.setPartyCJson(request.getPartyCJson());
|
||||
ContractPartySupport.normalizeAndFillSummary(candidate);
|
||||
ContractPartySupport.validateRequiredParties(candidate, true);
|
||||
ContractPartySupport.validateBillingPlanBindings(candidate);
|
||||
validateContractPartyCombinationUnique(candidate);
|
||||
validatePaymentRatios(request.getPaymentRatioJson());
|
||||
applyContractNo(source, request.getContractNo(), false);
|
||||
source.setContractName(request.getContractName());
|
||||
source.setSignType(request.getSignType());
|
||||
source.setPartyB(request.getPartyB());
|
||||
source.setPartyAJson(candidate.getPartyAJson());
|
||||
source.setPartyBJson(candidate.getPartyBJson());
|
||||
source.setPartyCJson(candidate.getPartyCJson());
|
||||
source.setPartyA(candidate.getPartyA());
|
||||
source.setPartyB(candidate.getPartyB());
|
||||
source.setPartyC(candidate.getPartyC());
|
||||
source.setStartDate(request.getStartDate());
|
||||
source.setEndDate(request.getEndDate());
|
||||
source.setContractFormat(request.getContractFormat());
|
||||
@@ -665,9 +679,12 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
} else if (!Objects.equals(contractManage.getAllDept(), 1)) {
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("合同管理");
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
queryWrapper.and(wrapper -> wrapper.eq(ContractManage::getOrganizationId, currentDeptId)
|
||||
.or().eq(ContractManage::getCreateUser, currentUserId)
|
||||
.or().eq(ContractManage::getHandlerUserId, currentUserId));
|
||||
queryWrapper.and(wrapper -> {
|
||||
wrapper.eq(ContractManage::getOrganizationId, currentDeptId)
|
||||
.or().eq(ContractManage::getCreateUser, currentUserId)
|
||||
.or().eq(ContractManage::getHandlerUserId, currentUserId);
|
||||
TransportBusinessSupport.orProjectVisibleByBusinessDept(wrapper, List.of(currentDeptId));
|
||||
});
|
||||
} else if (Func.isNotEmpty(contractManage.getOrganizationId())) {
|
||||
queryWrapper.eq(ContractManage::getOrganizationId, contractManage.getOrganizationId());
|
||||
}
|
||||
@@ -683,6 +700,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
.eq(Func.isNotEmpty(contractManage.getArchiveStatus()), ContractManage::getArchiveStatus, contractManage.getArchiveStatus())
|
||||
.eq(Func.isNotEmpty(contractManage.getContractStage()), ContractManage::getContractStage, contractManage.getContractStage())
|
||||
.orderByDesc(ContractManage::getCreateTime);
|
||||
applyPartyName(queryWrapper, contractManage.getPartyName());
|
||||
applyEffectiveType(queryWrapper, contractManage.getEffectiveType());
|
||||
applyExpireScope(queryWrapper, contractManage.getExpireScope());
|
||||
// 是否同步金蝶(kingdee_id 是否已回填)/ 是否同步合规(hcs_sync_status 是否 synced)
|
||||
@@ -708,6 +726,18 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 客商筛选:匹配合同甲/乙/丙方名称(模糊)
|
||||
*/
|
||||
private void applyPartyName(LambdaQueryWrapper<ContractManage> queryWrapper, String partyName) {
|
||||
if (Func.isEmpty(partyName)) {
|
||||
return;
|
||||
}
|
||||
queryWrapper.and(wrapper -> wrapper.like(ContractManage::getPartyA, partyName)
|
||||
.or().like(ContractManage::getPartyB, partyName)
|
||||
.or().like(ContractManage::getPartyC, partyName));
|
||||
}
|
||||
|
||||
private void applyEffectiveType(LambdaQueryWrapper<ContractManage> queryWrapper, String effectiveType) {
|
||||
if (Func.isEmpty(effectiveType)) {
|
||||
return;
|
||||
@@ -737,7 +767,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
.lt(ContractManage::getTemporaryEndDate, today)
|
||||
.or(wrapper2 -> wrapper2.ne(ContractManage::getContractStage, STAGE_TEMPORARY)
|
||||
.lt(ContractManage::getEndDate, today)));
|
||||
default -> throw new ServiceException("无效的生效类型筛选条件");
|
||||
default -> throw new ServiceException("无效的合同状态筛选条件");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -774,6 +804,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
contractManage.setOrganizationId(oldRecord.getOrganizationId());
|
||||
contractManage.setOrganizationName(oldRecord.getOrganizationName());
|
||||
}
|
||||
if (Func.isEmpty(contractManage.getPlatformCompanyId())) {
|
||||
contractManage.setPlatformCompanyId(oldRecord.getPlatformCompanyId());
|
||||
contractManage.setPlatformCompanyName(oldRecord.getPlatformCompanyName());
|
||||
}
|
||||
contractManage.setHandlerUserId(oldRecord.getHandlerUserId());
|
||||
contractManage.setHandlerUserName(oldRecord.getHandlerUserName());
|
||||
// 归档状态仅由审核通过逻辑自动维护,保存时保留库中已有值。
|
||||
@@ -798,7 +832,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
if (!generateWhenEmpty) {
|
||||
throw new ServiceException("请输入合同编号");
|
||||
}
|
||||
normalizedContractNo = nextContractNo(contractManage.getOrganizationId());
|
||||
normalizedContractNo = nextContractNo(contractManage.getOrganizationId(), contractManage.getPlatformCompanyId());
|
||||
}
|
||||
TransportBusinessSupport.validateLength(normalizedContractNo, 100, "合同编号不能超过100字");
|
||||
String currentContractNo = TransportBusinessSupport.trimToNull(contractManage.getContractNo());
|
||||
@@ -857,30 +891,39 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String nextContractNo(Long organizationId) {
|
||||
public synchronized String nextContractNo(Long organizationId, Long platformCompanyId) {
|
||||
if (Func.isEmpty(organizationId)) {
|
||||
throw new ServiceException("请选择合同负责部门");
|
||||
}
|
||||
if (Func.isEmpty(platformCompanyId)) {
|
||||
throw new ServiceException("请选择平台公司");
|
||||
}
|
||||
Dept dept = SysCache.getDept(organizationId);
|
||||
if (dept == null) {
|
||||
throw new ServiceException("合同负责部门不存在");
|
||||
}
|
||||
String prefix = TransportBusinessSupport.trimToNull(dept.getContractCodePrefix());
|
||||
Dept platformCompany = SysCache.getDept(platformCompanyId);
|
||||
if (platformCompany == null) {
|
||||
throw new ServiceException("平台公司不存在");
|
||||
}
|
||||
BusinessDeptConfig config = SysCache.getBusinessDeptConfig(platformCompanyId, organizationId);
|
||||
String prefix = config == null ? null : TransportBusinessSupport.trimToNull(config.getContractCodePrefix());
|
||||
if (Func.isEmpty(prefix)) {
|
||||
throw new ServiceException("请先在组织机构中配置该部门的合同编码前缀");
|
||||
throw new ServiceException("部门合同前缀未配置,无法自动生成,请手动输入合同编号");
|
||||
}
|
||||
int year = LocalDate.now().getYear();
|
||||
String head = prefix + "(" + year + ")";
|
||||
// 按前缀+年份扫描已占用流水号;系统已有相同流水号则跳过
|
||||
List<ContractManage> latestList = list(Wrappers.<ContractManage>lambdaQuery()
|
||||
.select(ContractManage::getContractNo)
|
||||
.eq(ContractManage::getIsDeleted, 0)
|
||||
.eq(ContractManage::getOrganizationId, organizationId)
|
||||
.likeRight(ContractManage::getContractNo, head)
|
||||
.orderByDesc(ContractManage::getContractNo)
|
||||
.last("LIMIT 50"));
|
||||
.last("LIMIT 200"));
|
||||
Pattern pattern = Pattern.compile("^" + Pattern.quote(prefix) + "(" + year + ")(\\d{3})号$");
|
||||
// 当年尚无匹配编号时,从组织配置的初始流水号起编;否则在已有最大流水号上加1
|
||||
int next = resolveContractInitialSerial(dept);
|
||||
int initialSerial = resolveContractInitialSerial(config);
|
||||
int maxUsed = 0;
|
||||
boolean hasMatched = false;
|
||||
for (ContractManage item : latestList) {
|
||||
String contractNo = TransportBusinessSupport.trimToNull(item.getContractNo());
|
||||
if (Func.isEmpty(contractNo)) {
|
||||
@@ -890,22 +933,24 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
}
|
||||
next = Integer.parseInt(matcher.group(1)) + 1;
|
||||
break;
|
||||
hasMatched = true;
|
||||
maxUsed = Math.max(maxUsed, Integer.parseInt(matcher.group(1)));
|
||||
}
|
||||
// 从初始流水号起编;若系统已有相同或更大流水号则跳到 maxUsed+1
|
||||
int next = hasMatched ? Math.max(initialSerial, maxUsed + 1) : initialSerial;
|
||||
if (next > 999) {
|
||||
throw new ServiceException("当前部门本年合同流水号已达上限999");
|
||||
}
|
||||
return head + String.format("%03d", next) + "号";
|
||||
}
|
||||
|
||||
private int resolveContractInitialSerial(Dept dept) {
|
||||
Integer serial = dept.getContractInitialSerial();
|
||||
private int resolveContractInitialSerial(BusinessDeptConfig config) {
|
||||
Integer serial = config == null ? null : config.getInitialSerial();
|
||||
if (serial == null || serial < 1) {
|
||||
return 1;
|
||||
}
|
||||
if (serial > 999) {
|
||||
throw new ServiceException("组织机构中配置的合同初始流水号不能超过999");
|
||||
throw new ServiceException("业务部门配置的合同初始流水号不能超过999");
|
||||
}
|
||||
return serial;
|
||||
}
|
||||
@@ -1022,8 +1067,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
contractManage.setOrganizationName(TransportBusinessSupport.trimToNull(contractManage.getOrganizationName()));
|
||||
contractManage.setContractCategory(TransportBusinessSupport.trimToNull(contractManage.getContractCategory()));
|
||||
contractManage.setSignType(TransportBusinessSupport.trimToNull(contractManage.getSignType()));
|
||||
contractManage.setPartyA(TransportBusinessSupport.trimToNull(contractManage.getPartyA()));
|
||||
contractManage.setPartyB(TransportBusinessSupport.trimToNull(contractManage.getPartyB()));
|
||||
contractManage.setPartyAJson(TransportBusinessSupport.trimToNull(contractManage.getPartyAJson()));
|
||||
contractManage.setPartyBJson(TransportBusinessSupport.trimToNull(contractManage.getPartyBJson()));
|
||||
contractManage.setPartyCJson(TransportBusinessSupport.trimToNull(contractManage.getPartyCJson()));
|
||||
ContractPartySupport.normalizeAndFillSummary(contractManage);
|
||||
contractManage.setSettlementMode(TransportBusinessSupport.trimToNull(contractManage.getSettlementMode()));
|
||||
contractManage.setContractFormat(TransportBusinessSupport.trimToNull(contractManage.getContractFormat()));
|
||||
contractManage.setRemark(TransportBusinessSupport.trimToNull(contractManage.getRemark()));
|
||||
@@ -1080,6 +1127,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
// 合同名称允许重复,唯一性由合同编号保证。
|
||||
TransportBusinessSupport.validateLength(contractManage.getMainContent(), 1000, "合同主要内容不能超过1000字");
|
||||
TransportBusinessSupport.validateLength(contractManage.getRemark(), 2000, "其他事项说明不能超过2000字");
|
||||
// 草稿不强制甲/乙客商与计费绑定,提交临时/正式时再完整校验
|
||||
}
|
||||
|
||||
private void validateBillingPlans(String billingPlanJson) {
|
||||
@@ -1091,18 +1139,11 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
if (!(parsed instanceof List<?> plans)) {
|
||||
throw new ServiceException("计费方案设置格式不正确");
|
||||
}
|
||||
Map<String, Integer> defaultPlanCount = new HashMap<>();
|
||||
for (Object value : plans) {
|
||||
if (!(value instanceof Map<?, ?> plan)) {
|
||||
throw new ServiceException("计费方案设置格式不正确");
|
||||
}
|
||||
String transportMode = trimValue(plan.get("transportMode"));
|
||||
if (isDefaultBillingPlan(plan)) {
|
||||
int count = defaultPlanCount.merge(transportMode, 1, Integer::sum);
|
||||
if (count > 1) {
|
||||
throw new ServiceException("同一运输方式仅支持配置一个默认计费方案");
|
||||
}
|
||||
}
|
||||
// 一个客商对应一个方案,不再校验「默认方案」
|
||||
validateBillingMatchConditions(plan);
|
||||
validateBillingRuleTaxRates(plan);
|
||||
}
|
||||
@@ -1164,15 +1205,6 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
return value != null && !String.valueOf(value).trim().isEmpty();
|
||||
}
|
||||
|
||||
private boolean isDefaultBillingPlan(Map<?, ?> plan) {
|
||||
Object value = plan.get("defaultPlan");
|
||||
return Boolean.TRUE.equals(value) || "true".equalsIgnoreCase(String.valueOf(value)) || "1".equals(String.valueOf(value));
|
||||
}
|
||||
|
||||
private String trimValue(Object value) {
|
||||
return value == null ? null : TransportBusinessSupport.trimToNull(String.valueOf(value));
|
||||
}
|
||||
|
||||
private void validatePaymentRatios(String paymentRatioJson) {
|
||||
if (Func.isEmpty(paymentRatioJson)) return;
|
||||
try {
|
||||
@@ -1221,8 +1253,9 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
TransportBusinessSupport.validateRequired(contractManage.getContractCategory(), "请选择合同类型");
|
||||
TransportBusinessSupport.validateRequired(contractManage.getProjectName(), "请选择所属项目");
|
||||
TransportBusinessSupport.validateRequired(contractManage.getOrganizationName(), "请选择合同负责部门");
|
||||
TransportBusinessSupport.validateRequired(contractManage.getPartyA(), "请选择甲方");
|
||||
TransportBusinessSupport.validateRequired(contractManage.getPartyB(), "请选择乙方");
|
||||
TransportBusinessSupport.validateRequired(contractManage.getPlatformCompanyName(), "请选择平台公司");
|
||||
ContractPartySupport.validateRequiredParties(contractManage, true);
|
||||
ContractPartySupport.validateBillingPlanBindings(contractManage);
|
||||
validateContractAmountRequired(contractManage);
|
||||
}
|
||||
|
||||
@@ -1255,6 +1288,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
TransportBusinessSupport.validateRequired(contractManage.getContractCategory(), "请选择合同类型");
|
||||
TransportBusinessSupport.validateRequired(contractManage.getProjectName(), "请选择所属项目");
|
||||
TransportBusinessSupport.validateRequired(contractManage.getOrganizationName(), "请选择合同负责部门");
|
||||
TransportBusinessSupport.validateRequired(contractManage.getPlatformCompanyName(), "请选择平台公司");
|
||||
TransportBusinessSupport.validateRequired(contractManage.getSettlementCurrency(), "请选择结算币种");
|
||||
TransportBusinessSupport.validateRequired(contractManage.getPartyA(), "请输入甲方");
|
||||
TransportBusinessSupport.validateRequired(contractManage.getPartyB(), "请输入乙方");
|
||||
@@ -1422,7 +1456,12 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("contractNo", contractManage.getContractNo());
|
||||
data.put("contractName", contractManage.getContractName());
|
||||
data.put("partyA", contractManage.getPartyA());
|
||||
data.put("partyB", contractManage.getPartyB());
|
||||
data.put("partyC", contractManage.getPartyC());
|
||||
data.put("partyAJson", contractManage.getPartyAJson());
|
||||
data.put("partyBJson", contractManage.getPartyBJson());
|
||||
data.put("partyCJson", contractManage.getPartyCJson());
|
||||
data.put("startDate", contractManage.getStartDate());
|
||||
data.put("endDate", contractManage.getEndDate());
|
||||
data.put("contractFormat", contractManage.getContractFormat());
|
||||
@@ -1601,7 +1640,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
return Objects.equals(contractManage.getOrganizationId(), currentDeptId)
|
||||
|| Objects.equals(contractManage.getCreateUser(), currentUserId)
|
||||
|| Objects.equals(contractManage.getHandlerUserId(), currentUserId);
|
||||
|| Objects.equals(contractManage.getHandlerUserId(), currentUserId)
|
||||
|| TransportBusinessSupport.isProjectOwnedByBusinessDept(contractManage.getProjectId(), currentDeptId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-72
@@ -27,10 +27,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.system.pojo.entity.MpUser;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.entity.TransportVehicle;
|
||||
import org.springblade.transport.pojo.vo.DriverVehicleCardVO;
|
||||
@@ -55,19 +52,24 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class DriverAppServiceImpl implements IDriverAppService {
|
||||
|
||||
/**
|
||||
* 启用状态。框架基类只有 status 列与 BladeConstant.DB_STATUS_NORMAL = 1,没有枚举;
|
||||
* 2 是「停用」,业务侧一律写字面量常量。
|
||||
*/
|
||||
private static final int ENABLED_STATUS = 1;
|
||||
|
||||
private final IDriverService driverService;
|
||||
private final ITransportVehicleService transportVehicleService;
|
||||
private final IMpUserClient mpUserClient;
|
||||
|
||||
@Override
|
||||
public DriverVO currentByPhone(String mobile) {
|
||||
Driver driver = currentDriver(mobile);
|
||||
public DriverVO current() {
|
||||
Driver driver = currentDriver();
|
||||
return driver == null ? null : DriverWrapper.build().entityVO(driver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DriverVehicleCardVO> myVehicles() {
|
||||
Driver driver = currentDriver(null);
|
||||
Driver driver = currentDriver();
|
||||
if (driver == null || Func.isEmpty(driver.getDrivingVehicle())) {
|
||||
return List.of();
|
||||
}
|
||||
@@ -78,6 +80,8 @@ public class DriverAppServiceImpl implements IDriverAppService {
|
||||
// 精确 + 规范化双条件匹配,统一走共享实现(避免各调用点各自实现而漏掉写法差异)
|
||||
LambdaQueryWrapper<TransportVehicle> plateQuery = Wrappers.lambdaQuery();
|
||||
VehiclePlateSupport.applyPlateMatch(plateQuery, plates);
|
||||
// 停用车辆不再出现在司机端:管理员停用即意味着这辆车不该再被司机拿去接单
|
||||
plateQuery.eq(TransportVehicle::getStatus, ENABLED_STATUS);
|
||||
List<TransportVehicle> vehicles = transportVehicleService.list(
|
||||
plateQuery.orderByDesc(TransportVehicle::getUpdateTime));
|
||||
// 去重(精确与规范化可能命中同一条)
|
||||
@@ -92,28 +96,23 @@ public class DriverAppServiceImpl implements IDriverAppService {
|
||||
|
||||
@Override
|
||||
public Driver currentDriverEntity() {
|
||||
return currentDriver(null);
|
||||
return currentDriver();
|
||||
}
|
||||
|
||||
private Driver currentDriver(String mobileHint) {
|
||||
/**
|
||||
* 当前司机的唯一解析入口:只按登录用户的 {@code user_id} 查档案。
|
||||
* <p>
|
||||
* 手机号不能用来认人:同一号码可能存在多条档案,按号码查「最近更新的一条」
|
||||
* 取到的可能是别人的档案;号码本身也会被改。
|
||||
*/
|
||||
private Driver currentDriver() {
|
||||
Long userId = AuthUtil.getUserId();
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new ServiceException("未登录");
|
||||
}
|
||||
String phone = resolvePhone(userId, mobileHint);
|
||||
Driver driver = null;
|
||||
if (Func.isNotEmpty(phone)) {
|
||||
driver = driverService.getOne(Wrappers.<Driver>lambdaQuery()
|
||||
.eq(Driver::getMobile, phone)
|
||||
.orderByDesc(Driver::getUpdateTime)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
if (driver == null) {
|
||||
driver = driverService.getOne(Wrappers.<Driver>lambdaQuery()
|
||||
.eq(Driver::getUserId, userId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
return driver;
|
||||
return driverService.getOne(Wrappers.<Driver>lambdaQuery()
|
||||
.eq(Driver::getUserId, userId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private DriverVehicleCardVO toCard(TransportVehicle vehicle) {
|
||||
@@ -178,53 +177,4 @@ public class DriverAppServiceImpl implements IDriverAppService {
|
||||
return date == null ? "" : date.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析用于匹配司机档案的手机号。
|
||||
* 优先级:前端传入且与本人一致的 mobile → JWT account(司机账号多为手机号)→ 用户中心 phone/account
|
||||
*/
|
||||
private String resolvePhone(Long userId, String mobileHint) {
|
||||
String selfPhone = resolveSelfPhone(userId);
|
||||
String hint = Func.isEmpty(mobileHint) ? null : mobileHint.trim();
|
||||
if (Func.isNotEmpty(hint)) {
|
||||
if (Func.isEmpty(selfPhone)) {
|
||||
// 解析不出本人号码时不能拿前端传的 mobile 去查:
|
||||
// currentDriver 会直接按 mobile 等值查询,传入他人号码即可读到他人档案。
|
||||
// 这里返回空让它退回按 user_id 查,宁可查不到也不越权。
|
||||
return null;
|
||||
}
|
||||
if (!selfPhone.equals(hint)) {
|
||||
throw new ServiceException("只能查询本人司机档案");
|
||||
}
|
||||
return hint;
|
||||
}
|
||||
return selfPhone;
|
||||
}
|
||||
|
||||
private String resolveSelfPhone(Long userId) {
|
||||
String account = AuthUtil.getUserAccount();
|
||||
if (isMobileLike(account)) {
|
||||
return account.trim();
|
||||
}
|
||||
// 小程序用户 phone 带 @Sensitive(MOBILE),经 Feign 回来是脱敏串,
|
||||
// 因此这里只取未脱敏的 account;脱敏的 phone 一律不用。
|
||||
R<MpUser> result = mpUserClient.userById(userId);
|
||||
if (result != null && R.isSuccess(result) && result.getData() != null) {
|
||||
String userAccount = result.getData().getAccount();
|
||||
if (isMobileLike(userAccount)) {
|
||||
return userAccount.trim();
|
||||
}
|
||||
}
|
||||
return Func.isEmpty(account) ? null : account.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是「可作为司机档案手机号使用」的号码串。
|
||||
* <p>
|
||||
* 只判 1 开头的 11~15 位纯数字:库里既有 11 位真实手机号,也有 12 位账号
|
||||
* (如 175874212082),写死 11 位会让这类账号的司机解析不出本人号码。
|
||||
*/
|
||||
private boolean isMobileLike(String value) {
|
||||
return Func.isNotEmpty(value) && value.trim().matches("^1\\d{10,14}$");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-2
@@ -209,9 +209,11 @@ public class DriverCertServiceImpl implements IDriverCertService {
|
||||
Driver driver = requireCurrentDriver();
|
||||
// 当前司机实体已加载过,直接读它身上的指针,不再回查一次
|
||||
TransportCertRecord effective = findEffectiveRecord(driver.getEffectiveCertRecordId());
|
||||
// 生效内容以修订版为准:审核员改过的值才是档案里真正生效的那份,
|
||||
// 仍读 snapshot_json 会让司机端拿旧值覆盖审核员的改动
|
||||
return effective == null
|
||||
? null
|
||||
: JSON.parseObject(effective.getSnapshotJson(), PersonCertSubmitRequest.class);
|
||||
: JSON.parseObject(CertificationRuleSupport.effectiveSnapshotJson(effective), PersonCertSubmitRequest.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -269,10 +271,14 @@ public class DriverCertServiceImpl implements IDriverCertService {
|
||||
if (!VehiclePlateSupport.isBound(requireCurrentDriver().getDrivingVehicle(), vehicle.getPlateNo())) {
|
||||
throw new ServiceException("无权查看该车辆的认证信息");
|
||||
}
|
||||
// 停用车辆不再对司机端开放:司机列表里已经不显示它,详情/认证入口也不该能进
|
||||
if (Func.isNotEmpty(vehicle.getStatus()) && vehicle.getStatus() != 1) {
|
||||
throw new ServiceException("该车辆已停用,暂不可查看,请联系调度启用");
|
||||
}
|
||||
TransportCertRecord effective = findEffectiveRecord(vehicle.getEffectiveCertRecordId());
|
||||
return effective == null
|
||||
? null
|
||||
: JSON.parseObject(effective.getSnapshotJson(), VehicleCertSubmitRequest.class);
|
||||
: JSON.parseObject(CertificationRuleSupport.effectiveSnapshotJson(effective), VehicleCertSubmitRequest.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -406,6 +412,7 @@ public class DriverCertServiceImpl implements IDriverCertService {
|
||||
if (!VehiclePlateSupport.isBound(driver.getDrivingVehicle(), vehicle.getPlateNo())) {
|
||||
throw new ServiceException("无权操作该车辆的认证");
|
||||
}
|
||||
requireEnabledVehicle(vehicle);
|
||||
return vehicle;
|
||||
}
|
||||
|
||||
@@ -426,11 +433,24 @@ public class DriverCertServiceImpl implements IDriverCertService {
|
||||
// 已登记但不在本人名下,可能属于其他司机 —— 拒绝,不能顺手改归属
|
||||
throw new ServiceException("该车牌已归属其他司机,请联系管理员");
|
||||
}
|
||||
requireEnabledVehicle(existing);
|
||||
return existing;
|
||||
}
|
||||
return createVehicleForDriver(driver, request, normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用车辆不接受司机的认证提交
|
||||
* <p>
|
||||
* 停用是调度端的管理动作(车辆停驶、报废待办),司机端列表已不再显示停用车;
|
||||
* 若这里放行,司机仍能凭车牌把停用车提交认证,等于绕过停用。
|
||||
*/
|
||||
private void requireEnabledVehicle(TransportVehicle vehicle) {
|
||||
if (vehicle != null && Func.isNotEmpty(vehicle.getStatus()) && vehicle.getStatus() != 1) {
|
||||
throw new ServiceException("该车辆已停用,请联系调度启用后再认证");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按车牌(容忍写法差异)查已登记的车辆
|
||||
*
|
||||
|
||||
+29
@@ -25,6 +25,7 @@
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import cn.hutool.core.util.DesensitizedUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
@@ -171,6 +172,31 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
|
||||
return bindExistingProfile(existing.getId(), mpUserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long resolveDriverIdByMobile(String tenantId, String mobile) {
|
||||
String resolvedTenantId = trimToNull(tenantId);
|
||||
if (resolvedTenantId == null) {
|
||||
resolvedTenantId = trimToNull(AuthUtil.getTenantId());
|
||||
}
|
||||
String mobileSnapshot = trimToNull(mobile);
|
||||
// 脱敏手机号(如 138****8000)与空号都不是可用业务数据,查不到也不该查
|
||||
if (resolvedTenantId == null || mobileSnapshot == null || mobileSnapshot.contains("*")) {
|
||||
return null;
|
||||
}
|
||||
// 手机号会被二次放号,同租户内可能残留多条历史档案:只有唯一命中才认,不猜
|
||||
List<Driver> matchedDrivers = list(Wrappers.<Driver>lambdaQuery()
|
||||
.select(Driver::getId)
|
||||
.eq(Driver::getTenantId, resolvedTenantId)
|
||||
.eq(Driver::getMobile, mobileSnapshot));
|
||||
if (matchedDrivers.size() != 1) {
|
||||
log.warn("司机档案解析未唯一命中,driver_id 留空。租户={} 手机号={} 命中={} 条",
|
||||
resolvedTenantId, DesensitizedUtil.mobilePhone(mobileSnapshot), matchedDrivers.size());
|
||||
return null;
|
||||
}
|
||||
Driver matchedDriver = matchedDrivers.get(0);
|
||||
return matchedDriver == null ? null : matchedDriver.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public DriverSyncMobileVO syncMobile(String tenantId, Long mpUserId, String oldPhone, String newPhone) {
|
||||
@@ -392,6 +418,9 @@ public class DriverServiceImpl extends BaseServiceImpl<DriverMapper, Driver> imp
|
||||
driver.setDrivingLicenseBack(trimToNull(driver.getDrivingLicenseBack()));
|
||||
driver.setQualificationFront(trimToNull(driver.getQualificationFront()));
|
||||
driver.setQualificationBack(trimToNull(driver.getQualificationBack()));
|
||||
if (driver.getIdCardLongTerm() == null) {
|
||||
driver.setIdCardLongTerm(DEFAULT_FALSE);
|
||||
}
|
||||
if (driver.getDrivingLicenseLongTerm() == null) {
|
||||
driver.setDrivingLicenseLongTerm(DEFAULT_FALSE);
|
||||
}
|
||||
|
||||
+43
-102
@@ -33,10 +33,7 @@ import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.constant.BladeConstant;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.system.pojo.entity.MpUser;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -154,7 +151,6 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
private final IWaybillService waybillService;
|
||||
private final IDriverService driverService;
|
||||
private final IMpUserClient mpUserClient;
|
||||
private final IProcessConfigService processConfigService;
|
||||
private final ITransportVehicleService transportVehicleService;
|
||||
private final CertificationGate certificationGate;
|
||||
@@ -166,12 +162,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO currentTask() {
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
Driver driver = currentDriver();
|
||||
if (driver == null) {
|
||||
return null;
|
||||
}
|
||||
// 进行中:running,或无需确认接单但仍为 pending 的历史数据
|
||||
List<Waybill> candidates = waybillService.list(scopedQuery(phone)
|
||||
List<Waybill> candidates = waybillService.list(scopedQuery(driver.getId())
|
||||
.in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING))
|
||||
.orderByDesc(Waybill::getUpdateTime)
|
||||
.last("LIMIT 20"));
|
||||
@@ -187,12 +183,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
@Override
|
||||
public DriverWaybillPreviewVO pendingPreview(Integer size) {
|
||||
DriverWaybillPreviewVO preview = new DriverWaybillPreviewVO();
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
Driver driver = currentDriver();
|
||||
if (driver == null) {
|
||||
return preview;
|
||||
}
|
||||
int limit = normalizePreviewSize(size);
|
||||
List<Waybill> pendingList = waybillService.list(scopedQuery(phone)
|
||||
List<Waybill> pendingList = waybillService.list(scopedQuery(driver.getId())
|
||||
.in(Waybill::getBusinessStatus, PENDING_STATUSES)
|
||||
.orderByDesc(Waybill::getCreateTime));
|
||||
List<Waybill> needAccept = pendingList.stream()
|
||||
@@ -207,11 +203,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
@Override
|
||||
public DriverWaybillTabCountsVO tabCounts() {
|
||||
DriverWaybillTabCountsVO vo = new DriverWaybillTabCountsVO();
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
Driver driver = currentDriver();
|
||||
if (driver == null) {
|
||||
return vo;
|
||||
}
|
||||
List<Waybill> waybills = waybillService.list(scopedQuery(phone)
|
||||
List<Waybill> waybills = waybillService.list(scopedQuery(driver.getId())
|
||||
.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES));
|
||||
long pending = 0;
|
||||
long doing = 0;
|
||||
@@ -243,12 +239,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE);
|
||||
Page<DriverWaybillCardVO> empty = new Page<>(pageNo, pageSize);
|
||||
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
Driver driver = currentDriver();
|
||||
if (driver == null) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<Waybill> wrapper = scopedQuery(phone);
|
||||
LambdaQueryWrapper<Waybill> wrapper = scopedQuery(driver.getId());
|
||||
// 先按 Tab 口径拉候选,再按过程配置校正 pending→running 后内存分页
|
||||
applyStatusFilterForQuery(wrapper, status);
|
||||
applyKeyword(wrapper, keyword);
|
||||
@@ -271,18 +267,17 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO detail(Long id) {
|
||||
requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
return toCard(normalizeAcceptStatus(waybill), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO tripCurrentTask() {
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
Driver driver = currentDriver();
|
||||
if (driver == null) {
|
||||
return null;
|
||||
}
|
||||
TripQueryVO query = TripQueryVO.forDriver(phone);
|
||||
TripQueryVO query = TripQueryVO.forDriver(driver.getId());
|
||||
// 待确认(待回单确认)的趟车司机仍需回到卡片上继续操作,与进行中并列作为当前任务
|
||||
query.setStatuses(List.of(TRIP_STATUS_RUNNING, TRIP_STATUS_PENDING_RETURN_CONFIRM));
|
||||
query.setCurrent(1);
|
||||
@@ -294,11 +289,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
@Override
|
||||
public DriverWaybillPreviewVO tripPendingPreview(Integer size) {
|
||||
DriverWaybillPreviewVO preview = new DriverWaybillPreviewVO();
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
Driver driver = currentDriver();
|
||||
if (driver == null) {
|
||||
return preview;
|
||||
}
|
||||
TripQueryVO query = TripQueryVO.forDriver(phone);
|
||||
TripQueryVO query = TripQueryVO.forDriver(driver.getId());
|
||||
query.setStatus(TRIP_STATUS_PENDING);
|
||||
query.setCurrent(1);
|
||||
query.setSize(normalizePreviewSize(size));
|
||||
@@ -313,11 +308,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
@Override
|
||||
public DriverWaybillTabCountsVO tripTabCounts() {
|
||||
DriverWaybillTabCountsVO counts = new DriverWaybillTabCountsVO();
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
Driver driver = currentDriver();
|
||||
if (driver == null) {
|
||||
return counts;
|
||||
}
|
||||
TripTabCountsVO tripCounts = tripQueryService.tabCounts(TripQueryVO.forDriver(phone));
|
||||
TripTabCountsVO tripCounts = tripQueryService.tabCounts(TripQueryVO.forDriver(driver.getId()));
|
||||
counts.setPending(tripCounts.getPending());
|
||||
counts.setDoing(tripCounts.getDoing());
|
||||
counts.setConfirm(tripCounts.getConfirm());
|
||||
@@ -328,11 +323,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
@Override
|
||||
public IPage<DriverWaybillCardVO> tripPage(Integer current, Integer size, Integer status, String keyword) {
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
Driver driver = currentDriver();
|
||||
if (driver == null) {
|
||||
return new Page<>(normalizePageNo(current), normalizePageSize(size));
|
||||
}
|
||||
TripQueryVO query = TripQueryVO.forDriver(phone);
|
||||
TripQueryVO query = TripQueryVO.forDriver(driver.getId());
|
||||
query.setStatus(status);
|
||||
query.setKeyword(keyword);
|
||||
query.setCurrent(current);
|
||||
@@ -347,15 +342,14 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO tripDetail(String tripId) {
|
||||
requireCurrentDriver();
|
||||
TripQueryVO query = TripQueryVO.forDriver(currentUserPhone());
|
||||
Driver driver = requireCurrentDriver();
|
||||
TripQueryVO query = TripQueryVO.forDriver(driver.getId());
|
||||
query.setTripId(tripId);
|
||||
return toTripCard(tripQueryService.detail(query), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WaybillTrackVO track(Long id) {
|
||||
requireCurrentDriver();
|
||||
Waybill waybill = loadDriverWaybill(id);
|
||||
LocalDate[] range = resolveTrackDateRange(waybill);
|
||||
return waybillService.trackVehicle(waybill.getId(), range[0].toString(), range[1].toString());
|
||||
@@ -742,7 +736,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
* 子单集合是司机可见的那一批:已拒单的子单不在其中,否则接单会把本人拒掉的单重新拉回来。
|
||||
*/
|
||||
private List<TripVO.TripWaybillVO> tripChildren(String tripId) {
|
||||
TripQueryVO query = TripQueryVO.forDriver(currentUserPhone());
|
||||
TripQueryVO query = TripQueryVO.forDriver(requireCurrentDriver().getId());
|
||||
query.setTripId(tripId);
|
||||
TripVO trip = tripQueryService.detail(query);
|
||||
List<TripVO.TripWaybillVO> children = trip.getWaybills() == null ? List.of() : trip.getWaybills();
|
||||
@@ -822,13 +816,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
if (id == null) {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
String phone = currentUserPhone();
|
||||
if (Func.isEmpty(phone)) {
|
||||
throw new ServiceException("当前用户手机号为空,无法查找运单");
|
||||
}
|
||||
Driver driver = requireCurrentDriver();
|
||||
// MySQL 的 FOR UPDATE 必须跟在 LIMIT 之后
|
||||
String tail = forUpdate ? "LIMIT 1 FOR UPDATE" : "LIMIT 1";
|
||||
Waybill waybill = waybillService.getOne(scopedQuery(phone, includeRejected)
|
||||
Waybill waybill = waybillService.getOne(scopedQuery(driver.getId(), includeRejected)
|
||||
.eq(Waybill::getId, id).last(tail));
|
||||
if (waybill == null) {
|
||||
throw new ServiceException("运单不存在或无权操作");
|
||||
@@ -885,14 +876,17 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 司机可见运单范围:运单司机手机号(driver_phone)等于当前用户手机号,
|
||||
* 司机可见运单范围:运单归属档案(driver_id)就是当前司机档案,
|
||||
* 且该运单未被这个司机拒单。
|
||||
* <p>
|
||||
* driver_id 必须有值且大于 0 才算有主:存量数据的 -1 是哨兵值,与 0 一样等同无主。
|
||||
* 运单上的 driver_phone 只是派单快照,不参与归属判定——司机改号不影响可见性。
|
||||
* <p>
|
||||
* 拒单后运单对司机彻底脱离——列表、计数、详情、打卡、接单一律不可达,等管理员重新派单。
|
||||
* 运单上的司机姓名/手机/车牌不清空,调度重新派单时要知道原来派给谁。
|
||||
*/
|
||||
private LambdaQueryWrapper<Waybill> scopedQuery(String phone) {
|
||||
return scopedQuery(phone, false);
|
||||
private LambdaQueryWrapper<Waybill> scopedQuery(Long driverId) {
|
||||
return scopedQuery(driverId, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -900,11 +894,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
* 拒单的运单不可能处于运输中,收口拦不到合法操作,
|
||||
* 而保留原判据可避免「已拒单 + 进行中」的历史脏数据被误伤
|
||||
*/
|
||||
private LambdaQueryWrapper<Waybill> scopedQuery(String phone, boolean includeRejected) {
|
||||
String normalizedPhone = Func.toStr(phone, "").trim();
|
||||
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery().and(w -> w
|
||||
.eq(Waybill::getDriverPhone, normalizedPhone)
|
||||
.or().apply("REPLACE(IFNULL(driver_phone,''),' ','') = {0}", normalizedPhone));
|
||||
private LambdaQueryWrapper<Waybill> scopedQuery(Long driverId, boolean includeRejected) {
|
||||
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery()
|
||||
.gt(Waybill::getDriverId, 0)
|
||||
.eq(Waybill::getDriverId, driverId);
|
||||
if (!includeRejected) {
|
||||
wrapper.and(w -> w.isNull(Waybill::getDriverAcceptStatus)
|
||||
.or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED));
|
||||
@@ -926,69 +919,18 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户手机号:司机档案 mobile 优先,其次小程序用户账号。
|
||||
* 当前登录司机:只按登录用户 ID 查司机档案,查不到即「无归属」。
|
||||
* <p>
|
||||
* 派单时运单 driver_phone 取自司机档案 mobile,故档案有手机号就以它为准。
|
||||
*/
|
||||
private String currentUserPhone() {
|
||||
Driver driver = currentDriver();
|
||||
if (driver != null && isMobileLike(driver.getMobile())) {
|
||||
return driver.getMobile().trim();
|
||||
}
|
||||
Long userId = AuthUtil.getUserId();
|
||||
if (userId == null || userId <= 0) {
|
||||
return null;
|
||||
}
|
||||
R<MpUser> result = mpUserClient.userById(userId);
|
||||
if (result != null && R.isSuccess(result) && result.getData() != null
|
||||
&& isMobileLike(result.getData().getAccount())) {
|
||||
return result.getData().getAccount().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录司机:优先 userId,其次手机号(JWT 账号 / 小程序用户 account)匹配 driver.mobile
|
||||
* 归属判据:driver_id = 当前登录人的司机档案 id;driver_phone 不参与判定。
|
||||
*/
|
||||
private Driver currentDriver() {
|
||||
Long userId = AuthUtil.getUserId();
|
||||
if (userId == null || userId <= 0) {
|
||||
return null;
|
||||
}
|
||||
Driver driver = driverService.getOne(Wrappers.<Driver>lambdaQuery()
|
||||
return driverService.getOne(Wrappers.<Driver>lambdaQuery()
|
||||
.eq(Driver::getUserId, userId)
|
||||
.last("LIMIT 1"));
|
||||
if (driver != null) {
|
||||
return driver;
|
||||
}
|
||||
String phone = resolveSelfPhone(userId);
|
||||
if (Func.isNotEmpty(phone)) {
|
||||
return driverService.getOne(Wrappers.<Driver>lambdaQuery()
|
||||
.eq(Driver::getMobile, phone)
|
||||
.orderByDesc(Driver::getUpdateTime)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析当前小程序用户可用于匹配司机档案的手机号
|
||||
*/
|
||||
private String resolveSelfPhone(Long userId) {
|
||||
String account = AuthUtil.getUserAccount();
|
||||
if (isMobileLike(account)) {
|
||||
return account.trim();
|
||||
}
|
||||
R<MpUser> result = mpUserClient.userById(userId);
|
||||
if (result != null && R.isSuccess(result) && result.getData() != null
|
||||
&& isMobileLike(result.getData().getAccount())) {
|
||||
return result.getData().getAccount().trim();
|
||||
}
|
||||
return Func.isEmpty(account) ? null : account.trim();
|
||||
}
|
||||
|
||||
private boolean isMobileLike(String value) {
|
||||
return Func.isNotEmpty(value) && value.trim().matches("^1\\d{10,14}$");
|
||||
}
|
||||
|
||||
private List<String> splitPlates(String drivingVehicle) {
|
||||
@@ -1308,8 +1250,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
|
||||
@Override
|
||||
public void assertCurrentDriverOwnsWaybill(Long waybillId) {
|
||||
// 归属口径与司机打卡、司机接单一致:按当前用户手机号匹配运单司机手机号
|
||||
requireCurrentDriver();
|
||||
// 归属口径与司机打卡、司机接单一致:运单 driver_id 必须是当前登录司机的档案(查不到档案即无归属)
|
||||
loadDriverWaybill(waybillId);
|
||||
}
|
||||
|
||||
|
||||
+87
-1
@@ -41,22 +41,28 @@ import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRejectRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalRouteAdjustRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
||||
import org.springblade.transport.pojo.dto.ExceptionMaintenanceRecordRequest;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposalFollowRecord;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.MaintenanceRecord;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.ExceptionDisposalFollowRecordVO;
|
||||
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||
import org.springblade.transport.pojo.vo.MaintenanceRecordVO;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IExceptionDisposalService;
|
||||
import org.springblade.transport.service.IMaintenanceRecordService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.MpUserDataScopeSupport;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -92,6 +98,12 @@ public class ExceptionDisposalServiceImpl
|
||||
/** 拒绝原因长度上限,与上报说明、异常原因同量级,且远小于跟进记录列宽 */
|
||||
private static final int REJECT_REASON_MAX_LENGTH = 200;
|
||||
|
||||
/** 异常子流程录入的维修记录固定为车辆:入口只对「车辆故障维修」类异常开放 */
|
||||
private static final String MAINTENANCE_VEHICLE_TYPE = "车辆";
|
||||
|
||||
/** 维修记录跟进说明前缀;金额必填,维修单位按实际填写拼在后面 */
|
||||
private static final String MAINTENANCE_FOLLOW_PREFIX = "已录入维修记录:费用";
|
||||
|
||||
/** 路线与任务信息的列宽上限,与 PC 改路线同一口径 */
|
||||
private static final int ROUTE_JSON_MAX_LENGTH = 8000;
|
||||
private static final int TASK_INFO_JSON_MAX_LENGTH = 8000;
|
||||
@@ -119,17 +131,20 @@ public class ExceptionDisposalServiceImpl
|
||||
private final IDriverWaybillService driverWaybillService;
|
||||
private final MpUserDataScopeSupport mpUserDataScopeSupport;
|
||||
private final LoadingManageMapper loadingManageMapper;
|
||||
private final IMaintenanceRecordService maintenanceRecordService;
|
||||
|
||||
public ExceptionDisposalServiceImpl(ExceptionDisposalFollowRecordMapper followRecordMapper,
|
||||
IWaybillService waybillService,
|
||||
IDriverWaybillService driverWaybillService,
|
||||
MpUserDataScopeSupport mpUserDataScopeSupport,
|
||||
LoadingManageMapper loadingManageMapper) {
|
||||
LoadingManageMapper loadingManageMapper,
|
||||
IMaintenanceRecordService maintenanceRecordService) {
|
||||
this.followRecordMapper = followRecordMapper;
|
||||
this.waybillService = waybillService;
|
||||
this.driverWaybillService = driverWaybillService;
|
||||
this.mpUserDataScopeSupport = mpUserDataScopeSupport;
|
||||
this.loadingManageMapper = loadingManageMapper;
|
||||
this.maintenanceRecordService = maintenanceRecordService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -146,6 +161,7 @@ public class ExceptionDisposalServiceImpl
|
||||
ExceptionDisposalVO vo = convertVO(entity);
|
||||
fillRouteNodes(vo, fillRouteAndCargo(vo, entity.getWaybillId()));
|
||||
vo.setFollowRecords(followRecords(id));
|
||||
vo.setMaintenanceRecord(maintenanceRecord(id));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -527,6 +543,76 @@ public class ExceptionDisposalServiceImpl
|
||||
updateById(disposal);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void submitMaintenanceRecord(ExceptionMaintenanceRecordRequest request) {
|
||||
if (request == null || request.getId() == null) {
|
||||
throw new ServiceException("请选择需要录入维修记录的异常");
|
||||
}
|
||||
ExceptionDisposal disposal = getExisting(request.getId());
|
||||
if (STATUS_REVOKED.equals(disposal.getDisposalStatus())) {
|
||||
throw new ServiceException("已撤销的异常不允许录入维修记录");
|
||||
}
|
||||
if (Func.isBlank(disposal.getVehicleNo())) {
|
||||
throw new ServiceException("异常未记录车牌号,无法录入维修记录");
|
||||
}
|
||||
// 唯一索引占位后无法再插入,先按 exception_id 拦住(含已逻辑删除的记录)
|
||||
long recorded = maintenanceRecordService.count(Wrappers.<MaintenanceRecord>lambdaQuery()
|
||||
.eq(MaintenanceRecord::getExceptionId, disposal.getId()));
|
||||
if (recorded > 0) {
|
||||
throw new ServiceException("该异常已录入维修记录");
|
||||
}
|
||||
|
||||
MaintenanceRecord record = new MaintenanceRecord();
|
||||
record.setExceptionId(disposal.getId());
|
||||
record.setVehicleType(MAINTENANCE_VEHICLE_TYPE);
|
||||
record.setVehicleNo(disposal.getVehicleNo());
|
||||
record.setMaintenanceTime(request.getMaintenanceTime());
|
||||
record.setLocation(request.getLocation());
|
||||
record.setMaintainer(request.getMaintainer());
|
||||
record.setCompany(request.getCompany());
|
||||
record.setContact(request.getContact());
|
||||
record.setReplacedPart(request.getReplacedPart());
|
||||
record.setCost(request.getCost());
|
||||
record.setMileage(request.getMileage());
|
||||
record.setFactoryTime(request.getFactoryTime());
|
||||
record.setAddress(request.getAddress());
|
||||
record.setRemark(request.getRemark());
|
||||
record.setAttachments(request.getAttachments());
|
||||
// 复用维修记录自身的校验与落库(车船类型、车牌、费用、出厂时间等口径与 PC 一致)
|
||||
try {
|
||||
maintenanceRecordService.submit(record);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
// 并发重复提交:前一条尚在事务中、计数看不见,唯一索引是最终防线
|
||||
throw new ServiceException("该异常已录入维修记录");
|
||||
}
|
||||
|
||||
String followContent = maintenanceFollowContent(record.getCost(), record.getCompany());
|
||||
insertFollowRecord(disposal.getId(), followContent);
|
||||
disposal.setLatestFollowContent(followContent);
|
||||
// 录入维修记录不结案:已了结的异常只补记录与最近跟进,不回退状态
|
||||
if (!WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES.contains(disposal.getDisposalStatus())) {
|
||||
disposal.setDisposalStatus(STATUS_PROCESSING);
|
||||
}
|
||||
updateById(disposal);
|
||||
}
|
||||
|
||||
/** 跟进说明:金额必填,维修单位为空时只写金额 */
|
||||
private String maintenanceFollowContent(BigDecimal cost, String company) {
|
||||
String amount = Func.isEmpty(cost) ? "" : cost.setScale(2, RoundingMode.HALF_UP).toPlainString();
|
||||
String content = MAINTENANCE_FOLLOW_PREFIX + amount + "元";
|
||||
return Func.isBlank(company) ? content : content + ",维修单位" + company.trim();
|
||||
}
|
||||
|
||||
/** 异常关联的维修记录:一条异常至多一条,逻辑删除的视为未录入 */
|
||||
private MaintenanceRecordVO maintenanceRecord(Long disposalId) {
|
||||
MaintenanceRecord record = maintenanceRecordService.getOne(Wrappers.<MaintenanceRecord>lambdaQuery()
|
||||
.eq(MaintenanceRecord::getExceptionId, disposalId)
|
||||
.eq(MaintenanceRecord::getIsDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
return record == null ? null : BeanUtil.copyProperties(record, MaintenanceRecordVO.class);
|
||||
}
|
||||
|
||||
/** 已了结(已完成 / 已撤销)的异常不允许再次处置 */
|
||||
private void assertNotSettled(ExceptionDisposal disposal) {
|
||||
if (WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES.contains(disposal.getDisposalStatus())) {
|
||||
|
||||
+134
-1
@@ -97,6 +97,7 @@ import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.service.KingdeeSyncContext;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.KingdeeApBillSyncSupport;
|
||||
import org.springblade.transport.support.KingdeeArBillSyncSupport;
|
||||
import org.springblade.transport.support.KingdeeSyncLockSupport;
|
||||
@@ -154,6 +155,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
/** 金蝶应收单勾稽结算明细分批查询单次条数上限(in 条件分批,避免单次过滤条件过长) */
|
||||
private static final int AR_RESULT_DETAIL_QUERY_BATCH = 500;
|
||||
|
||||
/** 金蝶冲销状态回写 single-flight 锁键前缀(多副本定时任务与手动触发共用) */
|
||||
private static final String WRITEOFF_SYNC_LOCK_PREFIX = "transport:formal-settlement:kingdee-writeoff-sync:";
|
||||
|
||||
private final FormalSettlementSourceMapper sourceMapper;
|
||||
private final FormalSettlementSummaryFeeMapper summaryFeeMapper;
|
||||
private final FormalSettlementPaymentMapper paymentMapper;
|
||||
@@ -1346,6 +1350,113 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String syncKingdeeWriteOffResult() {
|
||||
return kingdeeSyncLockSupport.runSingleFlight(WRITEOFF_SYNC_LOCK_PREFIX, bfiProperties.getArResult().getLockLeaseSeconds(),
|
||||
"金蝶冲销状态回写", this::executeWriteOffSync);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshKingdeeWriteOff(Long settlementId, boolean writtenOff) {
|
||||
String status = writtenOff ? KingdeeBfiConstant.KINGDEE_WRITEOFF_WRITTEN : KingdeeBfiConstant.KINGDEE_WRITEOFF_UNWRITTEN;
|
||||
FormalSettlement settlement = getById(settlementId);
|
||||
// 状态未变化不落库,避免预检反复点击重复更新
|
||||
if (settlement == null || status.equals(settlement.getKingdeeWriteOffStatus())) {
|
||||
return;
|
||||
}
|
||||
FormalSettlement patch = new FormalSettlement();
|
||||
patch.setId(settlementId);
|
||||
patch.setKingdeeWriteOffStatus(status);
|
||||
patch.setKingdeeWriteOffTime(LocalDateTime.now());
|
||||
updateById(patch);
|
||||
log.info("正式结算单[{}]金蝶冲销状态回写:{}", settlement.getFormalSettlementNo(), status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶冲销状态回写主体:A102-4/A302-4 批量查询已同步结算单的 hadwrittenoff(部分冲销视同已冲销)
|
||||
* 并回写冲销状态,由 {@link #syncKingdeeWriteOffResult()} 在分布式锁保护下调用。
|
||||
* <p>分批与事务口径同 {@link #executeArResultSync()}:结算明细分批 in 查询、远程整批查询消除逐单 N+1,
|
||||
* 仅回写状态有变化的结算单并 updateBatchById 批量落库;整轮任务不包外层事务,按防重键幂等可重跑。
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
private String executeWriteOffSync() {
|
||||
long startMillis = System.currentTimeMillis();
|
||||
// 已同步金蝶的结算单才在金蝶存在旧应付/应收单,未同步/生成失败不查询(前端展示 '-');
|
||||
// 已冲销为终态不再复查,仅扫未冲销单据(存量 NULL 由 06 脚本回填默认未冲销)
|
||||
List<FormalSettlement> settlements = list(Wrappers.<FormalSettlement>lambdaQuery()
|
||||
.eq(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED)
|
||||
.eq(FormalSettlement::getKingdeeWriteOffStatus, KingdeeBfiConstant.KINGDEE_WRITEOFF_UNWRITTEN)
|
||||
.orderByAsc(FormalSettlement::getId));
|
||||
if (Func.isEmpty(settlements)) {
|
||||
log.info("金蝶冲销状态回写:无待查未冲销结算单");
|
||||
return "金蝶冲销状态回写:无待查未冲销结算单";
|
||||
}
|
||||
log.info("金蝶冲销状态回写:本轮扫描待查未冲销结算单{}笔", settlements.size());
|
||||
// 结算明细分批一次查全(消除逐单查询 N+1),按结算单 id 分组后按结算类型组装防重键
|
||||
List<Long> settlementIds = settlements.stream().map(FormalSettlement::getId).toList();
|
||||
Map<Long, List<FormalSettlementDetail>> detailsBySettlement = new HashMap<>();
|
||||
kingdeeApBillSyncSupport.batchForeach(settlementIds, AR_RESULT_DETAIL_QUERY_BATCH, batch ->
|
||||
detailsBySettlement.putAll(detailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
|
||||
.in(FormalSettlementDetail::getFormalSettlementId, batch)
|
||||
.eq(FormalSettlementDetail::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetail::getLineNo)).stream()
|
||||
.collect(Collectors.groupingBy(FormalSettlementDetail::getFormalSettlementId))));
|
||||
Map<String, FormalSettlement> apSettlementBySrcCode = new LinkedHashMap<>();
|
||||
Map<String, FormalSettlement> arSettlementBySrcCode = new LinkedHashMap<>();
|
||||
for (FormalSettlement settlement : settlements) {
|
||||
List<FormalSettlementDetail> details = detailsBySettlement.getOrDefault(settlement.getId(), List.of());
|
||||
if (Func.isEmpty(details)) {
|
||||
continue;
|
||||
}
|
||||
if ("receivable".equals(settlement.getSettlementType())) {
|
||||
String srcCode = kingdeeArBillSyncSupport.arBillSrcCode(settlement, details);
|
||||
if (Func.isNotBlank(srcCode)) {
|
||||
arSettlementBySrcCode.putIfAbsent(srcCode, settlement);
|
||||
}
|
||||
} else {
|
||||
String srcCode = kingdeeApBillSyncSupport.apBillSrcCode(settlement, details);
|
||||
if (Func.isNotBlank(srcCode)) {
|
||||
apSettlementBySrcCode.putIfAbsent(srcCode, settlement);
|
||||
}
|
||||
}
|
||||
}
|
||||
// A102-4/A302-4 整批一次查询,冲销状态有变化的才落库(updateBatchById 批量更新,避免逐单 update)
|
||||
log.info("金蝶冲销状态回写:防重键组装完成,应付{}个、应收{}个,按批次大小{}分批查询",
|
||||
apSettlementBySrcCode.size(), arSettlementBySrcCode.size(), bfiProperties.getWriteOffResult().getQueryBatchSize());
|
||||
Map<String, Boolean> apWriteOffFlags = kingdeeApBillSyncSupport.queryApBillWriteOffFlags(new ArrayList<>(apSettlementBySrcCode.keySet()));
|
||||
Map<String, Boolean> arWriteOffFlags = kingdeeArBillSyncSupport.queryArBillWriteOffFlags(new ArrayList<>(arSettlementBySrcCode.keySet()));
|
||||
// 金蝶未返回的键按未冲销处理,返回数与组装数差异即金蝶侧查不到单据的规模,留痕便于对账
|
||||
log.info("金蝶冲销状态批量查询完成:应付键返回{}个、应收键返回{}个,未返回键按未冲销处理",
|
||||
apWriteOffFlags.size(), arWriteOffFlags.size());
|
||||
List<FormalSettlement> toUpdateSettlements = new ArrayList<>();
|
||||
apSettlementBySrcCode.forEach((srcCode, settlement) ->
|
||||
this.applyWriteOffFlag(settlement, apWriteOffFlags.get(srcCode), toUpdateSettlements));
|
||||
arSettlementBySrcCode.forEach((srcCode, settlement) ->
|
||||
this.applyWriteOffFlag(settlement, arWriteOffFlags.get(srcCode), toUpdateSettlements));
|
||||
if (Func.isNotEmpty(toUpdateSettlements)) {
|
||||
// 批量落库按同一分批大小拆分,避免单批过大
|
||||
updateBatchById(toUpdateSettlements, bfiProperties.getWriteOffResult().getQueryBatchSize());
|
||||
log.info("金蝶冲销状态回写:批量落库完成,本批更新{}笔", toUpdateSettlements.size());
|
||||
}
|
||||
String summary = "金蝶冲销状态回写完成:待查未冲销结算单" + (apSettlementBySrcCode.size() + arSettlementBySrcCode.size())
|
||||
+ "笔,回写" + toUpdateSettlements.size() + "笔";
|
||||
log.info("{},耗时{}ms", summary, System.currentTimeMillis() - startMillis);
|
||||
return summary;
|
||||
}
|
||||
|
||||
/** 应用单笔冲销查询结果:与本地冲销状态不一致时标记更新(查不到金蝶单据按未冲销处理),一致跳过 */
|
||||
private void applyWriteOffFlag(FormalSettlement settlement, Boolean writtenOff, List<FormalSettlement> toUpdateSettlements) {
|
||||
String status = Boolean.TRUE.equals(writtenOff) ? KingdeeBfiConstant.KINGDEE_WRITEOFF_WRITTEN : KingdeeBfiConstant.KINGDEE_WRITEOFF_UNWRITTEN;
|
||||
if (status.equals(settlement.getKingdeeWriteOffStatus())) {
|
||||
return;
|
||||
}
|
||||
settlement.setKingdeeWriteOffStatus(status);
|
||||
settlement.setKingdeeWriteOffTime(LocalDateTime.now());
|
||||
toUpdateSettlements.add(settlement);
|
||||
log.info("金蝶冲销状态回写:正式结算单[{}],{}", settlement.getFormalSettlementNo(), status);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String applyPayment(FormalSettlementPaymentRequest request) {
|
||||
@@ -2109,7 +2220,29 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(json, Map.class);
|
||||
if (parsed instanceof Map<?, ?> map) {
|
||||
map.forEach((key, value) -> result.put(String.valueOf(key), decimal(value)));
|
||||
BigDecimal otherFeeAmount = BigDecimal.ZERO;
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
String key = String.valueOf(entry.getKey());
|
||||
Object value = entry.getValue();
|
||||
if (Func.isBlank(key)) {
|
||||
continue;
|
||||
}
|
||||
if ("otherFreightAmount".equals(key) || "otherFeeTotal".equals(key) || "otherAmount".equals(key)) {
|
||||
otherFeeAmount = otherFeeAmount.add(decimal(value));
|
||||
continue;
|
||||
}
|
||||
if (TransportBusinessSupport.isFreightJsonMetaKey(key)) {
|
||||
continue;
|
||||
}
|
||||
if (value instanceof Map<?, ?> || value instanceof java.util.Collection<?>) {
|
||||
continue;
|
||||
}
|
||||
result.put(key, decimal(value));
|
||||
}
|
||||
if (otherFeeAmount.signum() > 0 && result.keySet().stream()
|
||||
.noneMatch(name -> name != null && (name.contains("其它费") || name.contains("其他费")))) {
|
||||
result.put("其它费用", otherFeeAmount);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException ignored) {
|
||||
// 兼容历史费用JSON,不影响正式结算保存。
|
||||
|
||||
+11
-5
@@ -845,10 +845,16 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
}
|
||||
if (Func.isBlank(entity.getDepartmentEmails())) throw new ServiceException("推送金蝶开票申请失败:部门邮箱(发票推送邮箱)不能为空");
|
||||
if (Func.isBlank(entity.getTaxpayerNo())) throw new ServiceException("推送金蝶开票申请失败:购方纳税人识别号不能为空");
|
||||
// 金蝶组织编码:项目业务部门反查唯一平台公司 → 平台公司金蝶组织编码;申请部门编码:项目业务部门自身金蝶编码(与付款申请单部门.编码同源,共用上下文缓存)
|
||||
// 金蝶组织/申请部门:按首张关联结算单合同的平台公司 + 业务部门配置取数
|
||||
KingdeeSyncContext kingdeeContext = KingdeeSyncContext.empty();
|
||||
String kingdeeOrgNumber = kingdeeApBillSyncSupport.kingdeeOrgNumber(entity.getProjectId(), kingdeeContext);
|
||||
String kingdeeDeptNumber = kingdeeApBillSyncSupport.kingdeeDeptNumber(entity.getProjectId(), kingdeeContext);
|
||||
FormalSettlement firstSettlement = formalSettlementMapper.selectById(relations.get(0).getFormalSettlementId());
|
||||
if (firstSettlement == null || Objects.equals(firstSettlement.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("推送金蝶开票申请失败:正式结算单["
|
||||
+ Func.toStr(relations.get(0).getFormalSettlementNo()) + "]不存在");
|
||||
}
|
||||
ContractManage contract = kingdeeApBillSyncSupport.requireContract(firstSettlement.getContractId(), kingdeeContext);
|
||||
String kingdeeOrgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract, kingdeeContext);
|
||||
String kingdeeDeptNumber = kingdeeApBillSyncSupport.kingdeeDeptNumber(contract, entity.getProjectId(), kingdeeContext);
|
||||
CustomerArchive customer = entity.getReceiverCustomerId() == null ? null : customerArchiveMapper.selectById(entity.getReceiverCustomerId());
|
||||
if (customer == null || (Func.isBlank(customer.getSupplierCode()) && Func.isBlank(customer.getCustomerCode()))) {
|
||||
throw new ServiceException("推送金蝶开票申请失败:受票方客户未在客商档案中维护供应商编码或客商编码,请补充后重试");
|
||||
@@ -941,9 +947,9 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
String firstArSrcCode, BigDecimal sheetAmount, List<BfiInvoiceApplySaveEntryDTO> items) {
|
||||
BfiInvoiceApplySaveDTO param = new BfiInvoiceApplySaveDTO();
|
||||
// ==================== 以下为【必填】字段(BfiInvoiceApplySaveDTO 标注),非必填字段先注释保留 ====================
|
||||
// 【必填】组织.编码:项目业务部门反查唯一平台公司 → 平台公司金蝶组织编码
|
||||
// 【必填】组织.编码:合同平台公司金蝶组织编码
|
||||
param.setOrgidNumber(orgNumber);
|
||||
// 【必填】申请部门.编码:项目业务部门金蝶编码(与付款申请单 awrp_department_number 同源)
|
||||
// 【必填】申请部门.编码:合同平台公司下业务部门配置的金蝶部门代码
|
||||
param.setAwrpOrgfieldNumber(deptNumber);
|
||||
// 【必填】业务单据编号(候选键):单张发票候选 billno(多张发票时开票申请单号-张次序号)
|
||||
param.setBillno(billNo);
|
||||
|
||||
+154
-36
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import cn.hutool.core.util.DesensitizedUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
@@ -11,6 +12,7 @@ import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
@@ -29,9 +31,11 @@ import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.LoadingCarrierContractVO;
|
||||
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.support.ContractPartySupport;
|
||||
import org.springblade.transport.support.LiveProcessConfigSupport;
|
||||
import org.springblade.transport.support.LoadableAdmissionSupport;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
@@ -89,6 +93,9 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
@Resource
|
||||
private IProcessConfigService processConfigService;
|
||||
|
||||
@Resource
|
||||
private IDriverService driverService;
|
||||
|
||||
@Override
|
||||
public IPage<LoadingManageVO> selectLoadingManagePage(IPage<LoadingManage> page, LoadingManageVO loadingManage) {
|
||||
IPage<LoadingManage> entityPage = page(page, buildQuery(loadingManage));
|
||||
@@ -112,23 +119,27 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
return List.of();
|
||||
}
|
||||
List<ContractManage> contracts = availableCarrierContracts(projectIds);
|
||||
Set<String> allowedCarrierNames = null;
|
||||
if (Func.isNotEmpty(waybillIds)) {
|
||||
List<Waybill> waybills = loadWaybills(waybillIds);
|
||||
Set<String> allowedPartyBs = intersectCarrierPartyBs(waybills, contracts);
|
||||
if (allowedPartyBs.isEmpty()) {
|
||||
allowedCarrierNames = intersectCarrierPartyBs(waybills, contracts);
|
||||
if (allowedCarrierNames.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Set<String> allowedNames = allowedCarrierNames;
|
||||
contracts = contracts.stream()
|
||||
.filter(contract -> allowedPartyBs.contains(normalizePartyName(contract.getPartyB())))
|
||||
.filter(contract -> ContractPartySupport.carrierPartyNames(contract).stream()
|
||||
.anyMatch(allowedNames::contains))
|
||||
.toList();
|
||||
}
|
||||
return contracts.stream().map(contract -> {
|
||||
LoadingCarrierContractVO option = new LoadingCarrierContractVO();
|
||||
option.setId(contract.getId());
|
||||
option.setContractName(contract.getContractName());
|
||||
option.setCarrierName(contract.getPartyB());
|
||||
return option;
|
||||
}).toList();
|
||||
List<LoadingCarrierContractVO> options = ContractPartySupport.expandCarrierOptions(contracts);
|
||||
if (allowedCarrierNames == null) {
|
||||
return options;
|
||||
}
|
||||
Set<String> allowedNames = allowedCarrierNames;
|
||||
return options.stream()
|
||||
.filter(option -> allowedNames.contains(normalizePartyName(option.getCarrierName())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -139,16 +150,17 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
List<Long> oldWaybillIds = new ArrayList<>();
|
||||
// 新建暂存为草稿;编辑已有单据时保持原业务状态(如待执行),避免被改回草稿
|
||||
String businessStatus = STATUS_DRAFT;
|
||||
if (Func.isNotEmpty(loadingManage.getId())) {
|
||||
LoadingManage oldRecord = loadEditable(loadingManage.getId(), true);
|
||||
oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson());
|
||||
loadingManage.setLoadingNo(oldRecord.getLoadingNo());
|
||||
loadingManage.setDeptId(oldRecord.getDeptId());
|
||||
loadingManage.setDeptName(oldRecord.getDeptName());
|
||||
if (Func.isNotEmpty(oldRecord.getBusinessStatus())) {
|
||||
businessStatus = oldRecord.getBusinessStatus();
|
||||
LoadingManage stored = Func.isEmpty(loadingManage.getId()) ? null : loadEditable(loadingManage.getId(), true);
|
||||
if (stored != null) {
|
||||
oldWaybillIds = waybillIds(stored.getWaybillIdsJson());
|
||||
loadingManage.setLoadingNo(stored.getLoadingNo());
|
||||
loadingManage.setDeptId(stored.getDeptId());
|
||||
loadingManage.setDeptName(stored.getDeptName());
|
||||
if (Func.isNotEmpty(stored.getBusinessStatus())) {
|
||||
businessStatus = stored.getBusinessStatus();
|
||||
}
|
||||
}
|
||||
applyDriverOwnership(loadingManage, stored);
|
||||
prepareCreateOrUpdate(loadingManage);
|
||||
loadingManage.setBusinessStatus(businessStatus);
|
||||
if (Func.isEmpty(loadingManage.getLoadingNo())) {
|
||||
@@ -200,6 +212,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
loadingManage.setDeptId(oldRecord.getDeptId());
|
||||
loadingManage.setDeptName(oldRecord.getDeptName());
|
||||
}
|
||||
applyDriverOwnership(loadingManage, oldRecord);
|
||||
prepareCreateOrUpdate(loadingManage);
|
||||
if (Func.isEmpty(loadingManage.getLoadingNo())) {
|
||||
loadingManage.setLoadingNo(nextCode());
|
||||
@@ -319,6 +332,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
loadingManage.setCargoName(first.getCargoName());
|
||||
loadingManage.setVehicleNo(first.getVehicleNo());
|
||||
loadingManage.setTrailerVehicleNo(first.getTrailerVehicleNo());
|
||||
loadingManage.setDriverId(first.getDriverId());
|
||||
loadingManage.setDriverName(first.getDriverName());
|
||||
loadingManage.setDriverPhone(first.getDriverPhone());
|
||||
loadingManage.setEscortName(first.getEscortName());
|
||||
@@ -478,6 +492,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
target.setTrailerVehicleNo(source.getTrailerVehicleNo());
|
||||
target.setDriverName(source.getDriverName());
|
||||
target.setDriverPhone(source.getDriverPhone());
|
||||
target.setDriverId(source.getDriverId());
|
||||
target.setEscortName(source.getEscortName());
|
||||
target.setEscortPhone(source.getEscortPhone());
|
||||
target.setCarrierType(source.getCarrierType());
|
||||
@@ -505,6 +520,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
target.setBusinessStatus(STATUS_DRAFT);
|
||||
target.setLoadingNo(nextCode());
|
||||
prepare(target);
|
||||
applyDriverOwnership(target, null);
|
||||
validate(target, true);
|
||||
save(target);
|
||||
return detail(target.getId());
|
||||
@@ -521,6 +537,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
throw new ServiceException("仅司机拒绝接单后允许重新派单");
|
||||
}
|
||||
prepare(loadingManage);
|
||||
applyDriverOwnership(loadingManage, oldRecord);
|
||||
validate(loadingManage, false);
|
||||
List<Long> oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson());
|
||||
loadingManage.setLoadingNo(oldRecord.getLoadingNo());
|
||||
@@ -662,7 +679,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
TransportBusinessSupport.validateAllDept(loadingManage.getAllDept(), "配载管理");
|
||||
LambdaQueryWrapper<LoadingManage> queryWrapper = Wrappers.<LoadingManage>lambdaQuery().eq(LoadingManage::getIsDeleted, 0);
|
||||
if (!Objects.equals(loadingManage.getAllDept(), 1)) {
|
||||
queryWrapper.eq(LoadingManage::getDeptId, TransportBusinessSupport.currentDeptId("配载管理"));
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("配载管理");
|
||||
queryWrapper.and(wrapper -> {
|
||||
wrapper.eq(LoadingManage::getDeptId, currentDeptId);
|
||||
TransportBusinessSupport.orProjectVisibleByBusinessDept(wrapper, List.of(currentDeptId));
|
||||
});
|
||||
} else if (Func.isNotEmpty(loadingManage.getDeptId())) {
|
||||
queryWrapper.eq(LoadingManage::getDeptId, loadingManage.getDeptId());
|
||||
}
|
||||
@@ -781,6 +802,32 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写司机归属:档案 ID 只在需要时解析,已落在单据上的归属不会被无关保存抹掉。
|
||||
* <p>
|
||||
* 判定顺序:调用方带了正数档案 ID 就用它;更新时 driver_phone 与库中一致且库中 driver_id 非空就保留库中值;
|
||||
* 其余情况(新建、换号、原本无主)按手机号在租户内唯一匹配司机档案,匹配不到落 null 并留 WARN,不阻断保存。
|
||||
*/
|
||||
private void applyDriverOwnership(LoadingManage loadingManage, LoadingManage stored) {
|
||||
Long driverId = loadingManage.getDriverId();
|
||||
if (driverId != null && driverId > 0) {
|
||||
return;
|
||||
}
|
||||
if (stored != null && Func.isNotEmpty(stored.getDriverId())
|
||||
&& Objects.equals(TransportBusinessSupport.trimToNull(stored.getDriverPhone()), loadingManage.getDriverPhone())) {
|
||||
loadingManage.setDriverId(stored.getDriverId());
|
||||
return;
|
||||
}
|
||||
String tenantId = Func.isNotEmpty(loadingManage.getTenantId()) ? loadingManage.getTenantId() : AuthUtil.getTenantId();
|
||||
Long resolvedDriverId = driverService.resolveDriverIdByMobile(tenantId, loadingManage.getDriverPhone());
|
||||
loadingManage.setDriverId(resolvedDriverId);
|
||||
if (resolvedDriverId == null) {
|
||||
log.warn("配载单 {} 的司机手机号未唯一匹配司机档案,driver_id 落空。租户={} 手机号={}",
|
||||
Func.isEmpty(loadingManage.getLoadingNo()) ? "(未编号)" : loadingManage.getLoadingNo(),
|
||||
tenantId, DesensitizedUtil.mobilePhone(loadingManage.getDriverPhone()));
|
||||
}
|
||||
}
|
||||
|
||||
private void validate(LoadingManage loadingManage, boolean draftMode) {
|
||||
TransportBusinessSupport.validateRequired(loadingManage.getLoadingSubNos(), "配载子单号不能为空");
|
||||
TransportBusinessSupport.validateRequired(loadingManage.getTransportType(), "运输类型不能为空");
|
||||
@@ -907,13 +954,17 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一配载单子运单除同项目外,还必须存在共同的承运商合同乙方(按客户合同甲方匹配后取交集)。
|
||||
* 同一配载单子运单除同项目外,还必须存在共同的承运商合同乙方(按客户合同乙方=承运商合同甲方匹配后取交集)。
|
||||
*/
|
||||
private void validateCompatibleCarrierPartyB(List<Waybill> waybills) {
|
||||
if (Func.isEmpty(waybills) || waybills.size() < 2) {
|
||||
return;
|
||||
}
|
||||
List<ContractManage> contracts = availableCarrierContracts(projectIdsOf(waybills));
|
||||
// 项目下没有任何可用承运商合同时不存在乙方交集约束;承运类型为「承运商」由 validateCarrierContract 要求选择合同
|
||||
if (Func.isEmpty(contracts)) {
|
||||
return;
|
||||
}
|
||||
Set<String> allowedPartyBs = intersectCarrierPartyBs(waybills, contracts);
|
||||
if (allowedPartyBs.isEmpty()) {
|
||||
throw new ServiceException("所选运单在同一项目下没有共同的承运商合同乙方,不能配载到同一配载单");
|
||||
@@ -927,6 +978,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
private void validateCarrierContract(LoadingManage loadingManage, boolean required, List<Waybill> waybills) {
|
||||
if (!Objects.equals(loadingManage.getCarrierType(), "承运商")) {
|
||||
loadingManage.setCarrierContractId(null);
|
||||
loadingManage.setCarrierPartyId(null);
|
||||
loadingManage.setCarrierPartyRole(null);
|
||||
if (!Objects.equals(loadingManage.getCarrierType(), CARRIER_SELF)) {
|
||||
loadingManage.setCarrierName(null);
|
||||
}
|
||||
@@ -937,6 +990,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
throw new ServiceException("请选择承运商合同");
|
||||
}
|
||||
loadingManage.setCarrierName(null);
|
||||
loadingManage.setCarrierPartyId(null);
|
||||
loadingManage.setCarrierPartyRole(null);
|
||||
return;
|
||||
}
|
||||
ContractManage carrierContract = availableCarrierContractById(loadingManage.getCarrierContractId());
|
||||
@@ -957,19 +1012,35 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
? "所选承运商合同不属于本项目"
|
||||
: "所选承运商合同不属于运单" + waybillNos + "的项目");
|
||||
}
|
||||
Map<String, Object> selectedParty = ContractPartySupport.findCarrierParty(
|
||||
carrierContract, loadingManage.getCarrierPartyId(), loadingManage.getCarrierName());
|
||||
if (selectedParty == null && Func.isNotEmpty(loadingManage.getCarrierName())
|
||||
&& !ContractPartySupport.matchesCarrier(carrierContract, loadingManage.getCarrierName(), loadingManage.getCarrierPartyId())) {
|
||||
throw new ServiceException("所选承运商不属于该承运商合同的乙方或丙方客商");
|
||||
}
|
||||
if (selectedParty != null) {
|
||||
Object customerName = selectedParty.get("customerName");
|
||||
Object partyId = selectedParty.get("id");
|
||||
Object partyRole = selectedParty.get("partyRole");
|
||||
loadingManage.setCarrierName(customerName == null ? null : normalizePartyName(String.valueOf(customerName)));
|
||||
loadingManage.setCarrierPartyId(partyId == null ? null : normalizePartyName(String.valueOf(partyId)));
|
||||
loadingManage.setCarrierPartyRole(partyRole == null ? null : ContractPartySupport.normalizeRole(String.valueOf(partyRole)));
|
||||
} else if (Func.isEmpty(loadingManage.getCarrierName())) {
|
||||
loadingManage.setCarrierName(ContractPartySupport.firstPartyName(carrierContract.getPartyBJson(), carrierContract.getPartyB()));
|
||||
loadingManage.setCarrierPartyRole(ContractPartySupport.ROLE_B);
|
||||
}
|
||||
if (Func.isNotEmpty(childWaybills)) {
|
||||
List<ContractManage> projectContracts = availableCarrierContracts(projectIdsOf(childWaybills));
|
||||
Set<String> allowedPartyBs = intersectCarrierPartyBs(childWaybills, projectContracts);
|
||||
String partyB = normalizePartyName(carrierContract.getPartyB());
|
||||
if (Func.isNotEmpty(allowedPartyBs) && !allowedPartyBs.contains(partyB)) {
|
||||
throw new ServiceException("所选承运商合同乙方与子运单可共同承运的乙方不一致,可选:"
|
||||
String carrierName = normalizePartyName(loadingManage.getCarrierName());
|
||||
if (Func.isNotEmpty(allowedPartyBs) && !allowedPartyBs.contains(carrierName)) {
|
||||
throw new ServiceException("所选承运客商与子运单可共同承运的客商不一致,可选:"
|
||||
+ String.join("、", allowedPartyBs));
|
||||
}
|
||||
if (allowedPartyBs.isEmpty()) {
|
||||
throw new ServiceException("所选运单在同一项目下没有共同的承运商合同乙方,不能配载到同一配载单");
|
||||
throw new ServiceException("所选运单在同一项目下没有共同的承运商合同客商,不能配载到同一配载单");
|
||||
}
|
||||
}
|
||||
loadingManage.setCarrierName(carrierContract.getPartyB());
|
||||
}
|
||||
|
||||
private void validateCompletedCarrierContract(LoadingManage loadingManage) {
|
||||
@@ -1008,14 +1079,13 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
}
|
||||
|
||||
/**
|
||||
* 各子运单按「客户合同甲方 = 承运商合同甲方」匹配可用承运商合同,再对乙方名称取交集。
|
||||
* 例:子单1可选 A/B/C,子单2可选 B,则交集为 B。
|
||||
* 各子运单按「客户合同乙方 ≈ 承运商合同甲方」匹配可用承运商合同,再对乙/丙方客商名称取交集。
|
||||
*/
|
||||
private Set<String> intersectCarrierPartyBs(List<Waybill> waybills, List<ContractManage> projectContracts) {
|
||||
Set<String> intersection = null;
|
||||
Map<Long, String> customerPartyAMap = loadCustomerContractPartyAMap(waybills);
|
||||
Map<Long, Set<String>> customerPartyBMap = loadCustomerContractPartyBMap(waybills);
|
||||
for (Waybill waybill : waybills) {
|
||||
Set<String> available = availableCarrierPartyBs(waybill, projectContracts, customerPartyAMap);
|
||||
Set<String> available = availableCarrierPartyBs(waybill, projectContracts, customerPartyBMap);
|
||||
if (intersection == null) {
|
||||
intersection = new LinkedHashSet<>(available);
|
||||
} else {
|
||||
@@ -1029,21 +1099,23 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
}
|
||||
|
||||
private Set<String> availableCarrierPartyBs(Waybill waybill, List<ContractManage> projectContracts,
|
||||
Map<Long, String> customerPartyAMap) {
|
||||
Map<Long, Set<String>> customerPartyBMap) {
|
||||
if (waybill == null || Func.isEmpty(projectContracts)) {
|
||||
return Set.of();
|
||||
}
|
||||
String partyA = customerPartyAMap.get(waybill.getContractId());
|
||||
Long contractId = waybill.getContractId();
|
||||
Set<String> customerPartyBs = contractId == null
|
||||
? Set.of()
|
||||
: customerPartyBMap.getOrDefault(contractId, Set.of());
|
||||
return projectContracts.stream()
|
||||
.filter(contract -> Objects.equals(contract.getProjectId(), waybill.getProjectId()))
|
||||
.filter(contract -> Func.isEmpty(partyA) || Objects.equals(normalizePartyName(contract.getPartyA()), partyA))
|
||||
.map(ContractManage::getPartyB)
|
||||
.map(this::normalizePartyName)
|
||||
.filter(contract -> customerPartyBs.isEmpty() || partyNamesOverlap(partyANames(contract), customerPartyBs))
|
||||
.flatMap(contract -> ContractPartySupport.carrierPartyNames(contract).stream())
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private Map<Long, String> loadCustomerContractPartyAMap(List<Waybill> waybills) {
|
||||
private Map<Long, Set<String>> loadCustomerContractPartyBMap(List<Waybill> waybills) {
|
||||
List<Long> contractIds = waybills.stream()
|
||||
.map(Waybill::getContractId)
|
||||
.filter(Objects::nonNull)
|
||||
@@ -1057,11 +1129,55 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
.in(ContractManage::getId, contractIds))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(ContractManage::getId,
|
||||
contract -> normalizePartyName(contract.getPartyA()),
|
||||
this::partyBNames,
|
||||
(left, right) -> left,
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
|
||||
private Set<String> partyANames(ContractManage contract) {
|
||||
Set<String> names = ContractPartySupport.parsePartyRows(contract.getPartyAJson()).stream()
|
||||
.map(row -> normalizePartyName(String.valueOf(row.get("customerName"))))
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (names.isEmpty() && Func.isNotEmpty(contract.getPartyA())) {
|
||||
for (String part : contract.getPartyA().split("[、,]")) {
|
||||
String name = normalizePartyName(part);
|
||||
if (Func.isNotEmpty(name)) {
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
private Set<String> partyBNames(ContractManage contract) {
|
||||
Set<String> names = ContractPartySupport.parsePartyRows(contract.getPartyBJson()).stream()
|
||||
.map(row -> normalizePartyName(String.valueOf(row.get("customerName"))))
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (names.isEmpty() && Func.isNotEmpty(contract.getPartyB())) {
|
||||
for (String part : contract.getPartyB().split("[、,]")) {
|
||||
String name = normalizePartyName(part);
|
||||
if (Func.isNotEmpty(name)) {
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
private boolean partyNamesOverlap(Set<String> left, Set<String> right) {
|
||||
if (Func.isEmpty(left) || Func.isEmpty(right)) {
|
||||
return false;
|
||||
}
|
||||
for (String name : left) {
|
||||
if (right.contains(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String normalizePartyName(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
@@ -1150,6 +1266,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
.set(Waybill::getCarrierType, loadingManage.getCarrierType())
|
||||
.set(Waybill::getCarrierContractId, loadingManage.getCarrierContractId())
|
||||
.set(Waybill::getCarrierName, loadingManage.getCarrierName())
|
||||
.set(Waybill::getDriverId, loadingManage.getDriverId())
|
||||
.set(Waybill::getDriverName, loadingManage.getDriverName())
|
||||
.set(Waybill::getDriverPhone, loadingManage.getDriverPhone())
|
||||
.set(Waybill::getVehicleNo, loadingManage.getVehicleNo())
|
||||
@@ -1264,6 +1381,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
.set(Waybill::getCarrierType, loadingManage.getCarrierType())
|
||||
.set(Waybill::getCarrierContractId, loadingManage.getCarrierContractId())
|
||||
.set(Waybill::getCarrierName, loadingManage.getCarrierName())
|
||||
.set(Waybill::getDriverId, loadingManage.getDriverId())
|
||||
.set(Waybill::getDriverName, loadingManage.getDriverName())
|
||||
.set(Waybill::getDriverPhone, loadingManage.getDriverPhone())
|
||||
.set(Waybill::getVehicleNo, loadingManage.getVehicleNo())
|
||||
|
||||
+13
@@ -27,6 +27,7 @@ package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
@@ -81,6 +82,18 @@ public class MaintenanceRecordServiceImpl extends BaseServiceImpl<MaintenanceRec
|
||||
return saveOrUpdate(maintenanceRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean deleteLogic(List<Long> ids) {
|
||||
if (Func.isNotEmpty(ids)) {
|
||||
// 唯一索引不因逻辑删除释放:不清空 exception_id,异常侧会永远显示「已录入」却查不到记录
|
||||
update(Wrappers.<MaintenanceRecord>lambdaUpdate()
|
||||
.set(MaintenanceRecord::getExceptionId, null)
|
||||
.in(MaintenanceRecord::getId, ids));
|
||||
}
|
||||
return super.deleteLogic(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<MaintenanceRecordExcel> importMaintenanceRecord(List<MaintenanceRecordExcel> data) {
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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.service.impl;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.transport.mapper.ManageArchiveDeptMapper;
|
||||
import org.springblade.transport.pojo.vo.ArchiveManageStatsVO;
|
||||
import org.springblade.transport.pojo.vo.DeptTreeNodeVO;
|
||||
import org.springblade.transport.pojo.vo.DriverVO;
|
||||
import org.springblade.transport.pojo.vo.TransportVehicleVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IManageArchiveService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 调度端档案统计
|
||||
*/
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class ManageArchiveServiceImpl implements IManageArchiveService {
|
||||
|
||||
/**
|
||||
* 组织选择器不提供这一支:web 端司机/车辆表单同样剔除「外部组织」。
|
||||
*/
|
||||
private static final String EXTERNAL_ORG_NAME = "外部组织";
|
||||
|
||||
private final IDriverService driverService;
|
||||
private final ITransportVehicleService transportVehicleService;
|
||||
private final ManageArchiveDeptMapper manageArchiveDeptMapper;
|
||||
|
||||
@Override
|
||||
public ArchiveManageStatsVO stats() {
|
||||
// 复用管理端同一套统计:today / warningDate 由各 service 的 prepareQuery 自己填,
|
||||
// 不传筛选条件即全量计数(与小程序侧 searchDrivers 同口径,未按组织收口)
|
||||
ArchiveManageStatsVO vo = new ArchiveManageStatsVO();
|
||||
vo.setDriver(driverService.expiryStat(new DriverVO()));
|
||||
vo.setVehicle(transportVehicleService.expiryStat(new TransportVehicleVO()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DeptTreeNodeVO> deptTree() {
|
||||
List<Dept> rows = manageArchiveDeptMapper.selectEnabledByTenant(AuthUtil.getTenantId());
|
||||
Map<Long, DeptTreeNodeVO> nodes = new LinkedHashMap<>();
|
||||
for (Dept row : rows) {
|
||||
DeptTreeNodeVO node = new DeptTreeNodeVO();
|
||||
node.setId(row.getId());
|
||||
node.setName(Func.toStr(row.getDeptName()));
|
||||
nodes.put(row.getId(), node);
|
||||
}
|
||||
List<DeptTreeNodeVO> roots = new ArrayList<>();
|
||||
for (Dept row : rows) {
|
||||
DeptTreeNodeVO node = nodes.get(row.getId());
|
||||
DeptTreeNodeVO parent = row.getParentId() == null ? null : nodes.get(row.getParentId());
|
||||
// 父部门停用或删除时该节点作为根保留,避免整支在树上消失
|
||||
if (parent == null) {
|
||||
roots.add(node);
|
||||
} else {
|
||||
parent.getChildren().add(node);
|
||||
}
|
||||
}
|
||||
removeExternalOrg(roots);
|
||||
return roots;
|
||||
}
|
||||
|
||||
private static void removeExternalOrg(List<DeptTreeNodeVO> nodes) {
|
||||
nodes.removeIf(node -> EXTERNAL_ORG_NAME.equals(Func.toStr(node.getName()).trim()));
|
||||
for (DeptTreeNodeVO node : nodes) {
|
||||
removeExternalOrg(node.getChildren());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+67
-14
@@ -38,6 +38,7 @@ import org.springblade.transport.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.RiskDisposal;
|
||||
import org.springblade.transport.pojo.entity.TransportVehicle;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.AdminDriverOptionVO;
|
||||
import org.springblade.transport.pojo.vo.AdminHomeBadgesVO;
|
||||
@@ -58,6 +59,7 @@ import org.springblade.transport.service.IExceptionDisposalService;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IManageWaybillService;
|
||||
import org.springblade.transport.service.IRiskDisposalService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.MpUserDataScopeSupport;
|
||||
@@ -122,6 +124,9 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
private static final String TRANSPORT_COMMON = "common";
|
||||
private static final String TRANSPORT_LOAD = "load";
|
||||
|
||||
/** 启用状态(base_entity.status):停用的司机不进派单检索、停用的车不进选车列表 */
|
||||
private static final int ENABLED_STATUS = 1;
|
||||
|
||||
private static final int FEED_LIMIT = 20;
|
||||
private static final int DEFAULT_PAGE_SIZE = 10;
|
||||
private static final int MAX_PAGE_SIZE = 50;
|
||||
@@ -141,6 +146,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
private final IDriverWaybillService driverWaybillService;
|
||||
private final ITripQueryService tripQueryService;
|
||||
private final ILoadingManageService loadingManageService;
|
||||
private final ITransportVehicleService transportVehicleService;
|
||||
private final MpUserDataScopeSupport mpUserDataScopeSupport;
|
||||
|
||||
@Override
|
||||
@@ -164,7 +170,8 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
home.setStats(stats());
|
||||
|
||||
AdminHomeBadgesVO badges = new AdminHomeBadgesVO();
|
||||
badges.setException(home.getStats().getException());
|
||||
// 快捷入口的红点是待办数,只数待处理:处置中的已经在跟,不再提醒
|
||||
badges.setException(countPendingExceptions());
|
||||
badges.setRisk(countPendingRisks());
|
||||
badges.setPendingReassign(countPendingReassignments());
|
||||
home.setBadges(badges);
|
||||
@@ -859,6 +866,8 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
String key = Func.toStr(keyword, "").trim();
|
||||
LambdaQueryWrapper<Driver> wrapper = Wrappers.<Driver>lambdaQuery()
|
||||
.eq(Driver::getIsDeleted, 0)
|
||||
// 停用司机不可派单:不进调度检索。停用只拦派单,不拦登录(司机仍能进小程序看自己的运单)
|
||||
.eq(Driver::getStatus, ENABLED_STATUS)
|
||||
.orderByDesc(Driver::getUpdateTime)
|
||||
.last("LIMIT 20");
|
||||
if (Func.isNotBlank(key)) {
|
||||
@@ -877,27 +886,63 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
@Override
|
||||
public List<AdminVehicleOptionVO> searchVehicles(String keyword) {
|
||||
String key = Func.toStr(keyword, "").trim();
|
||||
LambdaQueryWrapper<Driver> wrapper = Wrappers.<Driver>lambdaQuery()
|
||||
.eq(Driver::getIsDeleted, 0)
|
||||
.isNotNull(Driver::getDrivingVehicle)
|
||||
.ne(Driver::getDrivingVehicle, "")
|
||||
.orderByDesc(Driver::getUpdateTime)
|
||||
// 权威来源是车辆档案表:此前查的是司机表的 driving_vehicle 文本,既看不见没有司机绑定的车,
|
||||
// 也无从判断「是否停用」——停用的车会照样出现在派单选车列表里。
|
||||
LambdaQueryWrapper<TransportVehicle> wrapper = Wrappers.<TransportVehicle>lambdaQuery()
|
||||
.eq(TransportVehicle::getIsDeleted, 0)
|
||||
.eq(TransportVehicle::getStatus, ENABLED_STATUS)
|
||||
.orderByDesc(TransportVehicle::getUpdateTime)
|
||||
.last("LIMIT 30");
|
||||
if (Func.isNotBlank(key)) {
|
||||
wrapper.like(Driver::getDrivingVehicle, key);
|
||||
wrapper.like(TransportVehicle::getPlateNo, key);
|
||||
}
|
||||
java.util.LinkedHashMap<String, AdminVehicleOptionVO> map = new java.util.LinkedHashMap<>();
|
||||
for (Driver d : driverService.list(wrapper)) {
|
||||
String plate = Func.toStr(d.getDrivingVehicle(), "").trim();
|
||||
if (Func.isBlank(plate) || map.containsKey(plate)) {
|
||||
List<TransportVehicle> vehicles = transportVehicleService.list(wrapper);
|
||||
if (Func.isEmpty(vehicles)) {
|
||||
return List.of();
|
||||
}
|
||||
Map<String, String> driverNameByPlate = driverNamesByPlate(vehicles);
|
||||
List<AdminVehicleOptionVO> options = new ArrayList<>();
|
||||
for (TransportVehicle vehicle : vehicles) {
|
||||
String plate = Func.toStr(vehicle.getPlateNo(), "").trim();
|
||||
if (Func.isBlank(plate)) {
|
||||
continue;
|
||||
}
|
||||
AdminVehicleOptionVO vo = new AdminVehicleOptionVO();
|
||||
vo.setVehicleNo(plate);
|
||||
vo.setDriverName(Func.toStr(d.getDriverName(), ""));
|
||||
map.put(plate, vo);
|
||||
vo.setDriverName(Func.toStr(driverNameByPlate.get(plate.toUpperCase()), ""));
|
||||
options.add(vo);
|
||||
}
|
||||
return new java.util.ArrayList<>(map.values());
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 车牌 → 绑定司机姓名
|
||||
* <p>
|
||||
* 绑定关系是司机侧按车牌文本声明的({@code driver.driving_vehicle}),车辆行不存司机,
|
||||
* 所以选车列表要回到司机表补名字;多车牌串的司机匹配不到,这条既有缺陷不在本次改动内。
|
||||
*/
|
||||
private Map<String, String> driverNamesByPlate(List<TransportVehicle> vehicles) {
|
||||
List<String> plates = vehicles.stream()
|
||||
.map(TransportVehicle::getPlateNo)
|
||||
.filter(Func::isNotEmpty)
|
||||
.map(String::toUpperCase)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (plates.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<Driver> drivers = driverService.list(Wrappers.<Driver>lambdaQuery()
|
||||
.eq(Driver::getIsDeleted, 0)
|
||||
.in(Driver::getDrivingVehicle, plates)
|
||||
.select(Driver::getDriverName, Driver::getDrivingVehicle));
|
||||
Map<String, String> names = new LinkedHashMap<>();
|
||||
for (Driver driver : drivers) {
|
||||
if (Func.isEmpty(driver.getDrivingVehicle()) || Func.isEmpty(driver.getDriverName())) {
|
||||
continue;
|
||||
}
|
||||
names.putIfAbsent(driver.getDrivingVehicle().toUpperCase(), driver.getDriverName());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** 运输方式字典值 → 展示文案 */
|
||||
@@ -1169,6 +1214,14 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
return exceptionDisposalService.count(wrapper);
|
||||
}
|
||||
|
||||
/** 快捷入口「异常处置」红点:只数待处理(pending),处置中已不算待办 */
|
||||
private long countPendingExceptions() {
|
||||
LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.eq(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING);
|
||||
applyExceptionDeptScope(wrapper);
|
||||
return exceptionDisposalService.count(wrapper);
|
||||
}
|
||||
|
||||
private long countPendingRisks() {
|
||||
LambdaQueryWrapper<RiskDisposal> wrapper = Wrappers.<RiskDisposal>lambdaQuery()
|
||||
.eq(RiskDisposal::getDisposalStatus, RISK_PENDING);
|
||||
|
||||
+53
-9
@@ -4,8 +4,10 @@
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import cn.hutool.core.util.DesensitizedUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
@@ -24,9 +26,11 @@ import org.springblade.transport.pojo.vo.MasterOrderCarrierVO;
|
||||
import org.springblade.transport.pojo.vo.MasterOrderVO;
|
||||
import org.springblade.transport.service.IMasterOrderService;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProjectApplyService;
|
||||
import org.springblade.transport.service.ITransportPlanService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.ContractPartySupport;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
import org.springblade.transport.wrapper.WaybillWrapper;
|
||||
@@ -50,6 +54,7 @@ import java.util.Objects;
|
||||
* @author Chill
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, MasterOrder> implements IMasterOrderService {
|
||||
|
||||
private static final String CARRIER_SELF = "自运";
|
||||
@@ -58,13 +63,15 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
|
||||
private final ITransportPlanService transportPlanService;
|
||||
private final IProjectApplyService projectApplyService;
|
||||
private final IContractManageService contractManageService;
|
||||
private final IDriverService driverService;
|
||||
|
||||
public MasterOrderServiceImpl(IWaybillService waybillService, ITransportPlanService transportPlanService, IProjectApplyService projectApplyService,
|
||||
IContractManageService contractManageService) {
|
||||
IContractManageService contractManageService, IDriverService driverService) {
|
||||
this.waybillService = waybillService;
|
||||
this.transportPlanService = transportPlanService;
|
||||
this.projectApplyService = projectApplyService;
|
||||
this.contractManageService = contractManageService;
|
||||
this.driverService = driverService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,11 +88,15 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
|
||||
|
||||
@Override
|
||||
public List<MasterOrderCarrierVO> carriers(Long id) {
|
||||
return availableCarrierContracts(getRequired(id)).stream().map(contract -> {
|
||||
return ContractPartySupport.expandCarrierOptions(availableCarrierContracts(getRequired(id))).stream().map(option -> {
|
||||
MasterOrderCarrierVO carrier = new MasterOrderCarrierVO();
|
||||
carrier.setContractId(contract.getId());
|
||||
carrier.setContractName(contract.getContractName());
|
||||
carrier.setCarrierName(contract.getPartyB());
|
||||
carrier.setContractId(option.getId());
|
||||
carrier.setContractName(option.getContractName());
|
||||
carrier.setCarrierName(option.getCarrierName());
|
||||
carrier.setCarrierPartyId(option.getCarrierPartyId());
|
||||
carrier.setCarrierPartyRole(option.getCarrierPartyRole());
|
||||
carrier.setCreditCode(option.getCreditCode());
|
||||
carrier.setCustomerId(option.getCustomerId());
|
||||
return carrier;
|
||||
}).toList();
|
||||
}
|
||||
@@ -261,8 +272,23 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
|
||||
waybill.setContractId(masterOrder.getContractId()); waybill.setContractName(masterOrder.getContractName());
|
||||
waybill.setCustomerName(masterOrder.getCustomerName()); waybill.setMasterNo(masterOrder.getMasterNo());
|
||||
waybill.setTransportType(string(dispatch, "transportType")); waybill.setCarrierType(string(dispatch, "carrierType"));
|
||||
waybill.setCarrierContractId(longValue(dispatch, "carrierContractId")); waybill.setCarrierName(string(dispatch, "carrierName")); waybill.setDriverName(string(dispatch, "driverName"));
|
||||
waybill.setCarrierContractId(longValue(dispatch, "carrierContractId"));
|
||||
waybill.setCarrierName(string(dispatch, "carrierName"));
|
||||
waybill.setCarrierPartyId(string(dispatch, "carrierPartyId"));
|
||||
waybill.setCarrierPartyRole(string(dispatch, "carrierPartyRole"));
|
||||
waybill.setDriverName(string(dispatch, "driverName"));
|
||||
waybill.setDriverPhone(string(dispatch, "driverPhone")); waybill.setVehicleNo(string(dispatch, "vehicleNo")); waybill.setTrailerVehicleNo(string(dispatch, "trailerVehicleNo"));
|
||||
// 归属主键是档案 ID:调度带了就用它,否则按手机号解析;解析入口只返回 null 或唯一命中的
|
||||
// 档案 ID,承运商司机解析为空也照常派单,落空时留 WARN
|
||||
Long driverId = longValue(dispatch, "driverId");
|
||||
if (driverId == null || driverId <= 0) {
|
||||
driverId = driverService.resolveDriverIdByMobile(waybill.getTenantId(), waybill.getDriverPhone());
|
||||
if (driverId == null) {
|
||||
log.warn("总单 {} 派单的司机手机号未唯一匹配司机档案,driver_id 落空(不阻断派单)。租户={} 手机号={}",
|
||||
masterOrder.getMasterNo(), waybill.getTenantId(), DesensitizedUtil.mobilePhone(waybill.getDriverPhone()));
|
||||
}
|
||||
}
|
||||
waybill.setDriverId(driverId);
|
||||
waybill.setCaptainName(string(dispatch, "captainName")); waybill.setCabinNo(string(dispatch, "cabinNo")); waybill.setContainerNo(string(dispatch, "containerNo"));
|
||||
waybill.setEscortName(string(dispatch, "escortName")); waybill.setEscortPhone(string(dispatch, "escortPhone")); waybill.setMileage(nullableDecimal(dispatch, "mileage"));
|
||||
waybill.setDepartureName(string(dispatch, "departureName")); waybill.setDepartureAddress(string(dispatch, "departureAddress"));
|
||||
@@ -586,7 +612,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
|
||||
.and(wrapper -> wrapper.isNull(ContractManage::getContractStage)
|
||||
.or().ne(ContractManage::getContractStage, "terminated"))
|
||||
.orderByDesc(ContractManage::getCreateTime))
|
||||
.stream().filter(contract -> Func.isNotEmpty(contract.getPartyB())).toList();
|
||||
.stream().filter(contract -> Func.isNotEmpty(ContractPartySupport.carrierPartyNames(contract))).toList();
|
||||
}
|
||||
|
||||
private void validateCarrier(Map<String, Object> dispatch, Map<Long, ContractManage> availableCarrierContracts) {
|
||||
@@ -598,11 +624,29 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
|
||||
}
|
||||
}
|
||||
String carrierName = string(dispatch, "carrierName");
|
||||
String carrierPartyId = string(dispatch, "carrierPartyId");
|
||||
Long carrierContractId = longValue(dispatch, "carrierContractId");
|
||||
ContractManage carrierContract = carrierContractId == null ? null : availableCarrierContracts.get(carrierContractId);
|
||||
if ("承运商".equals(carrierType) && (carrierContract == null
|
||||
|| !Objects.equals(carrierContract.getPartyB(), carrierName))) {
|
||||
throw new ServiceException("所选承运商不属于总单客户合同对应项目的有效承运商合同乙方");
|
||||
|| !ContractPartySupport.matchesCarrier(carrierContract, carrierName, carrierPartyId))) {
|
||||
throw new ServiceException("所选承运商不属于总单客户合同对应项目的有效承运商合同乙/丙方客商");
|
||||
}
|
||||
if ("承运商".equals(carrierType) && carrierContract != null) {
|
||||
Map<String, Object> selectedParty = ContractPartySupport.findCarrierParty(carrierContract, carrierPartyId, carrierName);
|
||||
if (selectedParty != null) {
|
||||
Object selectedName = selectedParty.get("customerName");
|
||||
Object selectedPartyId = selectedParty.get("id");
|
||||
Object selectedPartyRole = selectedParty.get("partyRole");
|
||||
if (selectedName != null) {
|
||||
dispatch.put("carrierName", String.valueOf(selectedName).trim());
|
||||
}
|
||||
if (selectedPartyId != null) {
|
||||
dispatch.put("carrierPartyId", String.valueOf(selectedPartyId).trim());
|
||||
}
|
||||
if (selectedPartyRole != null) {
|
||||
dispatch.put("carrierPartyRole", ContractPartySupport.normalizeRole(String.valueOf(selectedPartyRole)));
|
||||
}
|
||||
}
|
||||
}
|
||||
String transportType = string(dispatch, "transportType", "");
|
||||
boolean road = transportType.toLowerCase().contains("road") || transportType.contains("公路");
|
||||
|
||||
+2
-2
@@ -1397,8 +1397,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
// 结算组织.编码
|
||||
param.setSettleorgNumber(orgNumber);
|
||||
// 部门.编码:项目业务部门自身的金蝶编码(结算/申请/付款组织仍传上溯平台公司编码)
|
||||
param.setAwrpDepartmentNumber(kingdeeApBillSyncSupport.kingdeeDeptNumber(contract.getProjectId(), context));
|
||||
// 部门.编码:合同平台公司下业务部门配置的金蝶部门代码
|
||||
param.setAwrpDepartmentNumber(kingdeeApBillSyncSupport.kingdeeDeptNumber(contract, contract.getProjectId(), context));
|
||||
// 【必填】单据类型.编码
|
||||
param.setBilltypeNumber(bfiProperties.getPayApply().getBilltype());
|
||||
// 【必填】申请日期:yyyy-MM-dd HH:mm:ss(各接口统一格式)
|
||||
|
||||
+126
-14
@@ -72,6 +72,7 @@ import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.IProjectApplyService;
|
||||
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.wrapper.PreSettlementWrapper;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.system.feign.ISysClient;
|
||||
@@ -977,11 +978,12 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
detail.setBatchNo(source.getBatchNo());
|
||||
detail.setUnitPrice(source.getUnitPrice());
|
||||
detail.setFreightAmount(money(source.getFreightAmount()));
|
||||
detail.setFeeItemsJson(source.getFeeItemsJson());
|
||||
detail.setFeeItemsJson(sanitizeFeeItemsJson(source.getFeeItemsJson()));
|
||||
BigDecimal originalAmount = sourceOriginalAmount(source);
|
||||
BigDecimal settlementAmount = resolveSourceSettlementAmount(source, originalAmount);
|
||||
detail.setOriginalAmount(originalAmount);
|
||||
detail.setAdjustAmount(money(source.getTotalAmount()).subtract(originalAmount));
|
||||
detail.setSettlementAmountTax(money(source.getTotalAmount()));
|
||||
detail.setAdjustAmount(settlementAmount.subtract(originalAmount));
|
||||
detail.setSettlementAmountTax(settlementAmount);
|
||||
detail.setCurrency(Func.isEmpty(source.getCurrency()) ? LOCAL_CURRENCY : source.getCurrency());
|
||||
detail.setRemark(source.getRemark());
|
||||
Waybill waybill = source.getWaybillId() == null ? null : waybillService.getById(source.getWaybillId());
|
||||
@@ -1008,10 +1010,13 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
.eq(ReceivablePayableCargoFee::getDetailId, source.getId())
|
||||
.eq(ReceivablePayableCargoFee::getIsDeleted, 0));
|
||||
if (sourceFees.isEmpty() || sourceFees.stream().noneMatch(item -> item.getOriginalAmount() != null)) {
|
||||
return money(source.getTotalAmount());
|
||||
return resolveSourceSettlementAmount(source, money(source.getTotalAmount()));
|
||||
}
|
||||
return sourceFees.stream().map(ReceivablePayableCargoFee::getOriginalAmount)
|
||||
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (ReceivablePayableCargoFee sourceFee : sourceFees) {
|
||||
total = total.add(resolveCargoFeeAmount(sourceFee, sourceFee.getOriginalAmount()));
|
||||
}
|
||||
return money(total);
|
||||
}
|
||||
|
||||
private void copySourceFees(PreSettlementDetail detail, ReceivablePayableDetail source) {
|
||||
@@ -1031,10 +1036,11 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
fee.setMileage(source.getMileage());
|
||||
fee.setUnitPrice(source.getUnitPrice());
|
||||
fee.setFreightAmount(money(source.getFreightAmount()));
|
||||
fee.setFeeItemsJson(source.getFeeItemsJson());
|
||||
fee.setOriginalAmount(money(source.getTotalAmount()));
|
||||
fee.setFeeItemsJson(sanitizeFeeItemsJson(source.getFeeItemsJson()));
|
||||
BigDecimal amount = resolveSourceSettlementAmount(source, money(source.getTotalAmount()));
|
||||
fee.setOriginalAmount(amount);
|
||||
fee.setAdjustAmount(BigDecimal.ZERO.setScale(2));
|
||||
fee.setSettlementAmountTax(money(source.getTotalAmount()));
|
||||
fee.setSettlementAmountTax(amount);
|
||||
fee.setRemark(source.getRemark());
|
||||
detailFeeMapper.insert(fee);
|
||||
return;
|
||||
@@ -1045,6 +1051,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
fee.setId(null);
|
||||
fee.setPreSettlementDetailId(detail.getId());
|
||||
fee.setSourceFeeId(sourceFee.getId());
|
||||
fee.setFeeItemsJson(sanitizeFeeItemsJson(sourceFee.getFeeItemsJson()));
|
||||
// 历史应付费用可能没有 after_amount,不能在首次打开明细调整时被当成 0。
|
||||
BigDecimal settlementAmount = sourceFee.getAfterAmount() != null
|
||||
? money(sourceFee.getAfterAmount())
|
||||
@@ -1052,8 +1059,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
? money(source.getTotalAmount())
|
||||
: sourceFee.getOriginalAmount() != null
|
||||
? money(sourceFee.getOriginalAmount()) : BigDecimal.ZERO.setScale(2);
|
||||
settlementAmount = resolveCargoFeeAmount(sourceFee, settlementAmount);
|
||||
BigDecimal originalAmount = sourceFee.getOriginalAmount() == null
|
||||
? settlementAmount : money(sourceFee.getOriginalAmount());
|
||||
? settlementAmount : resolveCargoFeeAmount(sourceFee, sourceFee.getOriginalAmount());
|
||||
fee.setOriginalAmount(originalAmount);
|
||||
fee.setAdjustAmount(settlementAmount.subtract(originalAmount));
|
||||
fee.setSettlementAmountTax(settlementAmount);
|
||||
@@ -1070,14 +1078,23 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
.filter(row -> !Integer.valueOf(1).equals(row.getManualFlag()))
|
||||
.collect(Collectors.toMap(this::summaryKey, Function.identity(), (left, right) -> left));
|
||||
Map<String, BigDecimal> aggregates = new LinkedHashMap<>();
|
||||
List<Long> detailIds = listDetails(settlementId).stream().map(PreSettlementDetail::getId).toList();
|
||||
List<PreSettlementDetail> details = listDetails(settlementId);
|
||||
List<Long> detailIds = details.stream().map(PreSettlementDetail::getId).toList();
|
||||
List<PreSettlementDetailFee> feeRows = detailIds.isEmpty() ? List.of()
|
||||
: detailFeeMapper.selectList(Wrappers.<PreSettlementDetailFee>lambdaQuery()
|
||||
.in(PreSettlementDetailFee::getPreSettlementDetailId, detailIds)
|
||||
.eq(PreSettlementDetailFee::getIsDeleted, 0)
|
||||
.orderByAsc(PreSettlementDetailFee::getCreateTime));
|
||||
Set<Long> repairedDetailIds = new LinkedHashSet<>();
|
||||
for (PreSettlementDetailFee feeRow : feeRows) {
|
||||
boolean polluted = isPollutedFeeItemsJson(feeRow.getFeeItemsJson());
|
||||
Map<String, BigDecimal> feeItems = parseFeeItems(feeRow.getFeeItemsJson());
|
||||
if (polluted) {
|
||||
repairDetailFeeAmounts(feeRow, feeItems);
|
||||
if (feeRow.getPreSettlementDetailId() != null) {
|
||||
repairedDetailIds.add(feeRow.getPreSettlementDetailId());
|
||||
}
|
||||
}
|
||||
boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
|
||||
BigDecimal componentAmount = feeItems.values().stream().map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
@@ -1093,8 +1110,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
summaryKey(feeTypeMap.getOrDefault(feeItem,
|
||||
""), feeItem),
|
||||
money(amount), BigDecimal::add));
|
||||
BigDecimal residualAmount = money(money(feeRow.getSettlementAmountTax())
|
||||
.subtract(componentAmount));
|
||||
// 脏数据已在上面修复结算金额;正常明细才用 settlementAmountTax 与费用项差额补「其他费用」
|
||||
BigDecimal residualAmount = money(money(feeRow.getSettlementAmountTax()).subtract(componentAmount));
|
||||
if (residualAmount.signum() != 0) {
|
||||
aggregates.merge(
|
||||
summaryKey(feeTypeMap.getOrDefault("其他费用", ""), "其他费用"),
|
||||
@@ -1141,6 +1158,11 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
summaryFeeMapper.insert(row);
|
||||
}
|
||||
renumberSummaryFees(settlementId);
|
||||
if (!repairedDetailIds.isEmpty()) {
|
||||
details.stream()
|
||||
.filter(detail -> repairedDetailIds.contains(detail.getId()))
|
||||
.forEach(this::refreshDetail);
|
||||
}
|
||||
}
|
||||
|
||||
private void applySummaryRequest(Long settlementId, List<PreSettlementSaveRequest.SummaryFee> requestRows) {
|
||||
@@ -1729,7 +1751,29 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(json, Map.class);
|
||||
if (parsed instanceof Map<?, ?> map) {
|
||||
map.forEach((key, value) -> result.put(String.valueOf(key), decimal(value)));
|
||||
BigDecimal otherFeeAmount = BigDecimal.ZERO;
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
String key = String.valueOf(entry.getKey());
|
||||
Object value = entry.getValue();
|
||||
if (Func.isBlank(key)) {
|
||||
continue;
|
||||
}
|
||||
if ("otherFreightAmount".equals(key) || "otherFeeTotal".equals(key) || "otherAmount".equals(key)) {
|
||||
otherFeeAmount = otherFeeAmount.add(decimal(value));
|
||||
continue;
|
||||
}
|
||||
if (TransportBusinessSupport.isFreightJsonMetaKey(key)) {
|
||||
continue;
|
||||
}
|
||||
if (value instanceof Map<?, ?> || value instanceof java.util.Collection<?>) {
|
||||
continue;
|
||||
}
|
||||
result.put(key, decimal(value));
|
||||
}
|
||||
if (otherFeeAmount.signum() > 0 && result.keySet().stream()
|
||||
.noneMatch(name -> name != null && (name.contains("其它费") || name.contains("其他费")))) {
|
||||
result.put("其它费用", otherFeeAmount);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 历史脏数据不影响结算单展示。
|
||||
@@ -1737,6 +1781,74 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isPollutedFeeItemsJson(String json) {
|
||||
if (Func.isEmpty(json)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(json, Map.class);
|
||||
if (!(parsed instanceof Map<?, ?> map)) {
|
||||
return false;
|
||||
}
|
||||
return map.keySet().stream()
|
||||
.map(String::valueOf)
|
||||
.anyMatch(TransportBusinessSupport::isFreightJsonMetaKey);
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String sanitizeFeeItemsJson(String json) {
|
||||
Map<String, BigDecimal> feeItems = parseFeeItems(json);
|
||||
return JsonUtil.toJson(feeItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* 来源应付明细金额:若 fee_items_json 混入 freightJson 结构字段,按清洗后费用项重算,避免转预结算翻倍。
|
||||
*/
|
||||
private BigDecimal resolveSourceSettlementAmount(ReceivablePayableDetail source, BigDecimal fallback) {
|
||||
if (source == null) {
|
||||
return money(fallback);
|
||||
}
|
||||
if (!isPollutedFeeItemsJson(source.getFeeItemsJson())) {
|
||||
return money(fallback);
|
||||
}
|
||||
Map<String, BigDecimal> feeItems = parseFeeItems(source.getFeeItemsJson());
|
||||
boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
|
||||
BigDecimal feeItemTotal = feeItems.values().stream().map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return money(containsFreight ? feeItemTotal : money(source.getFreightAmount()).add(feeItemTotal));
|
||||
}
|
||||
|
||||
private BigDecimal resolveCargoFeeAmount(ReceivablePayableCargoFee sourceFee, BigDecimal fallback) {
|
||||
if (sourceFee == null) {
|
||||
return money(fallback);
|
||||
}
|
||||
if (!isPollutedFeeItemsJson(sourceFee.getFeeItemsJson())) {
|
||||
return money(fallback);
|
||||
}
|
||||
Map<String, BigDecimal> feeItems = parseFeeItems(sourceFee.getFeeItemsJson());
|
||||
boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
|
||||
BigDecimal feeItemTotal = feeItems.values().stream().map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return money(containsFreight ? feeItemTotal : money(sourceFee.getFreightAmount()).add(feeItemTotal));
|
||||
}
|
||||
|
||||
/** 重建合计时顺带修复已落库的脏费用行金额与 fee_items_json。 */
|
||||
private void repairDetailFeeAmounts(PreSettlementDetailFee feeRow, Map<String, BigDecimal> sanitizedFeeItems) {
|
||||
boolean containsFreight = sanitizedFeeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
|
||||
BigDecimal feeItemTotal = sanitizedFeeItems.values().stream().map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal corrected = money(containsFreight
|
||||
? feeItemTotal
|
||||
: money(feeRow.getFreightAmount()).add(feeItemTotal));
|
||||
feeRow.setFeeItemsJson(JsonUtil.toJson(sanitizedFeeItems));
|
||||
feeRow.setOriginalAmount(corrected);
|
||||
feeRow.setSettlementAmountTax(corrected);
|
||||
feeRow.setAdjustAmount(BigDecimal.ZERO.setScale(2));
|
||||
detailFeeMapper.updateById(feeRow);
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> normalizeFeeItems(Map<String, BigDecimal> feeItems) {
|
||||
Map<String, BigDecimal> result = new LinkedHashMap<>();
|
||||
if (feeItems == null) return result;
|
||||
|
||||
+16
-3
@@ -195,7 +195,17 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submitApproval(Long id) {
|
||||
ProjectApply projectApply = loadEditable(id, true);
|
||||
ProjectApply projectApply = loadExists(id);
|
||||
if (!canCurrentUserOperate(projectApply)) {
|
||||
throw new ServiceException("无权操作其他组织项目立项");
|
||||
}
|
||||
// 已在审批中则幂等成功,避免「先 MK 后本地」时 sync 已写成 reviewing 再被编辑校验拦住
|
||||
if (List.of(STATUS_REVIEWING, STATUS_CHANGE_REVIEWING).contains(projectApply.getApprovalStatus())) {
|
||||
return true;
|
||||
}
|
||||
if (!List.of(STATUS_DRAFT, STATUS_WITHDRAWN, STATUS_REJECTED, "change_rejected").contains(projectApply.getApprovalStatus())) {
|
||||
throw new ServiceException("当前状态不允许提交");
|
||||
}
|
||||
validateSubmit(projectApply);
|
||||
projectApply.setApprovalStatus(STATUS_REVIEWING);
|
||||
projectApply.setCurrentNode(resolveApprovalNode(projectApply));
|
||||
@@ -417,11 +427,13 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
applyUndertakeDeptContains(queryWrapper, projectApply.getUndertakeDeptId());
|
||||
}
|
||||
} else if (!Objects.equals(projectApply.getAllDept(), 1)) {
|
||||
// 非超管默认收口:业务部门、平台公司、创建人/经办人/负责人可见
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目立项");
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
queryWrapper.and(wrapper -> {
|
||||
if (currentDeptId != null) {
|
||||
wrapper.apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
|
||||
wrapper.eq(ProjectApply::getBusinessDeptId, currentDeptId)
|
||||
.or().apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
|
||||
}
|
||||
wrapper.or().eq(ProjectApply::getCreateUser, currentUserId)
|
||||
.or().eq(ProjectApply::getHandlerUserId, currentUserId)
|
||||
@@ -1091,7 +1103,8 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
}
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目立项");
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
return containsUndertakeDept(projectApply.getUndertakeDeptId(), currentDeptId)
|
||||
return Objects.equals(projectApply.getBusinessDeptId(), currentDeptId)
|
||||
|| containsUndertakeDept(projectApply.getUndertakeDeptId(), currentDeptId)
|
||||
|| Objects.equals(projectApply.getCreateUser(), currentUserId)
|
||||
|| Objects.equals(projectApply.getHandlerUserId(), currentUserId)
|
||||
|| Objects.equals(projectApply.getPrincipalUserId(), currentUserId);
|
||||
|
||||
+21
-3
@@ -170,7 +170,17 @@ public class ProjectChangeServiceImpl extends BaseServiceImpl<ProjectChangeMappe
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submitApproval(Long id) {
|
||||
ProjectChange projectChange = loadEditable(id);
|
||||
ProjectChange projectChange = loadExists(id);
|
||||
if (!canCurrentUserOperate(projectChange)) {
|
||||
throw new ServiceException("无权操作其他组织项目变更");
|
||||
}
|
||||
// 已在审批中则幂等成功,避免 MK 回写 reviewing 后再提交被编辑校验拦住
|
||||
if (Objects.equals(projectChange.getApprovalStatus(), STATUS_REVIEWING)) {
|
||||
return true;
|
||||
}
|
||||
if (!List.of(STATUS_DRAFT, STATUS_WITHDRAWN, STATUS_REJECTED).contains(projectChange.getApprovalStatus())) {
|
||||
throw new ServiceException("当前状态不允许提交");
|
||||
}
|
||||
validateSubmit(projectChange);
|
||||
projectChange.setApprovalStatus(STATUS_REVIEWING);
|
||||
projectChange.setCurrentNode(resolveApprovalNode(projectChange));
|
||||
@@ -256,11 +266,13 @@ public class ProjectChangeServiceImpl extends BaseServiceImpl<ProjectChangeMappe
|
||||
applyUndertakeDeptContains(queryWrapper, projectChange.getUndertakeDeptId());
|
||||
}
|
||||
} else if (!Objects.equals(projectChange.getAllDept(), 1)) {
|
||||
// 非超管默认收口:业务部门、平台公司、创建人/经办人/负责人可见
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目变更");
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
queryWrapper.and(wrapper -> {
|
||||
if (currentDeptId != null) {
|
||||
wrapper.apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
|
||||
wrapper.eq(ProjectChange::getBusinessDeptId, currentDeptId)
|
||||
.or().apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
|
||||
}
|
||||
wrapper.or().eq(ProjectChange::getCreateUser, currentUserId)
|
||||
.or().eq(ProjectChange::getHandlerUserId, currentUserId)
|
||||
@@ -520,7 +532,13 @@ public class ProjectChangeServiceImpl extends BaseServiceImpl<ProjectChangeMappe
|
||||
return true;
|
||||
}
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目变更");
|
||||
if (currentDeptId == null || Func.isBlank(projectChange.getUndertakeDeptId())) {
|
||||
if (currentDeptId == null) {
|
||||
return false;
|
||||
}
|
||||
if (Objects.equals(projectChange.getBusinessDeptId(), currentDeptId)) {
|
||||
return true;
|
||||
}
|
||||
if (Func.isBlank(projectChange.getUndertakeDeptId())) {
|
||||
return false;
|
||||
}
|
||||
return List.of(projectChange.getUndertakeDeptId().split(",")).stream()
|
||||
|
||||
+190
-17
@@ -61,6 +61,8 @@ import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.ContractPartySupport;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
import org.springblade.transport.wrapper.ReceivablePayableDetailWrapper;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
@@ -478,6 +480,8 @@ public class ReceivablePayableDetailServiceImpl
|
||||
? FEE_SOURCE_MANUAL : FEE_SOURCE_AUTO);
|
||||
}
|
||||
});
|
||||
// 历史脏数据:fee_items_json 误含 freightJson 结构字段导致结算金额翻倍,打开详情时自动修复
|
||||
repairPollutedFreightFeeItems(detail, rows);
|
||||
ReceivablePayableFeeDetailVO result = buildFeeDetail(rows);
|
||||
// 只展示本明细已生成的费用项。合同上其他计费方案的费用项不能叠到当前明细。
|
||||
return result;
|
||||
@@ -563,6 +567,12 @@ public class ReceivablePayableDetailServiceImpl
|
||||
item.put("id", contract.getId());
|
||||
item.put("contractNo", contract.getContractNo());
|
||||
item.put("contractName", contract.getContractName());
|
||||
item.put("partyA", contract.getPartyA());
|
||||
item.put("partyB", contract.getPartyB());
|
||||
item.put("partyC", contract.getPartyC());
|
||||
item.put("partyAJson", contract.getPartyAJson());
|
||||
item.put("partyBJson", contract.getPartyBJson());
|
||||
item.put("partyCJson", contract.getPartyCJson());
|
||||
item.put("billingPlanJson", contract.getBillingPlanJson());
|
||||
return item;
|
||||
}).toList();
|
||||
@@ -1255,9 +1265,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
&& !Objects.equals(waybill.getProjectId(), contract.getProjectId())) {
|
||||
return false;
|
||||
}
|
||||
String carrierName = trimToNull(waybill.getCarrierName());
|
||||
String partyB = trimToNull(contract.getPartyB());
|
||||
return carrierName != null && carrierName.equals(partyB);
|
||||
return ContractPartySupport.matchesCarrier(contract, waybill.getCarrierName(), waybill.getCarrierPartyId());
|
||||
}
|
||||
|
||||
private ContractManage findCarrierContract(Waybill waybill) {
|
||||
@@ -1265,20 +1273,50 @@ public class ReceivablePayableDetailServiceImpl
|
||||
if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId()) || carrierName == null) {
|
||||
return null;
|
||||
}
|
||||
// 承运商合同甲方应与运单绑定的客户合同甲方一致,乙方必须是运单选择的承运商。
|
||||
// 承运商合同甲方应与运单绑定的客户合同乙方(我方)一致,乙/丙方必须包含运单选择的承运商。
|
||||
ContractManage customerContract = contractManageService.getById(waybill.getContractId());
|
||||
if (customerContract == null || Func.isEmpty(customerContract.getPartyA())) {
|
||||
if (customerContract == null || Func.isEmpty(customerContract.getPartyB())) {
|
||||
return null;
|
||||
}
|
||||
return contractManageService.getOne(Wrappers.<ContractManage>lambdaQuery()
|
||||
List<ContractManage> contracts = contractManageService.list(Wrappers.<ContractManage>lambdaQuery()
|
||||
.eq(ContractManage::getIsDeleted, 0)
|
||||
.eq(ContractManage::getProjectId, waybill.getProjectId())
|
||||
.eq(ContractManage::getPartyA, customerContract.getPartyA())
|
||||
.eq(ContractManage::getPartyB, carrierName)
|
||||
.eq(ContractManage::getContractCategory, "承运商合同")
|
||||
.and(wrapper -> wrapper.isNull(ContractManage::getContractStage)
|
||||
.or().ne(ContractManage::getContractStage, "terminated"))
|
||||
.orderByDesc(ContractManage::getCreateTime), false);
|
||||
.orderByDesc(ContractManage::getCreateTime));
|
||||
Set<String> customerPartyBs = splitPartyNames(customerContract.getPartyBJson(), customerContract.getPartyB());
|
||||
for (ContractManage contract : contracts) {
|
||||
Set<String> partyANames = splitPartyNames(contract.getPartyAJson(), contract.getPartyA());
|
||||
boolean partyAMatched = partyANames.isEmpty() || partyANames.stream().anyMatch(customerPartyBs::contains)
|
||||
|| customerPartyBs.contains(trimToNull(contract.getPartyA()));
|
||||
if (!partyAMatched) {
|
||||
continue;
|
||||
}
|
||||
if (ContractPartySupport.matchesCarrier(contract, carrierName, waybill.getCarrierPartyId())) {
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Set<String> splitPartyNames(String partyJson, String fallback) {
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
for (Map<String, Object> row : ContractPartySupport.parsePartyRows(partyJson)) {
|
||||
Object customerName = row.get("customerName");
|
||||
if (customerName != null && Func.isNotEmpty(String.valueOf(customerName).trim())) {
|
||||
names.add(String.valueOf(customerName).trim());
|
||||
}
|
||||
}
|
||||
if (names.isEmpty() && Func.isNotEmpty(fallback)) {
|
||||
for (String part : fallback.split("[、,]")) {
|
||||
String name = trimToNull(part);
|
||||
if (name != null) {
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
@@ -2005,12 +2043,15 @@ public class ReceivablePayableDetailServiceImpl
|
||||
private ReceivablePayableCargoFee buildCargoFee(Long detailId, Waybill waybill) {
|
||||
BigDecimal quantity = money(waybill.getQuantity());
|
||||
BigDecimal unitPrice = money(waybill.getUnitPrice());
|
||||
BigDecimal freightAmount = quantity.multiply(unitPrice).setScale(2, RoundingMode.HALF_UP);
|
||||
BigDecimal otherFeeAmount = money(waybill.getOtherFeeTotal());
|
||||
Map<String, Object> feeItems = parseMap(waybill.getFreightJson());
|
||||
if (feeItems.isEmpty() && otherFeeAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
feeItems.put("其它费用", otherFeeAmount);
|
||||
// 优先取运单 freightJson 中的运费;没有再回退数量×单价,避免与费用项重复累加
|
||||
BigDecimal freightAmount = waybillFreight(waybill);
|
||||
if (freightAmount == null) {
|
||||
freightAmount = quantity.multiply(unitPrice).setScale(2, RoundingMode.HALF_UP);
|
||||
} else {
|
||||
freightAmount = money(freightAmount);
|
||||
}
|
||||
// freightJson 是币种/运费合计等结构体,不能整包当作费用项目 Map
|
||||
Map<String, Object> feeItems = resolveWaybillFeeItems(waybill);
|
||||
BigDecimal feeItemTotal = feeItems.values().stream().map(this::decimal).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal total = freightAmount.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
|
||||
ReceivablePayableCargoFee cargoFee = new ReceivablePayableCargoFee();
|
||||
@@ -2052,13 +2093,31 @@ public class ReceivablePayableDetailServiceImpl
|
||||
*/
|
||||
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId,
|
||||
boolean matchOnly, boolean strictPlan) {
|
||||
List<Map<String, Object>> plans = parseList(contract == null ? null : contract.getBillingPlanJson());
|
||||
List<Map<String, Object>> plans = billingPlansForWaybill(contract, waybill);
|
||||
Map<String, Object> plan = "__matched__".equals(planId)
|
||||
? resolveDefaultBillingPlan(plans, waybill.getTransportType())
|
||||
: resolveBillingPlan(plans, planId);
|
||||
return calculatedFees(waybill, plan, matchOnly, strictPlan);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按运单客商过滤合同计费方案:承运商合同用承运客商,客户合同用客户名称。
|
||||
*/
|
||||
private List<Map<String, Object>> billingPlansForWaybill(ContractManage contract, Waybill waybill) {
|
||||
if (contract == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (waybill == null) {
|
||||
return parseList(contract.getBillingPlanJson());
|
||||
}
|
||||
if ("承运商合同".equals(contract.getContractCategory())) {
|
||||
return ContractPartySupport.filterBillingPlans(
|
||||
contract.getBillingPlanJson(), null, waybill.getCarrierName(), waybill.getCarrierPartyId());
|
||||
}
|
||||
return ContractPartySupport.filterBillingPlans(
|
||||
contract.getBillingPlanJson(), null, waybill.getCustomerName(), null);
|
||||
}
|
||||
|
||||
private List<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, Map<String, Object> plan,
|
||||
boolean matchOnly) {
|
||||
return calculatedFees(waybill, plan, matchOnly, false);
|
||||
@@ -2513,7 +2572,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
if (Func.isEmpty(vo.getDataSource())) {
|
||||
vo.setDataSource(isManualFee(row) ? FEE_SOURCE_MANUAL : FEE_SOURCE_AUTO);
|
||||
}
|
||||
Map<String, Object> feeItems = parseMap(row.getFeeItemsJson());
|
||||
Map<String, Object> feeItems = sanitizeFeeItems(parseMap(row.getFeeItemsJson()));
|
||||
feeItemNames.addAll(feeItems.keySet());
|
||||
vo.setFeeItems(feeItems);
|
||||
vo.setTransportQuantityText(formatQuantity(row.getTransportQuantity(), row.getQuantityUnit()));
|
||||
@@ -2530,6 +2589,52 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复误把运单 freightJson 结构字段写入 fee_items_json 的历史费用行,
|
||||
* 并按「运输费字段 + 清洗后费用项」重算结算金额,消除翻倍。
|
||||
*/
|
||||
private void repairPollutedFreightFeeItems(ReceivablePayableDetail detail, List<ReceivablePayableCargoFee> rows) {
|
||||
if (detail == null || rows == null || rows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
boolean repaired = false;
|
||||
for (ReceivablePayableCargoFee row : rows) {
|
||||
Map<String, Object> rawFeeItems = parseMap(row.getFeeItemsJson());
|
||||
if (!isPollutedFreightFeeItems(rawFeeItems)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> sanitized = sanitizeFeeItems(rawFeeItems);
|
||||
Map<String, BigDecimal> feeItemAmounts = new LinkedHashMap<>();
|
||||
sanitized.forEach((name, value) -> feeItemAmounts.put(name, decimal(value)));
|
||||
BigDecimal afterAmount = adjustedAfterAmount(money(row.getFreightAmount()), feeItemAmounts);
|
||||
row.setFeeItemsJson(JsonUtil.toJson(sanitized));
|
||||
row.setOriginalAmount(afterAmount);
|
||||
row.setAfterAmount(afterAmount);
|
||||
row.setAdjustAmount(BigDecimal.ZERO);
|
||||
cargoFeeMapper.updateById(row);
|
||||
repaired = true;
|
||||
}
|
||||
if (!repaired) {
|
||||
return;
|
||||
}
|
||||
BigDecimal freight = rows.stream().map(fee -> money(fee.getFreightAmount()))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal total = rows.stream().map(fee -> money(fee.getAfterAmount()))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
detail.setFreightAmount(freight);
|
||||
detail.setOtherFeeAmount(total.subtract(freight).setScale(2, RoundingMode.HALF_UP));
|
||||
detail.setTotalAmount(total);
|
||||
detail.setFeeItemsJson(aggregateFeeItemsJson(rows));
|
||||
updateById(detail);
|
||||
}
|
||||
|
||||
private boolean isPollutedFreightFeeItems(Map<String, Object> feeItems) {
|
||||
if (feeItems == null || feeItems.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return feeItems.keySet().stream().anyMatch(TransportBusinessSupport::isFreightJsonMetaKey);
|
||||
}
|
||||
|
||||
private List<String> contractFeeItemNames(Long contractId) {
|
||||
if (contractId == null) return List.of();
|
||||
ContractManage contract = contractManageService.getById(contractId);
|
||||
@@ -3000,7 +3105,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
|
||||
private String aggregateFeeItemsJson(List<ReceivablePayableCargoFee> fees) {
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
fees.forEach(fee -> parseMap(fee.getFeeItemsJson()).forEach((name, value) ->
|
||||
fees.forEach(fee -> sanitizeFeeItems(parseMap(fee.getFeeItemsJson())).forEach((name, value) ->
|
||||
feeItems.merge(name, decimal(value), BigDecimal::add)));
|
||||
return JsonUtil.toJson(feeItems);
|
||||
}
|
||||
@@ -3456,6 +3561,74 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从运单 freightJson / otherFeeTotal 解析费用项目(名称→金额)。
|
||||
* 仅保留真正的费用项;结构字段(currency、totalFreightAmount 等)不会写入。
|
||||
*/
|
||||
private Map<String, Object> resolveWaybillFeeItems(Waybill waybill) {
|
||||
Map<String, Object> feeItems = new LinkedHashMap<>();
|
||||
Map<String, Object> freight = waybillFreightMap(waybill);
|
||||
freight.forEach((key, value) -> {
|
||||
if (Func.isBlank(key) || TransportBusinessSupport.isFreightJsonMetaKey(key)) {
|
||||
return;
|
||||
}
|
||||
if (value instanceof Map<?, ?> || value instanceof Collection<?>) {
|
||||
return;
|
||||
}
|
||||
// 兼容历史「费用名→金额」扁平 JSON(键名通常含「费」)
|
||||
if (!key.contains("费")) {
|
||||
return;
|
||||
}
|
||||
feeItems.put(key, decimal(value));
|
||||
});
|
||||
BigDecimal otherFeeAmount = money(waybillOtherFee(waybill));
|
||||
if (otherFeeAmount.compareTo(BigDecimal.ZERO) > 0 && !containsOtherFeeItem(feeItems.keySet())) {
|
||||
feeItems.put("其它费用", otherFeeAmount);
|
||||
}
|
||||
return feeItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗已落库的 fee_items_json:过滤误写入的 freightJson 结构字段,
|
||||
* 并将 otherFreightAmount / otherFeeTotal 归一为「其它费用」。
|
||||
*/
|
||||
private Map<String, Object> sanitizeFeeItems(Map<String, Object> raw) {
|
||||
Map<String, Object> feeItems = new LinkedHashMap<>();
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return feeItems;
|
||||
}
|
||||
BigDecimal otherFeeAmount = BigDecimal.ZERO;
|
||||
for (Map.Entry<String, Object> entry : raw.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
if (Func.isBlank(key)) {
|
||||
continue;
|
||||
}
|
||||
if ("otherFreightAmount".equals(key) || "otherFeeTotal".equals(key) || "otherAmount".equals(key)) {
|
||||
otherFeeAmount = otherFeeAmount.add(decimal(value));
|
||||
continue;
|
||||
}
|
||||
if (TransportBusinessSupport.isFreightJsonMetaKey(key)) {
|
||||
continue;
|
||||
}
|
||||
if (value instanceof Map<?, ?> || value instanceof Collection<?>) {
|
||||
continue;
|
||||
}
|
||||
feeItems.put(key, decimal(value));
|
||||
}
|
||||
if (otherFeeAmount.compareTo(BigDecimal.ZERO) > 0 && !containsOtherFeeItem(feeItems.keySet())) {
|
||||
feeItems.put("其它费用", otherFeeAmount);
|
||||
}
|
||||
return feeItems;
|
||||
}
|
||||
|
||||
private boolean containsOtherFeeItem(Collection<String> names) {
|
||||
if (names == null || names.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return names.stream().anyMatch(name -> name != null && (name.contains("其它费") || name.contains("其他费")));
|
||||
}
|
||||
|
||||
private Map<String, Object> parseMap(String json) {
|
||||
if (Func.isEmpty(json)) {
|
||||
return new LinkedHashMap<>();
|
||||
|
||||
+8
-1
@@ -242,10 +242,15 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
+ "]尚未推送金蝶,请先执行「推送金蝶」");
|
||||
}
|
||||
// 冲销硬闸门:仅当金蝶侧原应付/应收单已被冲销(部分冲销放行)才允许以新单号重推,避免重复生成财务单据
|
||||
if (!this.kingdeeWrittenOff(original)) {
|
||||
boolean writtenOff = this.kingdeeWrittenOff(original);
|
||||
// 闸门查询即金蝶实时冲销状态,回写供结算单列表展示(查到未冲销同样回写,拦截提示后列表即同步)
|
||||
formalSettlementService.refreshKingdeeWriteOff(original.getId(), writtenOff);
|
||||
if (!writtenOff) {
|
||||
throw new ServiceException("结算单[" + Func.toStr(original.getFormalSettlementNo())
|
||||
+ "]的原应付/应收单尚未在金蝶冲销,禁止重推");
|
||||
}
|
||||
// 闸门查询即金蝶实时冲销状态,回写供结算单列表展示
|
||||
formalSettlementService.refreshKingdeeWriteOff(original.getId(), true);
|
||||
// 幂等:同一原单已克隆过新单则复用(重试从推送步骤续上),避免重复建单
|
||||
FormalSettlement clone = existingClone(original.getFormalSettlementNo());
|
||||
if (clone == null) {
|
||||
@@ -302,6 +307,8 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
result.put("syncStatus", KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
|
||||
try {
|
||||
boolean writtenOff = this.kingdeeWrittenOff(settlement);
|
||||
// 预检查询即金蝶实时冲销状态,顺带回写供结算单列表展示
|
||||
formalSettlementService.refreshKingdeeWriteOff(settlement.getId(), writtenOff);
|
||||
result.put("writtenOff", writtenOff);
|
||||
result.put("message", writtenOff
|
||||
? "该结算单已推送金蝶且旧单已冲销,审批后可直接重新推送新单"
|
||||
|
||||
+5
-1
@@ -195,7 +195,11 @@ public class ShippingTemplateServiceImpl extends BaseServiceImpl<ShippingTemplat
|
||||
TransportBusinessSupport.validateAllDept(shippingTemplate.getAllDept(), "发货模板");
|
||||
LambdaQueryWrapper<ShippingTemplate> queryWrapper = Wrappers.<ShippingTemplate>lambdaQuery().eq(ShippingTemplate::getIsDeleted, 0);
|
||||
if (!Objects.equals(shippingTemplate.getAllDept(), 1)) {
|
||||
queryWrapper.eq(ShippingTemplate::getDeptId, TransportBusinessSupport.currentDeptId("发货模板"));
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("发货模板");
|
||||
queryWrapper.and(wrapper -> {
|
||||
wrapper.eq(ShippingTemplate::getDeptId, currentDeptId);
|
||||
TransportBusinessSupport.orProjectVisibleByBusinessDept(wrapper, List.of(currentDeptId));
|
||||
});
|
||||
} else if (Func.isNotEmpty(shippingTemplate.getDeptId())) {
|
||||
queryWrapper.eq(ShippingTemplate::getDeptId, shippingTemplate.getDeptId());
|
||||
}
|
||||
|
||||
+5
-1
@@ -190,7 +190,11 @@ public class TemporaryCreditLimitServiceImpl extends BaseServiceImpl<TemporaryCr
|
||||
queryWrapper.eq(TemporaryCreditLimit::getApplyDeptId, temporaryCreditLimit.getApplyDeptId());
|
||||
}
|
||||
} else if (!Objects.equals(temporaryCreditLimit.getAllDept(), 1)) {
|
||||
queryWrapper.eq(TemporaryCreditLimit::getApplyDeptId, TransportBusinessSupport.currentDeptId("临时额度管理"));
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("临时额度管理");
|
||||
queryWrapper.and(wrapper -> {
|
||||
wrapper.eq(TemporaryCreditLimit::getApplyDeptId, currentDeptId);
|
||||
TransportBusinessSupport.orProjectVisibleByBusinessDept(wrapper, List.of(currentDeptId));
|
||||
});
|
||||
} else if (Func.isNotEmpty(temporaryCreditLimit.getApplyDeptId())) {
|
||||
queryWrapper.eq(TemporaryCreditLimit::getApplyDeptId, temporaryCreditLimit.getApplyDeptId());
|
||||
}
|
||||
|
||||
+6
-3
@@ -701,9 +701,12 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
LambdaQueryWrapper<TransportPlan> queryWrapper = Wrappers.<TransportPlan>lambdaQuery().eq(TransportPlan::getIsDeleted, 0);
|
||||
if (!Objects.equals(transportPlan.getAllDept(), 1)) {
|
||||
Long currentDeptId = TransportBusinessSupport.currentDeptId("运输计划");
|
||||
// dept_id 为空的历史数据按创建组织归入当前组织
|
||||
queryWrapper.and(wrapper -> wrapper.eq(TransportPlan::getDeptId, currentDeptId)
|
||||
.or(item -> item.isNull(TransportPlan::getDeptId).eq(TransportPlan::getCreateDept, currentDeptId)));
|
||||
// dept_id 为空的历史数据按创建组织归入当前组织;业务部门可见其项目下的计划
|
||||
queryWrapper.and(wrapper -> {
|
||||
wrapper.eq(TransportPlan::getDeptId, currentDeptId)
|
||||
.or(item -> item.isNull(TransportPlan::getDeptId).eq(TransportPlan::getCreateDept, currentDeptId));
|
||||
TransportBusinessSupport.orProjectVisibleByBusinessDept(wrapper, List.of(currentDeptId));
|
||||
});
|
||||
} else if (Func.isNotEmpty(transportPlan.getDeptId())) {
|
||||
queryWrapper.eq(TransportPlan::getDeptId, transportPlan.getDeptId());
|
||||
}
|
||||
|
||||
+3
-3
@@ -71,7 +71,6 @@ public class TransportVehicleServiceImpl extends BaseServiceImpl<TransportVehicl
|
||||
private static final int REMARK_MAX_LENGTH = 200;
|
||||
private static final int DEFAULT_ENABLED_STATUS = 1;
|
||||
private static final int DEFAULT_FALSE = 0;
|
||||
private static final int CERTIFICATION_PENDING = 0;
|
||||
private static final int CERTIFICATION_APPROVED = 1;
|
||||
private static final int CERTIFICATION_REJECTED = 2;
|
||||
|
||||
@@ -133,8 +132,9 @@ public class TransportVehicleServiceImpl extends BaseServiceImpl<TransportVehicl
|
||||
prepare(vehicle);
|
||||
validate(vehicle);
|
||||
checkUniquePlateNo(vehicle);
|
||||
vehicle.setCertificationStatus(CERTIFICATION_PENDING);
|
||||
vehicle.setCertificationRejectReason(null);
|
||||
// 保存档案不碰认证状态:认证状态与生效快照只由认证审核流水维护。
|
||||
// 这里曾强制置 certification_status = 0(审核中),于是管理端每存一次档案,
|
||||
// 已认证车辆就被改回「审核中」,而审核页并没有对应的待审记录——一个退不出去的死状态。
|
||||
return saveOrUpdate(vehicle);
|
||||
}
|
||||
|
||||
|
||||
+21
-17
@@ -389,22 +389,22 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 运次可见范围:配载段按配载单的司机手机号收口,未配载段按运单的司机手机号收口。
|
||||
* 运次可见范围:已配载段按配载单的 driver_id 收口,未配载段按运单的 driver_id 收口。
|
||||
* <p>
|
||||
* 手机号允许带空格,与运单列表的收口判据一致。
|
||||
* 归属判据只有司机档案 ID(null 或非正数视为无主),driver_phone 不参与判定。
|
||||
* 无主的运次由 {@link #scoped} 在外层挡掉。
|
||||
*/
|
||||
private void applyScope(LambdaQueryWrapper<Waybill> wrapper, TripQueryVO condition) {
|
||||
if (condition.isAllDrivers()) {
|
||||
return;
|
||||
}
|
||||
String phone = Func.toStr(condition.getDriverPhone(), "").trim();
|
||||
Long driverId = condition.getDriverId();
|
||||
wrapper.and(scope -> scope
|
||||
.apply("(IFNULL(loading_no, '') <> '' AND loading_no IN (SELECT lm.loading_no FROM blade_loading_manage lm"
|
||||
+ " WHERE lm.is_deleted = 0 AND lm.driver_phone = {0}))", phone)
|
||||
+ " WHERE lm.is_deleted = 0 AND lm.driver_id = {0}))", driverId)
|
||||
.or(lone -> lone
|
||||
.apply("IFNULL(loading_no, '') = ''")
|
||||
.and(row -> row.eq(Waybill::getDriverPhone, phone)
|
||||
.or().apply("REPLACE(IFNULL(driver_phone, ''), ' ', '') = {0}", phone))));
|
||||
.and(row -> row.eq(Waybill::getDriverId, driverId))));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -786,7 +786,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按运次主键取配载单:主键命中配载单即配载运次,再按司机收口
|
||||
* 按运次主键取配载单:主键命中配载单即配载运次,再按配载单的司机档案 ID 收口
|
||||
*/
|
||||
private LoadingManage visibleLoading(Long id, TripQueryVO condition) {
|
||||
LoadingManage loading = loadingManageService.getById(id);
|
||||
@@ -796,7 +796,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
if (condition.isAllDrivers()) {
|
||||
return loading;
|
||||
}
|
||||
return samePhone(loading.getDriverPhone(), condition.getDriverPhone()) ? loading : null;
|
||||
return ownedBy(loading.getDriverId(), condition.getDriverId()) ? loading : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -813,19 +813,23 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
if (condition.isAllDrivers()) {
|
||||
return waybill;
|
||||
}
|
||||
return samePhone(waybill.getDriverPhone(), condition.getDriverPhone()) ? waybill : null;
|
||||
return ownedBy(waybill.getDriverId(), condition.getDriverId()) ? waybill : null;
|
||||
}
|
||||
|
||||
/** 收口条件是否成立:管理端不收口;司机端必须给出有效的档案 ID,缺省一律拒绝 */
|
||||
private boolean scoped(TripQueryVO condition) {
|
||||
return condition.isAllDrivers() || Func.isNotEmpty(Func.toStr(condition.getDriverPhone(), "").trim());
|
||||
Long driverId = condition.getDriverId();
|
||||
return condition.isAllDrivers() || (driverId != null && driverId > 0);
|
||||
}
|
||||
|
||||
private boolean samePhone(String left, String right) {
|
||||
String expected = Func.toStr(right, "").trim().replace(" ", "");
|
||||
if (Func.isEmpty(expected)) {
|
||||
return false;
|
||||
}
|
||||
return expected.equals(Func.toStr(left, "").trim().replace(" ", ""));
|
||||
/**
|
||||
* 归属判据:行的司机档案 ID 与收口条件相等。
|
||||
* <p>
|
||||
* 条件侧缺省(空、0、负数)一律不可见——没有有效档案 ID 就等于没有收口条件,
|
||||
* 此时任何行都不能算作「本人」,否则业务主键未回填的历史数据会把运次漏给所有人。
|
||||
*/
|
||||
private boolean ownedBy(Long ownerId, Long expectedId) {
|
||||
return expectedId != null && expectedId > 0 && expectedId.equals(ownerId);
|
||||
}
|
||||
|
||||
private TripQueryVO condition(TripQueryVO query) {
|
||||
@@ -835,7 +839,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
/** 去掉 Tab 条件、其余照搬:统计四档时不按某一档收窄 */
|
||||
private TripQueryVO withoutTab(TripQueryVO source) {
|
||||
TripQueryVO copy = new TripQueryVO();
|
||||
copy.setDriverPhone(source.getDriverPhone());
|
||||
copy.setDriverId(source.getDriverId());
|
||||
copy.setAllDrivers(source.isAllDrivers());
|
||||
copy.setDeptIds(source.getDeptIds());
|
||||
copy.setProjectIds(source.getProjectIds());
|
||||
|
||||
+27
-1
@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.common.excel.ImportFailureExcelUtil;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
@@ -41,6 +42,7 @@ import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportRowErrorVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProjectApplyService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
@@ -68,6 +70,7 @@ import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 运单批次服务实现。 */
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImportBatchMapper, WaybillImportBatch> implements IWaybillImportBatchService {
|
||||
@@ -101,6 +104,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
private final IReceivablePayableDetailService receivablePayableDetailService;
|
||||
private final ILoadingManageService loadingManageService;
|
||||
private final ISysClient sysClient;
|
||||
private final IDriverService driverService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -207,7 +211,9 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
List<Waybill> rowWaybills = new ArrayList<>();
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
try {
|
||||
rowWaybills.add(buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft));
|
||||
Waybill builtWaybill = buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft);
|
||||
fillDriverOwnership(builtWaybill, index + 1);
|
||||
rowWaybills.add(builtWaybill);
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
|
||||
}
|
||||
@@ -485,6 +491,26 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
return id == null || id <= 0L ? null : id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 补齐导入运单的司机档案 ID:行内有(老系统导出)就用,否则按手机号解析。
|
||||
* 解析不到不阻断导入,只留行级 WARN:承运商司机与未登录过小程序的司机本来就没有档案。
|
||||
*/
|
||||
private void fillDriverOwnership(Waybill waybill, int rowNo) {
|
||||
Long driverId = waybill.getDriverId();
|
||||
if (driverId != null && driverId > 0) {
|
||||
return;
|
||||
}
|
||||
// 解析入口只返回 null 或唯一命中的档案 ID,不需要再按哨兵归一
|
||||
Long resolvedDriverId = driverService.resolveDriverIdByMobile(waybill.getTenantId(), waybill.getDriverPhone());
|
||||
if (resolvedDriverId != null) {
|
||||
waybill.setDriverId(resolvedDriverId);
|
||||
return;
|
||||
}
|
||||
waybill.setDriverId(null);
|
||||
log.warn("第{}行 运单 {} 的司机手机号未唯一匹配司机档案,driver_id 留空(不阻断导入)",
|
||||
rowNo, waybill.getWaybillNo());
|
||||
}
|
||||
|
||||
private Waybill buildWaybill(Map<String, Object> row, WaybillImportBatch batch, Long carrierContractId, boolean draft) {
|
||||
if (row == null) throw new ServiceException("运单数据不正确");
|
||||
Waybill waybill = new Waybill();
|
||||
|
||||
+91
-29
@@ -22,6 +22,7 @@
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import cn.hutool.core.util.DesensitizedUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
@@ -61,12 +62,14 @@ import org.springblade.transport.pojo.vo.WaybillPunchPhotoVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchRecordItemVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.IProjectApplyService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.ContractPartySupport;
|
||||
import org.springblade.transport.support.LiveProcessConfigSupport;
|
||||
import org.springblade.transport.support.LoadableAdmissionSupport;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
@@ -110,6 +113,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
@jakarta.annotation.Resource
|
||||
private ILoadingManageService loadingManageService;
|
||||
|
||||
@jakarta.annotation.Resource
|
||||
private IDriverService driverService;
|
||||
|
||||
@jakarta.annotation.Resource
|
||||
private IProcessConfigService processConfigService;
|
||||
|
||||
@@ -147,7 +153,8 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
|
||||
@Override
|
||||
public WaybillVO detail(Long id) {
|
||||
WaybillVO result = WaybillWrapper.build().entityVO(syncDriverAcceptState(loadEditable(id, false)));
|
||||
Waybill loaded = syncDriverAcceptState(loadEditable(id, false));
|
||||
WaybillVO result = WaybillWrapper.build().entityVO(loaded);
|
||||
fillCustomerNameFromContract(result);
|
||||
fillCarrierContractName(List.of(result));
|
||||
fillMileageMaintainable(List.of(result));
|
||||
@@ -1323,18 +1330,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
LambdaQueryWrapper<Waybill> queryWrapper;
|
||||
if (Func.isNotEmpty(idList)) {
|
||||
queryWrapper = Wrappers.<Waybill>lambdaQuery().eq(Waybill::getIsDeleted, 0);
|
||||
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(
|
||||
waybill.getAllDept(), waybill.getDeptId(), "运单管理", TransportBusinessSupport.MENU_WAYBILL_MANAGE);
|
||||
if (Func.isNotEmpty(deptIdList)) {
|
||||
if (deptIdList.size() == 1) {
|
||||
queryWrapper.eq(Waybill::getDeptId, deptIdList.get(0));
|
||||
} else {
|
||||
queryWrapper.in(Waybill::getDeptId, deptIdList);
|
||||
}
|
||||
}
|
||||
if (TransportBusinessSupport.isOwnCreateScope(TransportBusinessSupport.MENU_WAYBILL_MANAGE)) {
|
||||
queryWrapper.eq(Waybill::getCreateUser, AuthUtil.getUserId());
|
||||
}
|
||||
applyAccountScope(queryWrapper, waybill.getAllDept(), waybill.getDeptId());
|
||||
queryWrapper.in(Waybill::getId, idList).orderByDesc(Waybill::getCreateTime);
|
||||
} else {
|
||||
queryWrapper = buildQuery(waybill);
|
||||
@@ -1392,7 +1388,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public WaybillVO copy(Long id) {
|
||||
Waybill source = loadEditable(id, true);
|
||||
Waybill source = loadEditable(id, false);
|
||||
Waybill target = new Waybill();
|
||||
target.setProjectId(source.getProjectId());
|
||||
target.setProjectName(source.getProjectName());
|
||||
@@ -1610,7 +1606,18 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
TransportBusinessSupport.validateRequired(driverPhone, "手机号不能为空");
|
||||
TransportBusinessSupport.validateRequired(vehicleNo, "车牌号不能为空");
|
||||
|
||||
waybill.setDriverId(request.getDriverId());
|
||||
// 归属主键是档案 ID,手机号只是派单时的号码快照:调用方带了档案 ID 就用它,
|
||||
// 否则按手机号解析;解析入口只返回 null 或唯一命中的档案 ID,承运商司机、未建档司机
|
||||
// 解析为空,派单照旧成功,落空时留 WARN
|
||||
Long driverId = request.getDriverId();
|
||||
if (driverId == null || driverId <= 0) {
|
||||
driverId = driverService.resolveDriverIdByMobile(waybill.getTenantId(), driverPhone);
|
||||
if (driverId == null) {
|
||||
log.warn("运单 {} 的司机手机号未唯一匹配司机档案,driver_id 落空(不阻断派单)。租户={} 手机号={}",
|
||||
waybill.getWaybillNo(), waybill.getTenantId(), DesensitizedUtil.mobilePhone(driverPhone));
|
||||
}
|
||||
}
|
||||
waybill.setDriverId(driverId);
|
||||
waybill.setDriverName(driverName);
|
||||
waybill.setDriverPhone(driverPhone);
|
||||
waybill.setVehicleNo(vehicleNo);
|
||||
@@ -1801,21 +1808,57 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
return loadingManageService.detail(loadingManage.getId());
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<Waybill> buildQuery(WaybillVO waybill) {
|
||||
LambdaQueryWrapper<Waybill> queryWrapper = Wrappers.<Waybill>lambdaQuery().eq(Waybill::getIsDeleted, 0);
|
||||
// 与常用线路等模块一致:超管默认不按组织过滤;普通用户 allDept!=1 时强制当前组织
|
||||
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(
|
||||
waybill.getAllDept(), waybill.getDeptId(), "运单管理", TransportBusinessSupport.MENU_WAYBILL_MANAGE);
|
||||
if (Func.isNotEmpty(deptIdList)) {
|
||||
if (deptIdList.size() == 1) {
|
||||
queryWrapper.eq(Waybill::getDeptId, deptIdList.get(0));
|
||||
} else {
|
||||
queryWrapper.in(Waybill::getDeptId, deptIdList);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 运单可见范围:超管不限制;普通账号仅本组织运单,以及归属本账号项目的运单。
|
||||
* 本人创建权限仍只看自己创建的运单。
|
||||
*/
|
||||
private void applyAccountScope(LambdaQueryWrapper<Waybill> queryWrapper, Integer allDept, Long deptId) {
|
||||
if (TransportBusinessSupport.isOwnCreateScope(TransportBusinessSupport.MENU_WAYBILL_MANAGE)) {
|
||||
queryWrapper.eq(Waybill::getCreateUser, AuthUtil.getUserId());
|
||||
return;
|
||||
}
|
||||
List<Long> deptIdList = TransportBusinessSupport.resolveScopedDeptFilterIds(
|
||||
allDept, deptId, "运单管理", TransportBusinessSupport.MENU_WAYBILL_MANAGE);
|
||||
if (Func.isEmpty(deptIdList)) {
|
||||
return;
|
||||
}
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
queryWrapper.and(wrapper -> {
|
||||
if (deptIdList.size() == 1) {
|
||||
wrapper.eq(Waybill::getDeptId, deptIdList.get(0));
|
||||
} else {
|
||||
wrapper.in(Waybill::getDeptId, deptIdList);
|
||||
}
|
||||
TransportBusinessSupport.orProjectVisibleByBusinessDept(wrapper, deptIdList);
|
||||
TransportBusinessSupport.orProjectVisibleByAccount(wrapper, deptIdList, currentUserId);
|
||||
if (Func.isNotEmpty(currentUserId) && currentUserId > 0) {
|
||||
wrapper.or().eq(Waybill::getCreateUser, currentUserId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情、复制等按主键访问时复用列表可见范围,避免越权读取其他账号运单。
|
||||
*/
|
||||
private void assertWaybillVisible(Waybill waybill) {
|
||||
if (waybill == null || Func.isEmpty(waybill.getId())) {
|
||||
throw new ServiceException("运单管理不存在");
|
||||
}
|
||||
if (TransportBusinessSupport.isAdministrator()) {
|
||||
return;
|
||||
}
|
||||
WaybillVO probe = new WaybillVO();
|
||||
probe.setAllDept(0);
|
||||
LambdaQueryWrapper<Waybill> wrapper = buildQuery(probe);
|
||||
wrapper.eq(Waybill::getId, waybill.getId());
|
||||
if (count(wrapper) <= 0) {
|
||||
throw new ServiceException("无权查看该运单");
|
||||
}
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<Waybill> buildQuery(WaybillVO waybill) {
|
||||
LambdaQueryWrapper<Waybill> queryWrapper = Wrappers.<Waybill>lambdaQuery().eq(Waybill::getIsDeleted, 0);
|
||||
applyAccountScope(queryWrapper, waybill.getAllDept(), waybill.getDeptId());
|
||||
if (Func.isNotEmpty(waybill.getWaybillNo())) {
|
||||
queryWrapper.eq(Waybill::getWaybillNo, waybill.getWaybillNo());
|
||||
}
|
||||
@@ -2211,6 +2254,8 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
private void validateCarrierContract(Waybill waybill) {
|
||||
if ("自运".equals(waybill.getCarrierType())) {
|
||||
waybill.setCarrierContractId(null);
|
||||
waybill.setCarrierPartyId(null);
|
||||
waybill.setCarrierPartyRole(null);
|
||||
return;
|
||||
}
|
||||
if (Func.isEmpty(waybill.getCarrierContractId())) {
|
||||
@@ -2220,8 +2265,24 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
if (carrierContract == null || Objects.equals(carrierContract.getIsDeleted(), 1)
|
||||
|| !"承运商合同".equals(carrierContract.getContractCategory())
|
||||
|| !Objects.equals(carrierContract.getProjectId(), waybill.getProjectId())
|
||||
|| !Objects.equals(carrierContract.getPartyB(), waybill.getCarrierName())) {
|
||||
throw new ServiceException("承运商合同必须属于所选项目,且合同乙方须与承运商一致");
|
||||
|| !ContractPartySupport.matchesCarrier(carrierContract, waybill.getCarrierName(), waybill.getCarrierPartyId())) {
|
||||
throw new ServiceException("承运商合同必须属于所选项目,且承运商须为合同乙方或丙方客商");
|
||||
}
|
||||
Map<String, Object> selectedParty = ContractPartySupport.findCarrierParty(
|
||||
carrierContract, waybill.getCarrierPartyId(), waybill.getCarrierName());
|
||||
if (selectedParty != null) {
|
||||
Object customerName = selectedParty.get("customerName");
|
||||
Object partyId = selectedParty.get("id");
|
||||
Object partyRole = selectedParty.get("partyRole");
|
||||
if (customerName != null) {
|
||||
waybill.setCarrierName(String.valueOf(customerName).trim());
|
||||
}
|
||||
if (partyId != null) {
|
||||
waybill.setCarrierPartyId(String.valueOf(partyId).trim());
|
||||
}
|
||||
if (partyRole != null) {
|
||||
waybill.setCarrierPartyRole(ContractPartySupport.normalizeRole(String.valueOf(partyRole)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2236,6 +2297,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
if (checkDept) {
|
||||
TransportBusinessSupport.assertCurrentDept(waybill.getDeptId(), "运单管理", TransportBusinessSupport.MENU_WAYBILL_MANAGE);
|
||||
}
|
||||
assertWaybillVisible(waybill);
|
||||
return waybill;
|
||||
}
|
||||
|
||||
|
||||
+21
@@ -81,6 +81,27 @@ public final class CertificationRuleSupport {
|
||||
return previousPending;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取一条记录的「当前生效内容」
|
||||
* <p>
|
||||
* 审核员可以在通过时修订内容:修订版存在则以修订版为准,否则回退到司机原提交
|
||||
* ({@code snapshot_json} 永不修改,它是司机当时提交了什么的历史证据)。
|
||||
* <p>
|
||||
* 写回档案、司机端读生效值、审核详情对比三处必须用同一套取法,
|
||||
* 否则会出现「档案是新值、司机端还是旧值」这种自相矛盾。
|
||||
*
|
||||
* @param record 认证记录,可为 null
|
||||
* @return 生效内容 JSON;记录为空、或两者都为空时返回 null
|
||||
*/
|
||||
public static String effectiveSnapshotJson(TransportCertRecord record) {
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
return Func.isNotBlank(record.getAuditedSnapshotJson())
|
||||
? record.getAuditedSnapshotJson()
|
||||
: record.getSnapshotJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判定认证对象是否已认证
|
||||
* <p>
|
||||
|
||||
+539
@@ -0,0 +1,539 @@
|
||||
/**
|
||||
* 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.support;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.vo.LoadingCarrierContractVO;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 合同甲/乙/丙多方客商明细支持
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class ContractPartySupport {
|
||||
|
||||
public static final String ROLE_A = "A";
|
||||
public static final String ROLE_B = "B";
|
||||
public static final String ROLE_C = "C";
|
||||
|
||||
private ContractPartySupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化三方客商明细并回写名称汇总
|
||||
*/
|
||||
public static void normalizeAndFillSummary(ContractManage contractManage) {
|
||||
List<Map<String, Object>> partyARows = normalizePartyRows(contractManage.getPartyAJson(), true);
|
||||
List<Map<String, Object>> partyBRows = normalizePartyRows(contractManage.getPartyBJson(), true);
|
||||
List<Map<String, Object>> partyCRows = normalizePartyRows(contractManage.getPartyCJson(), false);
|
||||
contractManage.setPartyAJson(toJsonOrNull(partyARows));
|
||||
contractManage.setPartyBJson(toJsonOrNull(partyBRows));
|
||||
contractManage.setPartyCJson(toJsonOrNull(partyCRows));
|
||||
contractManage.setPartyA(joinCustomerNames(partyARows));
|
||||
contractManage.setPartyB(joinCustomerNames(partyBRows));
|
||||
contractManage.setPartyC(joinCustomerNames(partyCRows));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交级校验:甲/乙至少一行,计费方案必须绑定明细客商
|
||||
*/
|
||||
public static void validateRequiredParties(ContractManage contractManage, boolean requireCustomerId) {
|
||||
List<Map<String, Object>> partyARows = parsePartyRows(contractManage.getPartyAJson());
|
||||
List<Map<String, Object>> partyBRows = parsePartyRows(contractManage.getPartyBJson());
|
||||
List<Map<String, Object>> partyCRows = parsePartyRows(contractManage.getPartyCJson());
|
||||
if (partyARows.isEmpty()) {
|
||||
throw new ServiceException("请至少添加一个甲方客商");
|
||||
}
|
||||
if (partyBRows.isEmpty()) {
|
||||
throw new ServiceException("请至少添加一个乙方客商");
|
||||
}
|
||||
validatePartyRows("甲方", partyARows, requireCustomerId);
|
||||
validatePartyRows("乙方", partyBRows, requireCustomerId);
|
||||
if (!partyCRows.isEmpty()) {
|
||||
validatePartyRows("丙方", partyCRows, requireCustomerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验计费方案均已绑定合同明细中的客商。
|
||||
* 客户合同仅允许绑定甲方;承运商合同仅允许绑定乙方/丙方。
|
||||
*/
|
||||
public static void validateBillingPlanBindings(ContractManage contractManage) {
|
||||
String billingPlanJson = contractManage.getBillingPlanJson();
|
||||
if (Func.isEmpty(billingPlanJson)) {
|
||||
return;
|
||||
}
|
||||
Object parsed = JsonUtil.parse(billingPlanJson, List.class);
|
||||
if (!(parsed instanceof List<?> plans) || plans.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> allowedRoles = allowedBillingPartyRoles(contractManage.getContractCategory());
|
||||
Map<String, Map<String, Object>> partyByRowId = indexPartyRows(contractManage).entrySet().stream()
|
||||
.filter(entry -> allowedRoles.contains(normalizeRole(String.valueOf(entry.getValue().get("partyRole")))))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (left, right) -> left, LinkedHashMap::new));
|
||||
Set<String> customerKeys = partyByRowId.values().stream()
|
||||
.map(ContractPartySupport::customerMatchKey)
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toSet());
|
||||
for (int index = 0; index < plans.size(); index++) {
|
||||
Object value = plans.get(index);
|
||||
if (!(value instanceof Map<?, ?> plan)) {
|
||||
throw new ServiceException("计费方案设置格式不正确");
|
||||
}
|
||||
String partyRowId = trimToNull(plan.get("partyRowId"));
|
||||
String customerId = trimToNull(plan.get("customerId"));
|
||||
String customerName = trimToNull(plan.get("customerName"));
|
||||
String partyRole = normalizeRole(trimToNull(plan.get("partyRole")));
|
||||
if (Func.isEmpty(partyRowId) && Func.isEmpty(customerId) && Func.isEmpty(customerName)) {
|
||||
throw new ServiceException("计费方案第" + (index + 1) + "个必须选择适用客商");
|
||||
}
|
||||
if (Func.isNotEmpty(partyRole) && !allowedRoles.contains(partyRole)) {
|
||||
throw new ServiceException(billingPartyRoleMessage(contractManage.getContractCategory(), index + 1));
|
||||
}
|
||||
boolean matched = false;
|
||||
if (Func.isNotEmpty(partyRowId) && partyByRowId.containsKey(partyRowId)) {
|
||||
matched = true;
|
||||
} else {
|
||||
String key = customerMatchKey(customerId, customerName, partyRole);
|
||||
matched = Func.isNotEmpty(key) && customerKeys.contains(key);
|
||||
if (!matched && Func.isNotEmpty(customerName)) {
|
||||
matched = partyByRowId.values().stream()
|
||||
.anyMatch(row -> Objects.equals(trimToNull(row.get("customerName")), customerName)
|
||||
&& (Func.isEmpty(partyRole) || Objects.equals(normalizeRole(String.valueOf(row.get("partyRole"))), partyRole)));
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
throw new ServiceException(billingPartyRoleMessage(contractManage.getContractCategory(), index + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计费方案可选客商角色:客户合同=甲方;承运商合同=乙方/丙方
|
||||
*/
|
||||
public static Set<String> allowedBillingPartyRoles(String contractCategory) {
|
||||
String category = Func.toStr(contractCategory).trim();
|
||||
if ("承运商合同".equals(category)) {
|
||||
return Set.of(ROLE_B, ROLE_C);
|
||||
}
|
||||
if ("客户合同".equals(category)) {
|
||||
return Set.of(ROLE_A);
|
||||
}
|
||||
return Set.of(ROLE_A, ROLE_B, ROLE_C);
|
||||
}
|
||||
|
||||
private static String billingPartyRoleMessage(String contractCategory, int planIndex) {
|
||||
String category = Func.toStr(contractCategory).trim();
|
||||
if ("承运商合同".equals(category)) {
|
||||
return "计费方案第" + planIndex + "个适用客商只能选择乙方或丙方";
|
||||
}
|
||||
if ("客户合同".equals(category)) {
|
||||
return "计费方案第" + planIndex + "个适用客商只能选择甲方";
|
||||
}
|
||||
return "计费方案第" + planIndex + "个适用客商不在合同甲/乙/丙明细中";
|
||||
}
|
||||
|
||||
public static List<Map<String, Object>> parsePartyRows(String partyJson) {
|
||||
if (Func.isEmpty(partyJson)) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(partyJson, List.class);
|
||||
if (!(parsed instanceof List<?> rows) || rows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object row : rows) {
|
||||
if (row instanceof Map<?, ?> map) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
map.forEach((key, value) -> values.put(String.valueOf(key), value));
|
||||
result.add(values);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (Exception exception) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Map<String, Object>> carrierPartyRows(ContractManage contractManage) {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
parsePartyRows(contractManage == null ? null : contractManage.getPartyBJson()).forEach(row -> {
|
||||
row.put("partyRole", ROLE_B);
|
||||
rows.add(row);
|
||||
});
|
||||
parsePartyRows(contractManage == null ? null : contractManage.getPartyCJson()).forEach(row -> {
|
||||
row.put("partyRole", ROLE_C);
|
||||
rows.add(row);
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
public static Set<String> carrierPartyNames(ContractManage contractManage) {
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
for (Map<String, Object> row : carrierPartyRows(contractManage)) {
|
||||
String name = normalizePartyName(trimToNull(row.get("customerName")));
|
||||
if (Func.isNotEmpty(name)) {
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
// 兼容历史仅有汇总字段的合同
|
||||
if (names.isEmpty() && contractManage != null) {
|
||||
String partyB = normalizePartyName(contractManage.getPartyB());
|
||||
if (Func.isNotEmpty(partyB)) {
|
||||
names.add(partyB);
|
||||
}
|
||||
String partyC = normalizePartyName(contractManage.getPartyC());
|
||||
if (Func.isNotEmpty(partyC)) {
|
||||
names.add(partyC);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public static boolean matchesCarrier(ContractManage contractManage, String carrierName, String carrierPartyId) {
|
||||
if (contractManage == null) {
|
||||
return false;
|
||||
}
|
||||
List<Map<String, Object>> rows = carrierPartyRows(contractManage);
|
||||
if (Func.isNotEmpty(carrierPartyId)) {
|
||||
for (Map<String, Object> row : rows) {
|
||||
if (Objects.equals(trimToNull(row.get("id")), trimToNull(carrierPartyId))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
String normalizedName = normalizePartyName(carrierName);
|
||||
if (Func.isEmpty(normalizedName)) {
|
||||
return false;
|
||||
}
|
||||
if (rows.isEmpty()) {
|
||||
return Objects.equals(normalizePartyName(contractManage.getPartyB()), normalizedName)
|
||||
|| Objects.equals(normalizePartyName(contractManage.getPartyC()), normalizedName);
|
||||
}
|
||||
return rows.stream()
|
||||
.anyMatch(row -> Objects.equals(normalizePartyName(trimToNull(row.get("customerName"))), normalizedName));
|
||||
}
|
||||
|
||||
public static Map<String, Object> findCarrierParty(ContractManage contractManage, String carrierPartyId, String carrierName) {
|
||||
List<Map<String, Object>> rows = carrierPartyRows(contractManage);
|
||||
if (Func.isNotEmpty(carrierPartyId)) {
|
||||
for (Map<String, Object> row : rows) {
|
||||
if (Objects.equals(trimToNull(row.get("id")), trimToNull(carrierPartyId))) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
}
|
||||
String normalizedName = normalizePartyName(carrierName);
|
||||
if (Func.isEmpty(normalizedName)) {
|
||||
return null;
|
||||
}
|
||||
for (Map<String, Object> row : rows) {
|
||||
if (Objects.equals(normalizePartyName(trimToNull(row.get("customerName"))), normalizedName)) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<Map<String, Object>> filterBillingPlans(String billingPlanJson, Long customerId, String customerName, String partyRowId) {
|
||||
List<Map<String, Object>> plans = parseObjectList(billingPlanJson);
|
||||
if (plans.isEmpty()) {
|
||||
return plans;
|
||||
}
|
||||
String normalizedName = normalizePartyName(customerName);
|
||||
String normalizedPartyRowId = trimToNull(partyRowId);
|
||||
String customerIdText = customerId == null ? null : String.valueOf(customerId);
|
||||
List<Map<String, Object>> matched = plans.stream()
|
||||
.filter(plan -> {
|
||||
if (Func.isNotEmpty(normalizedPartyRowId)
|
||||
&& Objects.equals(trimToNull(plan.get("partyRowId")), normalizedPartyRowId)) {
|
||||
return true;
|
||||
}
|
||||
if (Func.isNotEmpty(customerIdText)
|
||||
&& Objects.equals(trimToNull(plan.get("customerId")), customerIdText)) {
|
||||
return true;
|
||||
}
|
||||
return Func.isNotEmpty(normalizedName)
|
||||
&& Objects.equals(normalizePartyName(trimToNull(plan.get("customerName"))), normalizedName);
|
||||
})
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
// 历史方案未绑定客商时,不强制过滤,避免老数据无法计费
|
||||
if (matched.isEmpty() && plans.stream().noneMatch(ContractPartySupport::hasCustomerBinding)) {
|
||||
return plans;
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
public static List<LoadingCarrierContractVO> expandCarrierOptions(Collection<ContractManage> contracts) {
|
||||
if (Func.isEmpty(contracts)) {
|
||||
return List.of();
|
||||
}
|
||||
List<LoadingCarrierContractVO> options = new ArrayList<>();
|
||||
for (ContractManage contract : contracts) {
|
||||
if (contract == null) {
|
||||
continue;
|
||||
}
|
||||
List<Map<String, Object>> rows = carrierPartyRows(contract);
|
||||
if (rows.isEmpty()) {
|
||||
LoadingCarrierContractVO option = new LoadingCarrierContractVO();
|
||||
option.setId(contract.getId());
|
||||
option.setContractName(contract.getContractName());
|
||||
option.setCarrierName(contract.getPartyB());
|
||||
option.setCarrierPartyRole(ROLE_B);
|
||||
options.add(option);
|
||||
continue;
|
||||
}
|
||||
for (Map<String, Object> row : rows) {
|
||||
LoadingCarrierContractVO option = new LoadingCarrierContractVO();
|
||||
option.setId(contract.getId());
|
||||
option.setContractName(contract.getContractName());
|
||||
option.setCarrierName(trimToNull(row.get("customerName")));
|
||||
option.setCarrierPartyId(trimToNull(row.get("id")));
|
||||
option.setCarrierPartyRole(normalizeRole(String.valueOf(row.get("partyRole"))));
|
||||
option.setCreditCode(trimToNull(row.get("creditCode")));
|
||||
option.setCustomerId(toLong(row.get("customerId")));
|
||||
options.add(option);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
public static String normalizePartyName(String value) {
|
||||
return Func.isEmpty(value) ? "" : value.trim();
|
||||
}
|
||||
|
||||
public static String normalizeRole(String role) {
|
||||
if (Func.isEmpty(role)) {
|
||||
return null;
|
||||
}
|
||||
String text = role.trim().toUpperCase();
|
||||
if ("A".equals(text) || "甲方".equals(role) || "甲".equals(role)) {
|
||||
return ROLE_A;
|
||||
}
|
||||
if ("B".equals(text) || "乙方".equals(role) || "乙".equals(role)) {
|
||||
return ROLE_B;
|
||||
}
|
||||
if ("C".equals(text) || "丙方".equals(role) || "丙".equals(role)) {
|
||||
return ROLE_C;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
public static String firstPartyName(String partyJson, String fallback) {
|
||||
List<Map<String, Object>> rows = parsePartyRows(partyJson);
|
||||
if (!rows.isEmpty()) {
|
||||
String name = trimToNull(rows.get(0).get("customerName"));
|
||||
if (Func.isNotEmpty(name)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return TransportBusinessSupport.trimToNull(fallback);
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> normalizePartyRows(String partyJson, boolean keepEmptyPlaceholder) {
|
||||
List<Map<String, Object>> source = parsePartyRows(partyJson);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Map<String, Object> sourceRow : source) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
String id = trimToNull(sourceRow.get("id"));
|
||||
if (Func.isEmpty(id)) {
|
||||
id = String.valueOf(IdWorker.getId());
|
||||
}
|
||||
row.put("id", id);
|
||||
Long customerId = toLong(sourceRow.get("customerId"));
|
||||
if (customerId != null) {
|
||||
row.put("customerId", customerId);
|
||||
}
|
||||
String customerName = trimToNull(sourceRow.get("customerName"));
|
||||
if (Func.isNotEmpty(customerName)) {
|
||||
row.put("customerName", customerName);
|
||||
}
|
||||
String creditCode = trimToNull(sourceRow.get("creditCode"));
|
||||
if (Func.isNotEmpty(creditCode)) {
|
||||
row.put("creditCode", creditCode);
|
||||
}
|
||||
Long contactId = toLong(sourceRow.get("contactId"));
|
||||
if (contactId != null) {
|
||||
row.put("contactId", contactId);
|
||||
}
|
||||
String contactName = trimToNull(sourceRow.get("contactName"));
|
||||
if (Func.isNotEmpty(contactName)) {
|
||||
row.put("contactName", contactName);
|
||||
}
|
||||
String contactPhone = trimToNull(sourceRow.get("contactPhone"));
|
||||
if (Func.isNotEmpty(contactPhone)) {
|
||||
row.put("contactPhone", contactPhone);
|
||||
}
|
||||
if (Func.isEmpty(customerName) && customerId == null && !keepEmptyPlaceholder) {
|
||||
continue;
|
||||
}
|
||||
result.add(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void validatePartyRows(String roleLabel, List<Map<String, Object>> rows, boolean requireCustomerId) {
|
||||
Set<String> customerKeys = new LinkedHashSet<>();
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
Map<String, Object> row = rows.get(index);
|
||||
String customerName = trimToNull(row.get("customerName"));
|
||||
Long customerId = toLong(row.get("customerId"));
|
||||
if (Func.isEmpty(customerName)) {
|
||||
throw new ServiceException(roleLabel + "第" + (index + 1) + "行请选择客商");
|
||||
}
|
||||
if (requireCustomerId && customerId == null) {
|
||||
throw new ServiceException(roleLabel + "第" + (index + 1) + "行请选择有效客商");
|
||||
}
|
||||
String contactName = trimToNull(row.get("contactName"));
|
||||
String contactPhone = trimToNull(row.get("contactPhone"));
|
||||
if (Func.isNotEmpty(contactName) && Func.isEmpty(contactPhone)) {
|
||||
throw new ServiceException(roleLabel + "第" + (index + 1) + "行联系人需带出联系电话");
|
||||
}
|
||||
String key = customerId == null ? customerName : String.valueOf(customerId);
|
||||
if (!customerKeys.add(key)) {
|
||||
throw new ServiceException(roleLabel + "存在重复客商:" + customerName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, Object>> indexPartyRows(ContractManage contractManage) {
|
||||
Map<String, Map<String, Object>> indexed = new LinkedHashMap<>();
|
||||
putIndexedRows(indexed, parsePartyRows(contractManage.getPartyAJson()), ROLE_A);
|
||||
putIndexedRows(indexed, parsePartyRows(contractManage.getPartyBJson()), ROLE_B);
|
||||
putIndexedRows(indexed, parsePartyRows(contractManage.getPartyCJson()), ROLE_C);
|
||||
return indexed;
|
||||
}
|
||||
|
||||
private static void putIndexedRows(Map<String, Map<String, Object>> indexed, List<Map<String, Object>> rows, String role) {
|
||||
for (Map<String, Object> row : rows) {
|
||||
Map<String, Object> copy = new LinkedHashMap<>(row);
|
||||
copy.put("partyRole", role);
|
||||
String id = trimToNull(copy.get("id"));
|
||||
if (Func.isNotEmpty(id)) {
|
||||
indexed.put(id, copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String customerMatchKey(Map<String, Object> row) {
|
||||
return customerMatchKey(trimToNull(row.get("customerId")), trimToNull(row.get("customerName")),
|
||||
normalizeRole(String.valueOf(row.get("partyRole"))));
|
||||
}
|
||||
|
||||
private static String customerMatchKey(String customerId, String customerName, String partyRole) {
|
||||
if (Func.isNotEmpty(customerId)) {
|
||||
return normalizeRole(partyRole) + ":" + customerId;
|
||||
}
|
||||
if (Func.isNotEmpty(customerName)) {
|
||||
return normalizeRole(partyRole) + ":" + customerName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean hasCustomerBinding(Map<String, Object> plan) {
|
||||
return Func.isNotEmpty(trimToNull(plan.get("partyRowId")))
|
||||
|| Func.isNotEmpty(trimToNull(plan.get("customerId")))
|
||||
|| Func.isNotEmpty(trimToNull(plan.get("customerName")));
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> parseObjectList(String json) {
|
||||
if (Func.isEmpty(json)) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(json, List.class);
|
||||
if (!(parsed instanceof List<?> list) || list.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object item : list) {
|
||||
if (item instanceof Map<?, ?> map) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
map.forEach((key, value) -> values.put(String.valueOf(key), value));
|
||||
result.add(values);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (Exception exception) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private static String joinCustomerNames(List<Map<String, Object>> rows) {
|
||||
List<String> names = rows.stream()
|
||||
.map(row -> trimToNull(row.get("customerName")))
|
||||
.filter(Func::isNotEmpty)
|
||||
.distinct()
|
||||
.toList();
|
||||
return names.isEmpty() ? null : String.join("、", names);
|
||||
}
|
||||
|
||||
private static String toJsonOrNull(List<Map<String, Object>> rows) {
|
||||
return rows == null || rows.isEmpty() ? null : JsonUtil.toJson(rows);
|
||||
}
|
||||
|
||||
private static String trimToNull(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
return text.isEmpty() || "null".equalsIgnoreCase(text) ? null : text;
|
||||
}
|
||||
|
||||
private static Long toLong(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
String text = trimToNull(value);
|
||||
if (Func.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(text);
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
-21
@@ -239,19 +239,34 @@ public class KingdeeApBillSyncSupport {
|
||||
if (Func.isBlank(awrpSrcCode)) {
|
||||
return false;
|
||||
}
|
||||
BfiApBillQueryDTO param = new BfiApBillQueryDTO();
|
||||
param.setAwrpSrcCode(List.of(awrpSrcCode));
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
for (BfiApBillVO apBill : BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(PAY_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryApBill(param);
|
||||
})) {
|
||||
if (awrpSrcCode.equals(apBill.getAwrpSrcCode()) && Boolean.TRUE.equals(apBill.getHadwrittenoff())) {
|
||||
return true;
|
||||
return queryApBillWriteOffFlags(List.of(awrpSrcCode)).getOrDefault(awrpSrcCode, Boolean.FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* A102-4 冲销状态批量查询:防重键批量分批查询财务应付单,返回 防重键 -> 是否已冲销
|
||||
* (hadwrittenoff=true,部分冲销同样视为已冲销;同键多单取或,查不到的键不在结果中即视为未冲销)。
|
||||
* 定时回写结算单金蝶冲销状态用,整批一次查询消除逐单远程查询 N+1
|
||||
*
|
||||
* @param awrpSrcCodes 财务应付单防重键列表
|
||||
* @return 防重键 -> 是否已冲销
|
||||
*/
|
||||
public Map<String, Boolean> queryApBillWriteOffFlags(List<String> awrpSrcCodes) {
|
||||
Map<String, Boolean> writeOffFlags = new LinkedHashMap<>();
|
||||
batchForeach(awrpSrcCodes, bfiProperties.getWriteOffResult().getQueryBatchSize(), batch -> {
|
||||
BfiApBillQueryDTO param = new BfiApBillQueryDTO();
|
||||
param.setAwrpSrcCode(batch);
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
for (BfiApBillVO apBill : BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(PAY_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryApBill(param);
|
||||
})) {
|
||||
if (Func.isNotEmpty(apBill.getAwrpSrcCode())) {
|
||||
writeOffFlags.merge(apBill.getAwrpSrcCode(), Boolean.TRUE.equals(apBill.getHadwrittenoff()), Boolean::logicalOr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return writeOffFlags;
|
||||
}
|
||||
|
||||
// ==================== A102-2 财务应付单保存 ====================
|
||||
@@ -309,8 +324,8 @@ public class KingdeeApBillSyncSupport {
|
||||
param.setCurrencyNumber(currency);
|
||||
// 【必填】付款组织.编码
|
||||
param.setPayorgNumber(orgNumber);
|
||||
// 部门.编码:项目业务部门自身的金蝶编码(结算/付款组织仍传上溯平台公司编码,口径与付款申请 A103-3 一致)
|
||||
param.setDepartmentNumber(this.kingdeeDeptNumber(contract.getProjectId(), context));
|
||||
// 部门.编码:合同平台公司下业务部门配置的金蝶部门代码
|
||||
param.setDepartmentNumber(this.kingdeeDeptNumber(contract, contract.getProjectId(), context));
|
||||
// 采购组织.编码(非必传)
|
||||
// param.setPurorgNumber(orgNumber);
|
||||
// 【必填】款项性质.编码
|
||||
@@ -1022,6 +1037,20 @@ public class KingdeeApBillSyncSupport {
|
||||
* @return 组织编码
|
||||
*/
|
||||
public String bfiOrgNumber(ContractManage contract, KingdeeSyncContext context) {
|
||||
if (contract == null) {
|
||||
throw new ServiceException("推送金蝶失败,合同不存在,无法确定金蝶组织编码");
|
||||
}
|
||||
// 优先取合同平台公司金蝶代码;未维护时回退项目业务部门上溯
|
||||
if (contract.getPlatformCompanyId() != null) {
|
||||
Dept platformCompany = SysCache.getDept(contract.getPlatformCompanyId());
|
||||
if (platformCompany == null) {
|
||||
throw new ServiceException("推送金蝶失败,合同平台公司不存在,无法确定金蝶组织编码");
|
||||
}
|
||||
if (Func.isBlank(platformCompany.getKingdeeDeptCode())) {
|
||||
throw new ServiceException("推送金蝶失败,平台公司[" + platformCompany.getDeptName() + "]未维护金蝶组织编码,请检查部门数据");
|
||||
}
|
||||
return platformCompany.getKingdeeDeptCode();
|
||||
}
|
||||
if (contract.getProjectId() == null) {
|
||||
throw new ServiceException("推送金蝶失败,合同[" + Func.toStr(contract.getContractNo()) + "]未关联项目,无法确定金蝶组织编码");
|
||||
}
|
||||
@@ -1079,20 +1108,41 @@ public class KingdeeApBillSyncSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶部门编码:项目业务部门自身的金蝶编码(blade_dept.kingdee_dept_code,不上溯平台公司),
|
||||
* 用于付款申请单 awrp_department_number(部门.编码);项目未关联/未维护业务部门/业务部门未维护金蝶编码时拦截
|
||||
* 金蝶部门编码:必须取「业务部门配置」中合同平台公司 + 业务部门的金蝶部门代码;
|
||||
* 未配置时拦截推送
|
||||
*
|
||||
* @param projectId 项目id
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 部门编码
|
||||
*/
|
||||
public String kingdeeDeptNumber(Long projectId, KingdeeSyncContext context) {
|
||||
ProjectApply project = this.requireOrgProject(projectId, context);
|
||||
Dept dept = SysCache.getDept(project.getBusinessDeptId());
|
||||
if (dept == null || Func.isBlank(dept.getKingdeeDeptCode())) {
|
||||
throw new ServiceException("推送金蝶失败,项目业务部门[" + Func.toStr(project.getBusinessDeptName()) + "]未维护金蝶编码,请检查部门数据");
|
||||
return kingdeeDeptNumber(null, projectId, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶部门编码:按合同平台公司下业务部门配置的金蝶部门代码推送
|
||||
*
|
||||
* @param contract 合同(优先取其平台公司、合同负责部门)
|
||||
* @param projectId 项目id(合同未带业务部门时回退项目业务部门)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 部门编码
|
||||
*/
|
||||
public String kingdeeDeptNumber(ContractManage contract, Long projectId, KingdeeSyncContext context) {
|
||||
Long platformCompanyId = contract == null ? null : contract.getPlatformCompanyId();
|
||||
Long businessDeptId = contract == null ? null : contract.getOrganizationId();
|
||||
if (businessDeptId == null && projectId != null) {
|
||||
ProjectApply project = this.requireOrgProject(projectId, context);
|
||||
businessDeptId = project.getBusinessDeptId();
|
||||
}
|
||||
return dept.getKingdeeDeptCode();
|
||||
if (platformCompanyId == null || businessDeptId == null) {
|
||||
throw new ServiceException("业务部门对应的金蝶组织代码未配置,单据无法推送,请联系管理员处理。");
|
||||
}
|
||||
org.springblade.system.pojo.entity.BusinessDeptConfig config =
|
||||
SysCache.getBusinessDeptConfig(platformCompanyId, businessDeptId);
|
||||
if (config == null || Func.isBlank(config.getKingdeeDeptCode())) {
|
||||
throw new ServiceException("业务部门对应的金蝶组织代码未配置,单据无法推送,请联系管理员处理。");
|
||||
}
|
||||
return config.getKingdeeDeptCode().trim();
|
||||
}
|
||||
|
||||
/** 金蝶组织/部门编码共用校验:项目存在且已维护业务部门 */
|
||||
|
||||
+29
-14
@@ -150,19 +150,34 @@ public class KingdeeArBillSyncSupport {
|
||||
if (Func.isBlank(arBillSrcCode)) {
|
||||
return false;
|
||||
}
|
||||
BfiArBillQueryDTO param = new BfiArBillQueryDTO();
|
||||
param.setAwrpSrcCode(List.of(arBillSrcCode));
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
for (BfiArBillVO arBill : BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(AR_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryArBill(param);
|
||||
})) {
|
||||
if (arBillSrcCode.equals(arBill.getAwrpSrcCode()) && Boolean.TRUE.equals(arBill.getHadwrittenoff())) {
|
||||
return true;
|
||||
return queryArBillWriteOffFlags(List.of(arBillSrcCode)).getOrDefault(arBillSrcCode, Boolean.FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* A302-4 冲销状态批量查询:防重键批量分批查询财务应收单,返回 防重键 -> 是否已冲销
|
||||
* (hadwrittenoff=true,部分冲销同样视为已冲销;同键多单取或,查不到的键不在结果中即视为未冲销)。
|
||||
* 定时回写结算单金蝶冲销状态用,整批一次查询消除逐单远程查询 N+1
|
||||
*
|
||||
* @param arBillSrcCodes 财务应收单防重键列表
|
||||
* @return 防重键 -> 是否已冲销
|
||||
*/
|
||||
public Map<String, Boolean> queryArBillWriteOffFlags(List<String> arBillSrcCodes) {
|
||||
Map<String, Boolean> writeOffFlags = new LinkedHashMap<>();
|
||||
kingdeeApBillSyncSupport.batchForeach(arBillSrcCodes, bfiProperties.getWriteOffResult().getQueryBatchSize(), batch -> {
|
||||
BfiArBillQueryDTO param = new BfiArBillQueryDTO();
|
||||
param.setAwrpSrcCode(batch);
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
for (BfiArBillVO arBill : BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(AR_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryArBill(param);
|
||||
})) {
|
||||
if (Func.isNotEmpty(arBill.getAwrpSrcCode())) {
|
||||
writeOffFlags.merge(arBill.getAwrpSrcCode(), Boolean.TRUE.equals(arBill.getHadwrittenoff()), Boolean::logicalOr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return writeOffFlags;
|
||||
}
|
||||
|
||||
// ==================== A302-2 财务应收单保存 ====================
|
||||
@@ -218,8 +233,8 @@ public class KingdeeArBillSyncSupport {
|
||||
param.setOrgNumber(orgNumber);
|
||||
// 收款组织.编码:同结算组织
|
||||
param.setRecorgNumber(orgNumber);
|
||||
// 部门.编码:项目业务部门自身的金蝶编码(结算/收款组织仍传上溯平台公司编码,口径与付款申请/应付单一致)
|
||||
param.setDepartmentNumber(kingdeeApBillSyncSupport.kingdeeDeptNumber(contract.getProjectId(), context));
|
||||
// 部门.编码:合同平台公司下业务部门配置的金蝶部门代码
|
||||
param.setDepartmentNumber(kingdeeApBillSyncSupport.kingdeeDeptNumber(contract, contract.getProjectId(), context));
|
||||
// 【必填】单据日期
|
||||
param.setBizdate(billDateText);
|
||||
// 【必填】往来类型:统一固定客户(KingdeeApBillSyncSupport#bfiAsstactType,与应付单/付款申请同口径)
|
||||
|
||||
+24
-19
@@ -64,11 +64,12 @@ import java.util.Map;
|
||||
/**
|
||||
* 金蝶收票单同步支撑组件(图05 环节②③:A303-2 收票单批量保存 + A302-6 收票指定应付 + A801-3 收票单查询)。
|
||||
* <p>收票登记「推送金蝶」由 {@link org.springblade.transport.service.IInvoiceReceiptService} 编排前置校验
|
||||
* (审批通过、已绑定镜像、A801-3 幂等预检)后调用本组件;先票后款链路:A303-2 推送即终态(pushed),
|
||||
* A302-6 指定应付移交付款结算单侧(应付单 A102-2 生成后按认领关系逐单指定)。
|
||||
* 报文组装取数:往来户=发票销售方金蝶供应商代码(A901-1),结算组织=页面手选部门沿部门树上溯平台公司,
|
||||
* 币别固定 CNY;票面字段取自票池镜像(A801-1 落库),明细分录逐行取发票明细(镜像 items_json),
|
||||
* 分录标识 awrp_entry_code=发票号码_seq(重推幂等候选键)。
|
||||
* (审批通过、已绑定镜像、一票一应付、A102-4 应付单已生成预检、A801-3 幂等预检)后调用本组件;
|
||||
* 报文组装取数:主表组织/币种/往来户复用应付支撑组件的合同派生方法,票面字段取自票池镜像
|
||||
* (invoice_type_code 等,A801-1 落库),明细分录与财务应付单 A102-2 同源同口径
|
||||
* (结算明细行+结算合计费用项各一条,{@link KingdeeApBillSyncSupport.BfiEntryGroup},分录标识同键)。
|
||||
* 防重键:收票单 awrp_src_code=收票登记id,awrp_entry_code=结算明细行单据号/结算合计费用项拼接
|
||||
* (与应付单分录同键,便于金蝶侧逐行勾稽);A302-6 按「收票单防重键 ↔ 财务应付单防重键」一票一应付指定。
|
||||
* 财务应付单侧(A102-2/A102-4)取数与校验复用 {@link KingdeeApBillSyncSupport}。</p>
|
||||
*
|
||||
* @author Chill
|
||||
@@ -150,19 +151,21 @@ public class KingdeeReceivedInvoiceSyncSupport {
|
||||
|
||||
/**
|
||||
* 推送金蝶收票单(A303-2 批量保存,调用方已完成 A801-3 幂等预检)。
|
||||
* 先票后款链路:收票登记不挂结算单/合同——主表往来户=发票销售方金蝶供应商代码(receipt.supplierCode,
|
||||
* 缺失实时查 A901-1 一次,仍缺失报错阻断),结算组织=页面手选部门(receipt.deptId)沿部门树反查平台公司,
|
||||
* 币别固定 CNY,往来类型取固定常量;销方=开票单位、购方=受票单位。
|
||||
* 明细分录逐行取发票明细行(镜像 items_json,{@link BfiInvoicePoolItemVO}),不与结算明细同源。
|
||||
* 主表:组织/币种/往来户编码按分摊结算单关联合同派生(与应付单同源),销方=开票单位、购方=受票单位;
|
||||
* 明细分录与财务应付单 A102-2 同源同口径:结算明细按 货物→费用大类→税率 拆分行+手动添加结算合计费用项每行一条,物料/数量/单价/金额取自
|
||||
* 与应付单分录同一解析结果({@link KingdeeApBillSyncSupport.BfiEntryGroup}),分录标识同键。
|
||||
* 联调口径沿用应付侧:先只送 DTO 标注【必填】字段与票面核心字段(发票号码),
|
||||
* 非必填的税号/地址/电话/开户行等 setter 注释保留,联调确认后放开
|
||||
*
|
||||
* @param receipt 收票登记(审批通过、已绑定镜像)
|
||||
* @param mirror 票池镜像(票面主表字段+明细行取数来源)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @param receipt 收票登记(审批通过、已绑定镜像)
|
||||
* @param mirror 票池镜像(票面主表字段取数来源)
|
||||
* @param contract 分摊应付结算单关联合同(组织/币种/往来户派生)
|
||||
* @param entryGroups 分摊结算单解析出的分录(与应付单 A102-2 同一解析结果)
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 推送结果(单据编号 + 星瀚收票单内码,A902 附件上传定位用)
|
||||
*/
|
||||
public ReceivedInvoicePushOutcome pushReceivedInvoice(InvoiceReceipt receipt, KingdeeInvoicePool mirror, KingdeeSyncContext context) {
|
||||
public ReceivedInvoicePushOutcome pushReceivedInvoice(InvoiceReceipt receipt, KingdeeInvoicePool mirror, ContractManage contract,
|
||||
List<KingdeeApBillSyncSupport.BfiEntryGroup> entryGroups, KingdeeSyncContext context) {
|
||||
if (Func.isBlank(mirror.getInvoiceTypeCode())) {
|
||||
throw new ServiceException("推送金蝶收票单失败:镜像缺少票易通发票类型编码,请先重新绑定票池发票");
|
||||
}
|
||||
@@ -183,12 +186,14 @@ public class KingdeeReceivedInvoiceSyncSupport {
|
||||
param.setInvoicetypef7Number(mirror.getInvoiceTypeCode());
|
||||
// 【必填】付款方式:(与应付单一致,不允许金蝶自动推付款)
|
||||
param.setPaymenttype(BfiConstant.PAY_MODE_CASH);
|
||||
// 【必填】往来类型:固定常量(收票登记不挂合同,直接取常量)
|
||||
param.setAsstacttype(KingdeeBfiConstant.BFI_CUSTOMER_ASSTACT_TYPE);
|
||||
// 【必填】结算组织.编码:页面手选部门沿部门树反查平台公司(收票登记 deptId)
|
||||
param.setOrgNumber(kingdeeApBillSyncSupport.kingdeeOrgNumberByDept(receipt.getDeptId(), context));
|
||||
// 【必填】结算币别:固定 CNY(收票登记不挂合同,数电票人民币)
|
||||
param.setCurrencyNumber(KingdeeBfiConstant.BFI_RECEIVED_CURRENCY_NUMBER);
|
||||
// 【必填】往来类型:统一固定客户(KingdeeApBillSyncSupport#bfiAsstactType,与应付单/付款申请同口径)
|
||||
param.setAsstacttype(kingdeeApBillSyncSupport.bfiAsstactType(contract));
|
||||
// 部门金蝶代码校验:收票单主表无部门字段,推送前仍按合同平台公司下业务部门配置校验
|
||||
kingdeeApBillSyncSupport.kingdeeDeptNumber(contract, contract.getProjectId(), context);
|
||||
// 【必填】结算组织.编码:合同平台公司金蝶组织编码(与应付单同源)
|
||||
param.setOrgNumber(kingdeeApBillSyncSupport.bfiOrgNumber(contract, context));
|
||||
// 【必填】结算币别:合同结算币种
|
||||
param.setCurrencyNumber(kingdeeApBillSyncSupport.bfiCurrency(contract));
|
||||
// 【必填】往来户.编码:发票销售方金蝶供应商代码(缺失实时查 A901-1,仍缺失报错阻断)
|
||||
String supplierNumber = this.requireSupplierNumber(receipt, mirror);
|
||||
param.setReceivablessuppNumber(supplierNumber);
|
||||
|
||||
+179
-5
@@ -23,22 +23,32 @@
|
||||
|
||||
package org.springblade.transport.support;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.constant.RoleConstant;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
import org.springblade.system.cache.RoleDataScopeCache;
|
||||
import org.springblade.system.cache.SysCache;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.system.pojo.vo.RoleDataScopeVO;
|
||||
import org.springblade.transport.mapper.ProjectApplyMapper;
|
||||
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 运输业务公共校验支持
|
||||
@@ -87,9 +97,24 @@ public final class TransportBusinessSupport {
|
||||
/** 临时额度管理菜单编码 */
|
||||
public static final String MENU_TEMPORARY_CREDIT_LIMIT = "temporary_credit_limit";
|
||||
|
||||
/**
|
||||
* 运单 freightJson 结构字段,不是费用项目名。
|
||||
* 误写入 fee_items_json / 预结算合计时需过滤,否则会出现 totalFreightAmount 等英文费用项并导致金额翻倍。
|
||||
*/
|
||||
public static final Set<String> FREIGHT_JSON_META_KEYS = Set.of(
|
||||
"currency", "totalFreightAmount", "otherFreightAmount", "freightItems",
|
||||
"freightTotal", "totalFreight", "totalAmount", "freight", "freightAmount",
|
||||
"transportFee", "otherFeeTotal", "otherAmount"
|
||||
);
|
||||
|
||||
private TransportBusinessSupport() {
|
||||
}
|
||||
|
||||
/** 是否运单运费 JSON 的结构字段名(不可作为费用项展示)。 */
|
||||
public static boolean isFreightJsonMetaKey(String key) {
|
||||
return Func.isNotBlank(key) && FREIGHT_JSON_META_KEYS.contains(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否超级管理员:当前用户任一角色别名等于 administrator 即为超级管理员。
|
||||
*
|
||||
@@ -109,14 +134,14 @@ public final class TransportBusinessSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析指定菜单下的角色数据权限(超管视为全部组织)。
|
||||
* 解析指定菜单下的角色数据权限(超管视为全部组织),并与用户「数据管理权限」组织取并集。
|
||||
*/
|
||||
public static RoleDataScopeVO resolveRoleDataScope(String menuCode) {
|
||||
if (isAdministrator()) {
|
||||
return RoleDataScopeVO.all();
|
||||
}
|
||||
RoleDataScopeVO scope = RoleDataScopeCache.resolveByMenuCode(menuCode);
|
||||
return scope == null ? RoleDataScopeVO.all() : scope;
|
||||
return mergeUserManageDepts(scope == null ? RoleDataScopeVO.all() : scope);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,10 +161,43 @@ public final class TransportBusinessSupport {
|
||||
if (isAdministrator()) {
|
||||
return true;
|
||||
}
|
||||
RoleDataScopeVO scope = RoleDataScopeCache.resolveWidest();
|
||||
RoleDataScopeVO scope = resolveWidestDataScope();
|
||||
return scope == null || scope.isAllDept();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析最宽角色数据权限,并与用户「数据管理权限」组织取并集。
|
||||
*/
|
||||
public static RoleDataScopeVO resolveWidestDataScope() {
|
||||
if (isAdministrator()) {
|
||||
return RoleDataScopeVO.all();
|
||||
}
|
||||
RoleDataScopeVO scope = RoleDataScopeCache.resolveWidest();
|
||||
return mergeUserManageDepts(scope == null ? RoleDataScopeVO.all() : scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色组织权限与用户数据管理组织取并集。
|
||||
* <p>
|
||||
* 全部组织 / 本人创建 不因用户管理组织改变;其余类型将用户管理组织(含下级)并入 deptIds。
|
||||
*/
|
||||
private static RoleDataScopeVO mergeUserManageDepts(RoleDataScopeVO scope) {
|
||||
if (scope == null || scope.isAllDept() || scope.isOwnCreate()) {
|
||||
return scope;
|
||||
}
|
||||
Long userId = AuthUtil.getUserId();
|
||||
List<Long> manageDeptIds = UserCache.getManageDeptIds(userId);
|
||||
if (Func.isEmpty(manageDeptIds)) {
|
||||
return scope;
|
||||
}
|
||||
LinkedHashSet<Long> mergedDeptIds = new LinkedHashSet<>();
|
||||
if (Func.isNotEmpty(scope.getDeptIds())) {
|
||||
mergedDeptIds.addAll(scope.getDeptIds());
|
||||
}
|
||||
mergedDeptIds.addAll(manageDeptIds);
|
||||
return RoleDataScopeVO.ofDepts(scope.getScopeType(), List.copyOf(mergedDeptIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否仅本人创建数据权限
|
||||
*/
|
||||
@@ -164,6 +222,29 @@ public final class TransportBusinessSupport {
|
||||
return dept;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表行是否只读:与 {@link #assertCurrentDept(Long, String, String)} 判定保持一致。
|
||||
* <p>
|
||||
* 超管 / 全部组织 / 本人创建数据权限下可编辑;指定组织权限下,仅所属组织在授权范围内时可编辑;
|
||||
* 否则仅当前登录用户所属组织的数据可编辑。
|
||||
*
|
||||
* @param deptId 业务数据所属组织
|
||||
* @param menuCode 菜单编码
|
||||
* @return true-前端仅展示查看
|
||||
*/
|
||||
public static boolean isDeptReadonly(Long deptId, String menuCode) {
|
||||
RoleDataScopeVO scope = resolveRoleDataScope(menuCode);
|
||||
if (scope.isAllDept() || scope.isOwnCreate()) {
|
||||
return false;
|
||||
}
|
||||
List<Long> allowedDeptIds = scope.getDeptIds();
|
||||
if (Func.isNotEmpty(allowedDeptIds)) {
|
||||
return deptId == null || deptId <= 0 || !allowedDeptIds.contains(deptId);
|
||||
}
|
||||
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
return currentDeptId != null && !Objects.equals(deptId, currentDeptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验目标业务数据属于当前用户可操作的组织。
|
||||
*/
|
||||
@@ -191,7 +272,7 @@ public final class TransportBusinessSupport {
|
||||
if (hasAllDeptScope()) {
|
||||
return;
|
||||
}
|
||||
RoleDataScopeVO scope = RoleDataScopeCache.resolveWidest();
|
||||
RoleDataScopeVO scope = resolveWidestDataScope();
|
||||
if (scope != null && scope.isOwnCreate()) {
|
||||
return;
|
||||
}
|
||||
@@ -218,6 +299,99 @@ public final class TransportBusinessSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表可见范围 OR:所属项目的业务部门落在指定组织内。
|
||||
* <p>
|
||||
* 与项目立项「本部门可见 business_dept_id」对齐,使合同/运单等业务数据
|
||||
* 在负责部门(organizationId/deptId)不是当前组织时,业务部门用户仍可看到。
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public static void orProjectVisibleByBusinessDept(AbstractWrapper<?, ?, ?> wrapper, Collection<Long> deptIds) {
|
||||
if (wrapper == null || Func.isEmpty(deptIds)) {
|
||||
return;
|
||||
}
|
||||
List<Long> ids = deptIds.stream().filter(Objects::nonNull).filter(id -> id > 0).distinct().toList();
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (ids.size() == 1) {
|
||||
wrapper.or().apply(
|
||||
"project_id IN (SELECT id FROM blade_project_apply WHERE is_deleted = 0 AND business_dept_id = {0})",
|
||||
ids.get(0));
|
||||
return;
|
||||
}
|
||||
String inList = ids.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||
wrapper.or().apply(
|
||||
"project_id IN (SELECT id FROM blade_project_apply WHERE is_deleted = 0 AND business_dept_id IN ("
|
||||
+ inList + "))");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断项目是否归属指定业务部门(用于详情只读/可操作判定)。
|
||||
*/
|
||||
public static boolean isProjectOwnedByBusinessDept(Long projectId, Long deptId) {
|
||||
if (Func.isEmpty(projectId) || Func.isEmpty(deptId) || deptId <= 0) {
|
||||
return false;
|
||||
}
|
||||
ProjectApplyMapper projectApplyMapper = SpringUtil.getBean(ProjectApplyMapper.class);
|
||||
if (projectApplyMapper == null) {
|
||||
return false;
|
||||
}
|
||||
Long count = projectApplyMapper.selectCount(Wrappers.<ProjectApply>lambdaQuery()
|
||||
.eq(ProjectApply::getId, projectId)
|
||||
.eq(ProjectApply::getBusinessDeptId, deptId)
|
||||
.eq(ProjectApply::getIsDeleted, 0));
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表实际使用的组织范围。
|
||||
* <p>
|
||||
* 角色菜单未单独收窄、解析结果为「全部组织」时,超管仍不限制;
|
||||
* 普通用户在未显式请求全部组织(allDept != 1)时收口到当前组织,
|
||||
* 避免账号看到其他组织的业务数据。本人创建权限不按组织收口。
|
||||
*/
|
||||
public static List<Long> resolveScopedDeptFilterIds(Integer allDept, Long deptId, String moduleName, String menuCode) {
|
||||
List<Long> deptIdList = resolveDeptFilterIds(allDept, deptId, moduleName, menuCode);
|
||||
if (Func.isNotEmpty(deptIdList) || isAdministrator() || isOwnCreateScope(menuCode) || Objects.equals(allDept, 1)) {
|
||||
return deptIdList;
|
||||
}
|
||||
return Collections.singletonList(currentDeptId(moduleName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号项目可见范围:承办部门命中指定组织,或当前用户是项目创建人、经办人、负责人。
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public static void orProjectVisibleByAccount(AbstractWrapper<?, ?, ?> wrapper, Collection<Long> deptIds, Long userId) {
|
||||
if (wrapper == null) {
|
||||
return;
|
||||
}
|
||||
List<Long> ids = deptIds == null ? List.of() : deptIds.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(id -> id > 0)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (!ids.isEmpty()) {
|
||||
if (ids.size() == 1) {
|
||||
wrapper.or().apply(
|
||||
"project_id IN (SELECT id FROM blade_project_apply WHERE is_deleted = 0 AND FIND_IN_SET({0}, IFNULL(undertake_dept_id,'')))",
|
||||
String.valueOf(ids.get(0)));
|
||||
} else {
|
||||
String undertakeSql = ids.stream()
|
||||
.map(id -> "FIND_IN_SET(" + id + ", IFNULL(undertake_dept_id,''))")
|
||||
.collect(Collectors.joining(" OR "));
|
||||
wrapper.or().apply(
|
||||
"project_id IN (SELECT id FROM blade_project_apply WHERE is_deleted = 0 AND (" + undertakeSql + "))");
|
||||
}
|
||||
}
|
||||
if (Func.isNotEmpty(userId) && userId > 0) {
|
||||
wrapper.or().apply(
|
||||
"project_id IN (SELECT id FROM blade_project_apply WHERE is_deleted = 0 AND (create_user = {0} OR handler_user_id = {0} OR principal_user_id = {0}))",
|
||||
userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析列表组织筛选 ID。
|
||||
*
|
||||
@@ -280,7 +454,7 @@ public final class TransportBusinessSupport {
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
RoleDataScopeVO scope = RoleDataScopeCache.resolveWidest();
|
||||
RoleDataScopeVO scope = resolveWidestDataScope();
|
||||
if (scope != null && scope.isOwnCreate()) {
|
||||
if (Objects.equals(allDept, 1) && Func.isNotEmpty(deptId) && deptId > 0) {
|
||||
return Collections.singletonList(deptId);
|
||||
|
||||
+45
-13
@@ -32,6 +32,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.hcs.config.HcsProperties;
|
||||
import org.springblade.hcs.constant.HcsConstant;
|
||||
@@ -61,6 +62,7 @@ import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.CustomerArchive;
|
||||
import org.springblade.transport.pojo.entity.HcsContractCheckLog;
|
||||
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
import org.springblade.transport.support.ContractPartySupport;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -577,44 +579,74 @@ public class HcsContractSyncSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* signer 相对方(签署方):甲/乙双方。fdType 档案优先:客商档案为内部组织(customerKind=internal/内部组织)
|
||||
* signer 相对方(签署方):甲/乙/丙多方客商。fdType 档案优先:客商档案为内部组织(customerKind=internal/内部组织)
|
||||
* 的一方为我方签约主体传 inner,另一方传 customer(外部相对方,2026-10-14 联调口径);
|
||||
* 双方均非内部组织或查不到档案时按合同类别推定(承运商合同甲=inner,客户合同乙=inner,其余 customer);
|
||||
* 丙方(三方合同)TMS 无签约主体字段来源,暂不支持
|
||||
* 多方时按明细展开,同地位多客商分别签署
|
||||
*/
|
||||
private List<HcsSignerDTO> buildSigners(ContractManage contractManage) {
|
||||
List<HcsSignerDTO> signers = new ArrayList<>();
|
||||
// 甲乙方各反查一次客商档案,编号与内部组织判定共用查询结果
|
||||
CustomerArchive partyAArchive = queryCustomerArchive(contractManage.getPartyA());
|
||||
CustomerArchive partyBArchive = queryCustomerArchive(contractManage.getPartyB());
|
||||
List<String> partyANames = partyNames(contractManage.getPartyAJson(), contractManage.getPartyA());
|
||||
List<String> partyBNames = partyNames(contractManage.getPartyBJson(), contractManage.getPartyB());
|
||||
List<String> partyCNames = partyNames(contractManage.getPartyCJson(), contractManage.getPartyC());
|
||||
String firstPartyA = partyANames.isEmpty() ? null : partyANames.get(0);
|
||||
String firstPartyB = partyBNames.isEmpty() ? null : partyBNames.get(0);
|
||||
CustomerArchive partyAArchive = queryCustomerArchive(firstPartyA);
|
||||
CustomerArchive partyBArchive = queryCustomerArchive(firstPartyB);
|
||||
boolean partyAInternal = HcsCreditCheckSupport.isInternalCustomer(partyAArchive);
|
||||
boolean partyBInternal = HcsCreditCheckSupport.isInternalCustomer(partyBArchive);
|
||||
String partyAType;
|
||||
String partyBType;
|
||||
String partyCType;
|
||||
if (partyAInternal || partyBInternal) {
|
||||
// 档案命中:内部组织一方=我方签约主体(inner),外部一方统一 customer
|
||||
partyAType = partyAInternal ? HcsConstant.SIGNER_FD_TYPE_INNER : HcsConstant.SIGNER_FD_TYPE_CUSTOMER;
|
||||
partyBType = partyBInternal ? HcsConstant.SIGNER_FD_TYPE_INNER : HcsConstant.SIGNER_FD_TYPE_CUSTOMER;
|
||||
partyCType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER;
|
||||
} else if ("承运商合同".equals(contractManage.getContractCategory())) {
|
||||
// 兜底:档案未命中内部组织,按类别推定——我方为付款/发包方,签约主体在甲方
|
||||
partyAType = HcsConstant.SIGNER_FD_TYPE_INNER;
|
||||
partyBType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER;
|
||||
partyCType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER;
|
||||
} else if ("客户合同".equals(contractManage.getContractCategory())) {
|
||||
// 兜底:我方为收款/承运方,签约主体在乙方
|
||||
partyAType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER;
|
||||
partyBType = HcsConstant.SIGNER_FD_TYPE_INNER;
|
||||
partyCType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER;
|
||||
} else {
|
||||
// 兜底:其他类别双方统一 customer(2026-10-14 联调口径)
|
||||
partyAType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER;
|
||||
partyBType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER;
|
||||
partyCType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER;
|
||||
}
|
||||
for (String partyName : partyANames) {
|
||||
CustomerArchive archive = Objects.equals(partyName, firstPartyA) ? partyAArchive : queryCustomerArchive(partyName);
|
||||
signers.add(signer("甲", partyAType, partyName, archive));
|
||||
}
|
||||
for (String partyName : partyBNames) {
|
||||
CustomerArchive archive = Objects.equals(partyName, firstPartyB) ? partyBArchive : queryCustomerArchive(partyName);
|
||||
signers.add(signer("乙", partyBType, partyName, archive));
|
||||
}
|
||||
for (String partyName : partyCNames) {
|
||||
signers.add(signer("丙", partyCType, partyName, queryCustomerArchive(partyName)));
|
||||
}
|
||||
// 甲方相对方:fdStanding=甲(编号按名称反查,反查不到留空,§9-U6)
|
||||
signers.add(signer("甲", partyAType, contractManage.getPartyA(), partyAArchive));
|
||||
// 乙方相对方:fdStanding=乙
|
||||
signers.add(signer("乙", partyBType, contractManage.getPartyB(), partyBArchive));
|
||||
return signers;
|
||||
}
|
||||
|
||||
private List<String> partyNames(String partyJson, String fallback) {
|
||||
List<String> names = new ArrayList<>();
|
||||
for (Map<String, Object> row : ContractPartySupport.parsePartyRows(partyJson)) {
|
||||
Object customerName = row.get("customerName");
|
||||
if (customerName != null && Func.isNotEmpty(String.valueOf(customerName).trim())) {
|
||||
names.add(String.valueOf(customerName).trim());
|
||||
}
|
||||
}
|
||||
if (names.isEmpty() && Func.isNotEmpty(fallback)) {
|
||||
for (String part : fallback.split("[、,]")) {
|
||||
if (Func.isNotEmpty(part) && Func.isNotEmpty(part.trim())) {
|
||||
names.add(part.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个相对方:fdType 由签约主体落位规则决定(我方主体 inner,外部相对方 customer)
|
||||
*/
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
|
||||
* Copyright of this software remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.task;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 金蝶冲销状态定时回写任务
|
||||
* <p>
|
||||
* A102-4/A302-4 批量查询已同步结算单对应金蝶旧应付/应收单的 hadwrittenoff(部分冲销视同已冲销),
|
||||
* 回写结算单冲销状态,用于结算单列表展示金蝶旧单是否已冲销。
|
||||
* 业务逻辑收敛在服务方法(分布式锁 + 分批查询 + 批量落库),便于手动触发联调。
|
||||
* <p>
|
||||
* 调度配置(Nacos thirdParty.bfi.writeOffResult.*):syncEnabled 开关、cron 表达式、queryBatchSize 分批大小。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RefreshScope
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeWriteOffResultTask {
|
||||
|
||||
private final IFormalSettlementService formalSettlementService;
|
||||
private final BfiProperties bfiProperties;
|
||||
|
||||
/** 金蝶冲销状态回写调度,默认每10分钟执行一次(分钟位与付款结果回写错峰) */
|
||||
@Scheduled(cron = "${thirdParty.bfi.writeOffResult.cron:0 4/10 * * * ?}")
|
||||
public void syncKingdeeWriteOffResult() {
|
||||
if (!bfiProperties.getWriteOffResult().isSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String summary = formalSettlementService.syncKingdeeWriteOffResult();
|
||||
log.info("定时回写金蝶冲销状态:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时回写金蝶冲销状态异常", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-4
@@ -23,12 +23,11 @@
|
||||
package org.springblade.transport.wrapper;
|
||||
|
||||
import org.springblade.core.mp.support.BaseEntityWrapper;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.pojo.entity.CommonCargo;
|
||||
import org.springblade.transport.pojo.vo.CommonCargoVO;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
@@ -52,8 +51,7 @@ public class CommonCargoWrapper extends BaseEntityWrapper<CommonCargo, CommonCar
|
||||
}
|
||||
commonCargoVO.setCreateUserName(UserCache.getUserRealName(commonCargo.getCreateUser()));
|
||||
commonCargoVO.setUpdateUserName(UserCache.getUserRealName(commonCargo.getUpdateUser()));
|
||||
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
commonCargoVO.setReadonly(currentDeptId != null && !Objects.equals(commonCargo.getDeptId(), currentDeptId));
|
||||
commonCargoVO.setReadonly(TransportBusinessSupport.isDeptReadonly(commonCargo.getDeptId(), TransportBusinessSupport.MENU_COMMON_CARGO));
|
||||
return commonCargoVO;
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -23,12 +23,11 @@
|
||||
package org.springblade.transport.wrapper;
|
||||
|
||||
import org.springblade.core.mp.support.BaseEntityWrapper;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.pojo.entity.CommonRoute;
|
||||
import org.springblade.transport.pojo.vo.CommonRouteVO;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -48,8 +47,7 @@ public class CommonRouteWrapper extends BaseEntityWrapper<CommonRoute, CommonRou
|
||||
CommonRouteVO commonRouteVO = Objects.requireNonNull(BeanUtil.copyProperties(commonRoute, CommonRouteVO.class));
|
||||
commonRouteVO.setCreateUserName(UserCache.getUserRealName(commonRoute.getCreateUser()));
|
||||
commonRouteVO.setUpdateUserName(UserCache.getUserRealName(commonRoute.getUpdateUser()));
|
||||
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
commonRouteVO.setReadonly(currentDeptId != null && !Objects.equals(commonRoute.getDeptId(), currentDeptId));
|
||||
commonRouteVO.setReadonly(TransportBusinessSupport.isDeptReadonly(commonRoute.getDeptId(), TransportBusinessSupport.MENU_COMMON_ROUTE));
|
||||
return commonRouteVO;
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -5,12 +5,11 @@
|
||||
package org.springblade.transport.wrapper;
|
||||
|
||||
import org.springblade.core.mp.support.BaseEntityWrapper;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
@@ -31,8 +30,7 @@ public class LoadingManageWrapper extends BaseEntityWrapper<LoadingManage, Loadi
|
||||
LoadingManageVO loadingManageVO = Objects.requireNonNull(BeanUtil.copyProperties(loadingManage, LoadingManageVO.class));
|
||||
loadingManageVO.setCreateUserName(UserCache.getUserRealName(loadingManage.getCreateUser()));
|
||||
loadingManageVO.setUpdateUserName(UserCache.getUserRealName(loadingManage.getUpdateUser()));
|
||||
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
loadingManageVO.setReadonly(currentDeptId != null && !Objects.equals(loadingManage.getDeptId(), currentDeptId));
|
||||
loadingManageVO.setReadonly(TransportBusinessSupport.isDeptReadonly(loadingManage.getDeptId(), TransportBusinessSupport.MENU_LOADING_MANAGE));
|
||||
loadingManageVO.setBusinessStatusName(businessStatusName(loadingManage.getBusinessStatus()));
|
||||
// 未维护里程的占位值 -1 对外展示为空
|
||||
if (loadingManageVO.getMileage() != null
|
||||
|
||||
+2
-4
@@ -23,12 +23,11 @@
|
||||
package org.springblade.transport.wrapper;
|
||||
|
||||
import org.springblade.core.mp.support.BaseEntityWrapper;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.pojo.entity.ProcessConfig;
|
||||
import org.springblade.transport.pojo.vo.ProcessConfigVO;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -48,8 +47,7 @@ public class ProcessConfigWrapper extends BaseEntityWrapper<ProcessConfig, Proce
|
||||
ProcessConfigVO processConfigVO = Objects.requireNonNull(BeanUtil.copyProperties(processConfig, ProcessConfigVO.class));
|
||||
processConfigVO.setCreateUserName(UserCache.getUserRealName(processConfig.getCreateUser()));
|
||||
processConfigVO.setUpdateUserName(UserCache.getUserRealName(processConfig.getUpdateUser()));
|
||||
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
processConfigVO.setReadonly(currentDeptId != null && !Objects.equals(processConfig.getDeptId(), currentDeptId));
|
||||
processConfigVO.setReadonly(TransportBusinessSupport.isDeptReadonly(processConfig.getDeptId(), TransportBusinessSupport.MENU_PROCESS_CONFIG));
|
||||
processConfigVO.setStatusName(statusName(processConfig.getStatus()));
|
||||
return processConfigVO;
|
||||
}
|
||||
|
||||
+2
-4
@@ -23,12 +23,11 @@
|
||||
package org.springblade.transport.wrapper;
|
||||
|
||||
import org.springblade.core.mp.support.BaseEntityWrapper;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.pojo.entity.ShippingTemplate;
|
||||
import org.springblade.transport.pojo.vo.ShippingTemplateVO;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -48,8 +47,7 @@ public class ShippingTemplateWrapper extends BaseEntityWrapper<ShippingTemplate,
|
||||
ShippingTemplateVO shippingTemplateVO = Objects.requireNonNull(BeanUtil.copyProperties(shippingTemplate, ShippingTemplateVO.class));
|
||||
shippingTemplateVO.setCreateUserName(UserCache.getUserRealName(shippingTemplate.getCreateUser()));
|
||||
shippingTemplateVO.setUpdateUserName(UserCache.getUserRealName(shippingTemplate.getUpdateUser()));
|
||||
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
shippingTemplateVO.setReadonly(currentDeptId != null && !Objects.equals(shippingTemplate.getDeptId(), currentDeptId));
|
||||
shippingTemplateVO.setReadonly(TransportBusinessSupport.isDeptReadonly(shippingTemplate.getDeptId(), TransportBusinessSupport.MENU_SHIPPING_TEMPLATE));
|
||||
return shippingTemplateVO;
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -27,6 +27,7 @@ package org.springblade.transport.wrapper;
|
||||
|
||||
import org.springblade.core.mp.support.BaseEntityWrapper;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.system.cache.UserDisplayCache;
|
||||
import org.springblade.transport.pojo.entity.TransportCertRecord;
|
||||
@@ -70,6 +71,8 @@ public class TransportCertRecordWrapper extends BaseEntityWrapper<TransportCertR
|
||||
transportCertRecordVO.setAuditUserName(UserCache.getUserRealName(transportCertRecordVO.getAuditUser()));
|
||||
// 状态文案统一在此翻译,避免小程序、Web、接口各处各造一套说法
|
||||
transportCertRecordVO.setStatusText(CertificationRuleSupport.statusText(transportCertRecordVO.getStatus()));
|
||||
// 是否经过审核员修订:修订版非空才算,空串与空白都不算(前端清空输入框会传空串)
|
||||
transportCertRecordVO.setRevised(Func.isNotBlank(transportCertRecordVO.getAuditedSnapshotJson()));
|
||||
return transportCertRecordVO;
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -23,9 +23,7 @@
|
||||
package org.springblade.transport.wrapper;
|
||||
|
||||
import org.springblade.core.mp.support.BaseEntityWrapper;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.pojo.entity.TransportPlan;
|
||||
import org.springblade.transport.pojo.vo.TransportPlanVO;
|
||||
@@ -50,8 +48,7 @@ public class TransportPlanWrapper extends BaseEntityWrapper<TransportPlan, Trans
|
||||
transportPlanVO.setCreateUserName(UserCache.getUserRealName(transportPlan.getCreateUser()));
|
||||
transportPlanVO.setUpdateUserName(UserCache.getUserRealName(transportPlan.getUpdateUser()));
|
||||
transportPlanVO.setDataSource(TransportBusinessSupport.normalizeTransportPlanDataSource(transportPlan.getDataSource()));
|
||||
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
transportPlanVO.setReadonly(currentDeptId != null && !Objects.equals(transportPlan.getDeptId(), currentDeptId));
|
||||
transportPlanVO.setReadonly(TransportBusinessSupport.isDeptReadonly(transportPlan.getDeptId(), TransportBusinessSupport.MENU_TRANSPORT_PLAN));
|
||||
transportPlanVO.setBusinessStatusName(businessStatusName(transportPlan.getBusinessStatus()));
|
||||
return transportPlanVO;
|
||||
}
|
||||
|
||||
+1
-3
@@ -23,7 +23,6 @@
|
||||
package org.springblade.transport.wrapper;
|
||||
|
||||
import org.springblade.core.mp.support.BaseEntityWrapper;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.DictBizCache;
|
||||
@@ -54,8 +53,7 @@ public class WaybillWrapper extends BaseEntityWrapper<Waybill, WaybillVO> {
|
||||
waybillVO.setCreateUserName(UserCache.getUserRealName(waybill.getCreateUser()));
|
||||
waybillVO.setUpdateUserName(UserCache.getUserRealName(waybill.getUpdateUser()));
|
||||
waybillVO.setDataSource(TransportBusinessSupport.normalizeWaybillDataSource(waybill.getDataSource()));
|
||||
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
waybillVO.setReadonly(currentDeptId != null && !Objects.equals(waybill.getDeptId(), currentDeptId));
|
||||
waybillVO.setReadonly(TransportBusinessSupport.isDeptReadonly(waybill.getDeptId(), TransportBusinessSupport.MENU_WAYBILL_MANAGE));
|
||||
String displayStatus = WaybillProcessSupport.normalizeBusinessStatus(
|
||||
waybill.getBusinessStatus(), waybill.getProcessJson(), waybill.getDriverAcceptStatus());
|
||||
waybillVO.setBusinessStatus(displayStatus);
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.transport.service.ICertAuditService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 认证审核接口的入参绑定测试。
|
||||
* <p>
|
||||
* 同一字段 body 与 query 都可传,两处都给时以 body 为准。
|
||||
*/
|
||||
class CertificationAuditEndpointTest {
|
||||
|
||||
private static final Long RECORD_ID = 5001L;
|
||||
private static final String REVISION = "{\"realName\":\"李四\",\"idCardNo\":\"450100199001019999\"}";
|
||||
|
||||
private ICertAuditService certAuditService;
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
certAuditService = mock(ICertAuditService.class);
|
||||
when(certAuditService.audit(any(), anyBoolean(), any(), any())).thenReturn(true);
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(new CertificationAuditController(certAuditService)).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("老调用:只带 query 的 id/pass/rejectReason,行为不变")
|
||||
void legacyQueryOnlyStillWorks() throws Exception {
|
||||
mockMvc.perform(post("/certification-audit/audit")
|
||||
.param("id", String.valueOf(RECORD_ID))
|
||||
.param("pass", "true"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
verify(certAuditService).audit(RECORD_ID, true, null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("新调用:body 携带修订内容,透传到服务层")
|
||||
void bodyCarriesRevision() throws Exception {
|
||||
String body = """
|
||||
{"id": 5001, "pass": true, "auditedSnapshotJson": "{\\"realName\\":\\"李四\\",\\"idCardNo\\":\\"450100199001019999\\"}"}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/certification-audit/audit")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
verify(certAuditService).audit(RECORD_ID, true, null, REVISION);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("两种都带:以 body 为准(query 里的旧值不得覆盖修订)")
|
||||
void bodyWinsOverQuery() throws Exception {
|
||||
String body = """
|
||||
{"id": 5001, "pass": true}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/certification-audit/audit")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body)
|
||||
.param("id", "6001")
|
||||
.param("pass", "false"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
verify(certAuditService).audit(RECORD_ID, true, null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("body 只给一半时,缺的字段回退到 query")
|
||||
void partialBodyFallsBackToQuery() throws Exception {
|
||||
String body = """
|
||||
{"id": 5001, "pass": false}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/certification-audit/audit")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body)
|
||||
.param("rejectReason", "行驶证模糊"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
verify(certAuditService).audit(RECORD_ID, false, "行驶证模糊", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("缺少 id 或 pass:按业务失败返回(code=400),不能把 null 传进服务层")
|
||||
void missingIdOrPassIsRejected() throws Exception {
|
||||
mockMvc.perform(post("/certification-audit/audit").param("pass", "true"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(400));
|
||||
|
||||
mockMvc.perform(post("/certification-audit/audit").param("id", String.valueOf(RECORD_ID)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(400));
|
||||
|
||||
verify(certAuditService, never()).audit(anyLong(), anyBoolean(), any(), any());
|
||||
}
|
||||
}
|
||||
+265
-22
@@ -96,7 +96,7 @@ class CertAuditServiceImplTest {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
service.audit(RECORD_ID, true, null);
|
||||
service.audit(RECORD_ID, true, null, null);
|
||||
|
||||
ArgumentCaptor<TransportCertRecord> recordCaptor = ArgumentCaptor.forClass(TransportCertRecord.class);
|
||||
verify(certRecordService).updateById(recordCaptor.capture());
|
||||
@@ -118,7 +118,7 @@ class CertAuditServiceImplTest {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
service.audit(RECORD_ID, true, null);
|
||||
service.audit(RECORD_ID, true, null, null);
|
||||
|
||||
ArgumentCaptor<TransportVehicle> captor = ArgumentCaptor.forClass(TransportVehicle.class);
|
||||
verify(transportVehicleService).updateById(captor.capture());
|
||||
@@ -141,7 +141,7 @@ class CertAuditServiceImplTest {
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.audit(RECORD_ID, true, null));
|
||||
() -> service.audit(RECORD_ID, true, null, null));
|
||||
assertTrue(exception.getMessage().contains("字段定义不匹配"), exception.getMessage());
|
||||
verify(transportVehicleService, never()).updateById(any(TransportVehicle.class));
|
||||
}
|
||||
@@ -154,7 +154,7 @@ class CertAuditServiceImplTest {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(oldEffectiveId));
|
||||
|
||||
service.audit(RECORD_ID, true, null);
|
||||
service.audit(RECORD_ID, true, null, null);
|
||||
|
||||
ArgumentCaptor<TransportVehicle> captor = ArgumentCaptor.forClass(TransportVehicle.class);
|
||||
verify(transportVehicleService).updateById(captor.capture());
|
||||
@@ -169,7 +169,7 @@ class CertAuditServiceImplTest {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING));
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
service.audit(RECORD_ID, true, null);
|
||||
service.audit(RECORD_ID, true, null, null);
|
||||
|
||||
ArgumentCaptor<TransportVehicle> captor = ArgumentCaptor.forClass(TransportVehicle.class);
|
||||
verify(transportVehicleService).updateById(captor.capture());
|
||||
@@ -183,7 +183,7 @@ class CertAuditServiceImplTest {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(driverService.getById(DRIVER_ID)).thenReturn(driver(null));
|
||||
|
||||
service.audit(RECORD_ID, true, null);
|
||||
service.audit(RECORD_ID, true, null, null);
|
||||
|
||||
ArgumentCaptor<Driver> captor = ArgumentCaptor.forClass(Driver.class);
|
||||
verify(driverService).updateById(captor.capture());
|
||||
@@ -199,7 +199,7 @@ class CertAuditServiceImplTest {
|
||||
existing.setDrivingVehicle("桂A12345,桂B67890");
|
||||
when(driverService.getById(DRIVER_ID)).thenReturn(existing);
|
||||
|
||||
service.audit(RECORD_ID, true, null);
|
||||
service.audit(RECORD_ID, true, null, null);
|
||||
|
||||
ArgumentCaptor<Driver> captor = ArgumentCaptor.forClass(Driver.class);
|
||||
verify(driverService).updateById(captor.capture());
|
||||
@@ -216,7 +216,7 @@ class CertAuditServiceImplTest {
|
||||
when(transportVehicleService.updateById(any(TransportVehicle.class))).thenReturn(false);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.audit(RECORD_ID, true, null));
|
||||
() -> service.audit(RECORD_ID, true, null, null));
|
||||
assertTrue(exception.getMessage().contains("未生效"), exception.getMessage());
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ class CertAuditServiceImplTest {
|
||||
when(driverService.updateById(any(Driver.class))).thenReturn(false);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.audit(RECORD_ID, true, null));
|
||||
() -> service.audit(RECORD_ID, true, null, null));
|
||||
assertTrue(exception.getMessage().contains("司机"), exception.getMessage());
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ class CertAuditServiceImplTest {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(existingEffectiveId));
|
||||
|
||||
service.audit(RECORD_ID, false, "证件照片模糊");
|
||||
service.audit(RECORD_ID, false, "证件照片模糊", null);
|
||||
|
||||
ArgumentCaptor<TransportCertRecord> recordCaptor = ArgumentCaptor.forClass(TransportCertRecord.class);
|
||||
verify(certRecordService).updateById(recordCaptor.capture());
|
||||
@@ -264,7 +264,7 @@ class CertAuditServiceImplTest {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING));
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(existingEffectiveId));
|
||||
|
||||
service.audit(RECORD_ID, false, "运输证过期");
|
||||
service.audit(RECORD_ID, false, "运输证过期", null);
|
||||
|
||||
// 指针字段没有被写成 null,也没有被写成新记录
|
||||
ArgumentCaptor<TransportVehicle> captor = ArgumentCaptor.forClass(TransportVehicle.class);
|
||||
@@ -277,8 +277,8 @@ class CertAuditServiceImplTest {
|
||||
void rejectRequiresReason() {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING));
|
||||
|
||||
assertThrows(ServiceException.class, () -> service.audit(RECORD_ID, false, null));
|
||||
assertThrows(ServiceException.class, () -> service.audit(RECORD_ID, false, " "));
|
||||
assertThrows(ServiceException.class, () -> service.audit(RECORD_ID, false, null, null));
|
||||
assertThrows(ServiceException.class, () -> service.audit(RECORD_ID, false, " ", null));
|
||||
verify(certRecordService, never()).updateById(any(TransportCertRecord.class));
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ class CertAuditServiceImplTest {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING));
|
||||
|
||||
assertThrows(ServiceException.class,
|
||||
() -> service.audit(RECORD_ID, false, "长".repeat(201)));
|
||||
() -> service.audit(RECORD_ID, false, "长".repeat(201), null));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 终态保护
|
||||
@@ -303,9 +303,9 @@ class CertAuditServiceImplTest {
|
||||
TransportCertRecord record = vehicleRecord(terminal);
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
|
||||
assertThrows(ServiceException.class, () -> service.audit(RECORD_ID, true, null),
|
||||
assertThrows(ServiceException.class, () -> service.audit(RECORD_ID, true, null, null),
|
||||
"状态 " + terminal + " 属终态,不应可再审");
|
||||
assertThrows(ServiceException.class, () -> service.audit(RECORD_ID, false, "驳回"),
|
||||
assertThrows(ServiceException.class, () -> service.audit(RECORD_ID, false, "驳回", null),
|
||||
"状态 " + terminal + " 属终态,不应可再审");
|
||||
}
|
||||
verify(certRecordService, never()).updateById(any(TransportCertRecord.class));
|
||||
@@ -315,13 +315,13 @@ class CertAuditServiceImplTest {
|
||||
@DisplayName("记录不存在:拒绝")
|
||||
void recordNotFound() {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(null);
|
||||
assertThrows(ServiceException.class, () -> service.audit(RECORD_ID, true, null));
|
||||
assertThrows(ServiceException.class, () -> service.audit(RECORD_ID, true, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("记录主键为空:拒绝")
|
||||
void recordIdRequired() {
|
||||
assertThrows(ServiceException.class, () -> service.audit(null, true, null));
|
||||
assertThrows(ServiceException.class, () -> service.audit(null, true, null, null));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 审核详情(diff 用)
|
||||
@@ -470,7 +470,7 @@ class CertAuditServiceImplTest {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
service.audit(RECORD_ID, true, null);
|
||||
service.audit(RECORD_ID, true, null, null);
|
||||
|
||||
ArgumentCaptor<Wrapper<TransportVehicle>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(transportVehicleService).update(captor.capture());
|
||||
@@ -494,7 +494,7 @@ class CertAuditServiceImplTest {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(driverService.getById(DRIVER_ID)).thenReturn(driver(null));
|
||||
|
||||
service.audit(RECORD_ID, true, null);
|
||||
service.audit(RECORD_ID, true, null, null);
|
||||
|
||||
ArgumentCaptor<Wrapper<Driver>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(driverService).update(captor.capture());
|
||||
@@ -518,7 +518,7 @@ class CertAuditServiceImplTest {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(777L));
|
||||
|
||||
service.audit(RECORD_ID, false, " 行驶证模糊 ");
|
||||
service.audit(RECORD_ID, false, " 行驶证模糊 ", null);
|
||||
|
||||
ArgumentCaptor<TransportVehicle> captor = ArgumentCaptor.forClass(TransportVehicle.class);
|
||||
verify(transportVehicleService).updateById(captor.capture());
|
||||
@@ -527,6 +527,236 @@ class CertAuditServiceImplTest {
|
||||
verify(transportVehicleService, never()).update(any());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 修订版
|
||||
|
||||
@Test
|
||||
@DisplayName("修订版:通过时写入新列,且 snapshot_json 一个字都不改")
|
||||
void approveStoresAuditedSnapshot() {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING));
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
String revision = vehicleRevision();
|
||||
|
||||
service.audit(RECORD_ID, true, null, revision);
|
||||
|
||||
ArgumentCaptor<TransportCertRecord> captor = ArgumentCaptor.forClass(TransportCertRecord.class);
|
||||
verify(certRecordService).updateById(captor.capture());
|
||||
assertEquals(revision, captor.getValue().getAuditedSnapshotJson(),
|
||||
"修订版必须原样留痕,供事后追责");
|
||||
assertNull(captor.getValue().getSnapshotJson(),
|
||||
"snapshot_json 是司机当时提交的内容,审核 patch 里不得出现");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("修订版:写回车辆档案的是修订内容,不是司机原提交")
|
||||
void approveAppliesRevisionToVehicleProfile() {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING));
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
String revision = vehicleRevision();
|
||||
|
||||
service.audit(RECORD_ID, true, null, revision);
|
||||
|
||||
ArgumentCaptor<TransportVehicle> captor = ArgumentCaptor.forClass(TransportVehicle.class);
|
||||
verify(transportVehicleService).updateById(captor.capture());
|
||||
assertEquals("修订后的所有人", captor.getValue().getOwnerName(), "档案要落修订后的内容");
|
||||
assertEquals(25000, captor.getValue().getApprovedLoadKg());
|
||||
assertEquals("450100999999", captor.getValue().getDrivingLicenseNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("修订版:司机档案同样以修订内容写回")
|
||||
void approveAppliesRevisionToDriverProfile() {
|
||||
TransportCertRecord record = driverRecord(TransportCertRecord.STATUS_PENDING);
|
||||
record.setSnapshotJson("{}");
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(driverService.getById(DRIVER_ID)).thenReturn(driver(null));
|
||||
String revision = "{\"realName\":\"李四\",\"idCardNo\":\"450100199001019999\","
|
||||
+ "\"driverFileNo\":\"450100888899\"}";
|
||||
|
||||
service.audit(RECORD_ID, true, null, revision);
|
||||
|
||||
ArgumentCaptor<Driver> captor = ArgumentCaptor.forClass(Driver.class);
|
||||
verify(driverService).updateById(captor.capture());
|
||||
assertEquals("李四", captor.getValue().getDriverName());
|
||||
assertEquals("450100199001019999", captor.getValue().getIdCardNo());
|
||||
assertEquals("450100888899", captor.getValue().getDrivingLicenseNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("修订版:未提供时不写新列,生效内容仍是司机原提交")
|
||||
void approveWithoutRevisionLeavesColumnNull() {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING));
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
service.audit(RECORD_ID, true, null, null);
|
||||
|
||||
ArgumentCaptor<TransportCertRecord> recordCaptor = ArgumentCaptor.forClass(TransportCertRecord.class);
|
||||
verify(certRecordService).updateById(recordCaptor.capture());
|
||||
assertNull(recordCaptor.getValue().getAuditedSnapshotJson(),
|
||||
"未修订时该列保持 NULL,不能塞一份复制品进来");
|
||||
ArgumentCaptor<TransportVehicle> vehicleCaptor = ArgumentCaptor.forClass(TransportVehicle.class);
|
||||
verify(transportVehicleService).updateById(vehicleCaptor.capture());
|
||||
assertEquals("广西现代物流集团有限公司", vehicleCaptor.getValue().getOwnerName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("修订版:纯空白等于未修订(前端清空输入框就是这种情况)")
|
||||
void blankRevisionIsTreatedAsAbsent() {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING));
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
service.audit(RECORD_ID, true, null, " ");
|
||||
|
||||
ArgumentCaptor<TransportCertRecord> captor = ArgumentCaptor.forClass(TransportCertRecord.class);
|
||||
verify(certRecordService).updateById(captor.capture());
|
||||
assertNull(captor.getValue().getAuditedSnapshotJson());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("修订版:JSON 无法解析时拒绝,且记录与档案都不写(事务回滚)")
|
||||
void approveRejectsMalformedRevision() {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING));
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.audit(RECORD_ID, true, null, "{不是JSON"));
|
||||
assertTrue(exception.getMessage().contains("修订内容格式不正确"), exception.getMessage());
|
||||
verify(certRecordService, never()).updateById(any(TransportCertRecord.class));
|
||||
verify(transportVehicleService, never()).updateById(any(TransportVehicle.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("修订版:司机缺身份证号与驾驶证号时拒绝(写回等于什么也没写)")
|
||||
void approveRejectsIncompleteDriverRevision() {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(driverRecord(TransportCertRecord.STATUS_PENDING));
|
||||
when(driverService.getById(DRIVER_ID)).thenReturn(driver(null));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.audit(RECORD_ID, true, null, "{\"realName\":\"李四\"}"));
|
||||
assertTrue(exception.getMessage().contains("身份证号"), exception.getMessage());
|
||||
verify(driverService, never()).updateById(any(Driver.class));
|
||||
verify(certRecordService, never()).updateById(any(TransportCertRecord.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("修订版:车辆缺行驶证号与核定载质量时拒绝")
|
||||
void approveRejectsIncompleteVehicleRevision() {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING));
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.audit(RECORD_ID, true, null, "{\"ownerName\":\"只改了个名字\"}"));
|
||||
assertTrue(exception.getMessage().contains("核定载质量"), exception.getMessage());
|
||||
verify(transportVehicleService, never()).updateById(any(TransportVehicle.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("驳回:修订内容完全忽略,新列保持 NULL")
|
||||
void rejectIgnoresRevision() {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING));
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
|
||||
service.audit(RECORD_ID, false, "行驶证模糊", vehicleRevision());
|
||||
|
||||
ArgumentCaptor<TransportCertRecord> captor = ArgumentCaptor.forClass(TransportCertRecord.class);
|
||||
verify(certRecordService).updateById(captor.capture());
|
||||
assertNull(captor.getValue().getAuditedSnapshotJson(),
|
||||
"驳回不产生生效内容,留修订版只会让人误读");
|
||||
assertEquals(TransportCertRecord.STATUS_REJECTED, captor.getValue().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空快照不再静默通过:司机记录没有内容、也没有修订时拒绝")
|
||||
void approveRejectsEmptyDriverSnapshot() {
|
||||
TransportCertRecord record = driverRecord(TransportCertRecord.STATUS_PENDING);
|
||||
record.setSnapshotJson(null);
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(driverService.getById(DRIVER_ID)).thenReturn(driver(null));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.audit(RECORD_ID, true, null, null));
|
||||
assertTrue(exception.getMessage().contains("司机档案"), exception.getMessage());
|
||||
verify(driverService, never()).updateById(any(Driver.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("审核详情:修订版内容随详情下发,生效一侧同样修订版优先")
|
||||
void detailExposesAuditedSnapshot() {
|
||||
TransportCertRecord record = vehicleRecord(TransportCertRecord.STATUS_PENDING);
|
||||
record.setAuditedSnapshotJson("{\"vin\":\"REVISED\"}");
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
|
||||
TransportCertRecord effective = new TransportCertRecord();
|
||||
effective.setId(777L);
|
||||
effective.setSnapshotJson("{\"vin\":\"OLD\"}");
|
||||
effective.setAuditedSnapshotJson("{\"vin\":\"OLD-REVISED\"}");
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(777L));
|
||||
when(certRecordService.getById(777L)).thenReturn(effective);
|
||||
|
||||
CertAuditDetailVO detail = service.detail(RECORD_ID);
|
||||
|
||||
assertEquals("{\"vin\":\"REVISED\"}", detail.getAuditedSnapshotJson());
|
||||
assertEquals("{\"vin\":\"OLD-REVISED\"}", detail.getEffectiveSnapshotJson(),
|
||||
"生效一侧也修订版优先,否则审核页拿旧值做对比");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("身份不可修订:司机修订改了身份证号时拒绝,记录不被改动")
|
||||
void approveRejectsDriverIdentityChange() {
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(driverRecord(TransportCertRecord.STATUS_PENDING));
|
||||
when(driverService.getById(DRIVER_ID)).thenReturn(driver(null));
|
||||
String revision = "{\"realName\":\"李四\",\"idCardNo\":\"450100199001019999\","
|
||||
+ "\"driverFileNo\":\"450100888888\"}";
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.audit(RECORD_ID, true, null, revision));
|
||||
|
||||
assertTrue(exception.getMessage().contains("身份证号"), exception.getMessage());
|
||||
verify(driverService, never()).updateById(any(Driver.class));
|
||||
verify(certRecordService, never()).updateById(any(TransportCertRecord.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("身份不可修订:车辆修订改了车牌号时拒绝(车牌不参与写回,也不能被悄悄换掉)")
|
||||
void approveRejectsVehicleIdentityChange() {
|
||||
String snapshot = "{\"plateNo\":\"桂A32523\",\"drivingLicenseNo\":\"450100123456\","
|
||||
+ "\"approvedLoadKg\":18000,\"ownerName\":\"广西现代物流集团有限公司\"}";
|
||||
when(certRecordService.getById(RECORD_ID))
|
||||
.thenReturn(vehicleRecord(TransportCertRecord.STATUS_PENDING, snapshot));
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(null));
|
||||
String revision = "{\"plateNo\":\"桂A99999\",\"drivingLicenseNo\":\"450100999999\","
|
||||
+ "\"approvedLoadKg\":25000}";
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.audit(RECORD_ID, true, null, revision));
|
||||
|
||||
assertTrue(exception.getMessage().contains("车牌号"), exception.getMessage());
|
||||
verify(transportVehicleService, never()).updateById(any(TransportVehicle.class));
|
||||
verify(certRecordService, never()).updateById(any(TransportCertRecord.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("身份不可修订:原提交该字段为空时允许补全,正常通过")
|
||||
void approveAllowsCompletingEmptyIdentity() {
|
||||
TransportCertRecord record = driverRecord(TransportCertRecord.STATUS_PENDING);
|
||||
record.setSnapshotJson("{\"realName\":\"张三\"}");
|
||||
when(certRecordService.getById(RECORD_ID)).thenReturn(record);
|
||||
when(driverService.getById(DRIVER_ID)).thenReturn(driver(null));
|
||||
String revision = "{\"realName\":\"张三\",\"idCardNo\":\"450100199001011234\","
|
||||
+ "\"driverFileNo\":\"450100888888\"}";
|
||||
|
||||
service.audit(RECORD_ID, true, null, revision);
|
||||
|
||||
ArgumentCaptor<Driver> captor = ArgumentCaptor.forClass(Driver.class);
|
||||
verify(driverService).updateById(captor.capture());
|
||||
assertEquals("450100199001011234", captor.getValue().getIdCardNo(),
|
||||
"原提交没有身份证号时修订版可以补上");
|
||||
}
|
||||
|
||||
private static String vehicleRevision() {
|
||||
return "{\"drivingLicenseNo\":\"450100999999\",\"approvedLoadKg\":25000,"
|
||||
+ "\"ownerName\":\"修订后的所有人\"}";
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 辅助
|
||||
|
||||
private TransportCertRecord vehicleRecord(Integer status) {
|
||||
@@ -556,6 +786,19 @@ class CertAuditServiceImplTest {
|
||||
+ "\"vehicleType\":\"重型货车\",\"driveLicenseImage\":\"http://oss/xsz.jpg\"}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 一份字段齐全的司机认证快照
|
||||
* <p>
|
||||
* 司机侧的空快照不再「静默跳过」而是拒审,故通过类用例不能再拿 "{}" 走流程;
|
||||
* 身份证号与驾驶证号是「这份快照是否为当前格式」的判据,缺一不可。
|
||||
*/
|
||||
static String completeDriverSnapshot() {
|
||||
return "{\"realName\":\"张三\",\"idCardNo\":\"450100199001011234\","
|
||||
+ "\"driverFileNo\":\"450100888888\",\"driverClass\":\"A2\","
|
||||
+ "\"headPhoto\":\"http://oss/head.jpg\",\"idCardFrontUrl\":\"http://oss/idcard-front.jpg\","
|
||||
+ "\"idCardBackUrl\":\"http://oss/idcard-back.jpg\"}";
|
||||
}
|
||||
|
||||
private TransportCertRecord driverRecord(Integer status) {
|
||||
TransportCertRecord record = new TransportCertRecord();
|
||||
record.setId(RECORD_ID);
|
||||
@@ -563,7 +806,7 @@ class CertAuditServiceImplTest {
|
||||
record.setBizId(DRIVER_ID);
|
||||
record.setBizKey("张三");
|
||||
record.setStatus(status);
|
||||
record.setSnapshotJson("{}");
|
||||
record.setSnapshotJson(completeDriverSnapshot());
|
||||
return record;
|
||||
}
|
||||
|
||||
|
||||
+69
-56
@@ -1,15 +1,16 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.system.pojo.entity.MpUser;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.entity.TransportVehicle;
|
||||
import org.springblade.transport.pojo.vo.DriverVehicleCardVO;
|
||||
@@ -17,19 +18,23 @@ import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.ITransportVehicleService;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
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.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 司机端车辆卡片(重新认证表单的数据来源)测试。
|
||||
* 司机端「当前司机」解析与车辆卡片(重新认证表单的数据来源)测试。
|
||||
* <p>
|
||||
* 卡片字段会**直接进表单**,所以这里盯的不是「显示得好不好看」,而是两条会让回显出错的契约:
|
||||
* 解析侧盯住 ADR-0028 的唯一入口:登录用户 → {@code user_id} → 档案,手机号只是档案上的一个字段,
|
||||
* 不参与查找;卡片字段会**直接进表单**,所以这里盯的不是「显示得好不好看」,而是两条会让回显出错的契约:
|
||||
* 日期字段只能是 {@code yyyy-MM-dd} 或空串(不能是「长期」这类文字);
|
||||
* 标了长期有效的字段不能再下发一个作废的止期。
|
||||
*/
|
||||
@@ -42,11 +47,17 @@ class DriverAppServiceImplTest {
|
||||
private ITransportVehicleService transportVehicleService;
|
||||
private DriverAppServiceImpl service;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
// 实体元数据就绪后,LambdaQueryWrapper 才能把 Driver::getUserId 解析成列名
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), Driver.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
driverService = mock(IDriverService.class);
|
||||
transportVehicleService = mock(ITransportVehicleService.class);
|
||||
service = new DriverAppServiceImpl(driverService, transportVehicleService, mock(IMpUserClient.class));
|
||||
service = new DriverAppServiceImpl(driverService, transportVehicleService);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -85,33 +96,69 @@ class DriverAppServiceImplTest {
|
||||
assertEquals("2028-12-04", card.getRoadTransportCertStartDate());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 手机号解析
|
||||
// ---------------------------------------------------------------- 当前司机解析
|
||||
|
||||
@Test
|
||||
@DisplayName("12 位手机号也能解析出本人号码——库里存在 12 位司机账号,写死 11 位会让他们查不到自己的档案")
|
||||
void twelveDigitAccountIsUsableAsPhone() {
|
||||
assertEquals("黄大富", currentByPhone("175874212082", "175874212082", null).getDriverName());
|
||||
@DisplayName("同号两条档案时取登录人自己那条——按手机号取「最近更新的一条」会把冒名者当成当前司机")
|
||||
void currentDriverComesFromUserIdNotMobile() {
|
||||
Driver owner = driver(9001L, USER_ID, "黄大富", MOBILE);
|
||||
owner.setUpdateTime(new Date(1735689600000L));
|
||||
Driver impostor = driver(9002L, 7002L, "冒名者", MOBILE);
|
||||
impostor.setUpdateTime(new Date(1767225600000L));
|
||||
|
||||
when(driverService.getOne(any(LambdaQueryWrapper.class))).thenAnswer(invocation -> {
|
||||
LambdaQueryWrapper<Driver> wrapper = invocation.getArgument(0);
|
||||
String sqlSegment = wrapper.getSqlSegment();
|
||||
if (sqlSegment.contains("mobile")) {
|
||||
return impostor;
|
||||
}
|
||||
return sqlSegment.contains("user_id") ? owner : null;
|
||||
});
|
||||
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
// 司机账号本身就是手机号:手机号优先的实现正是靠它命中那条同号档案
|
||||
authUtil.when(AuthUtil::getUserAccount).thenReturn(MOBILE);
|
||||
assertEquals("黄大富", service.current().getDriverName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("小程序用户 phone 是脱敏串时改用其 account——脱敏 phone 既查不到档案,也会把本人真实号码误判成越权")
|
||||
void maskedUserPhoneFallsBackToAccount() {
|
||||
MpUser user = new MpUser();
|
||||
user.setPhone("175****12082");
|
||||
user.setAccount("175874212082");
|
||||
@DisplayName("没有档案时返回空数据而不是异常——没档案的登录用户看到的是空页面、也不能操作")
|
||||
void missingProfileReturnsEmptyInsteadOfFailing() {
|
||||
when(driverService.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
|
||||
|
||||
assertEquals("黄大富", currentByPhone(null, null, user).getDriverName());
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
assertNull(service.current(), "查不到档案时档案为空");
|
||||
assertNull(service.currentDriverEntity(), "查不到档案时实体为空");
|
||||
assertTrue(service.myVehicles().isEmpty(), "查不到档案时车辆列表为空");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("传他人手机号一律拒绝——不能因为解析不到本人号码就放行,那等于允许按手机号查任意档案")
|
||||
void otherPeoplesMobileIsRejected() {
|
||||
assertEquals("只能查询本人司机档案", assertThrows(ServiceException.class,
|
||||
() -> currentByPhone("13900000001", "175874212082", null)).getMessage());
|
||||
@DisplayName("未登录直接抛异常,不能当成「没有档案」静默返回空")
|
||||
void anonymousIsRejected() {
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(null);
|
||||
assertEquals("未登录", assertThrows(ServiceException.class,
|
||||
() -> service.current()).getMessage());
|
||||
assertEquals("未登录", assertThrows(ServiceException.class, service::myVehicles).getMessage());
|
||||
assertEquals("未登录", assertThrows(ServiceException.class, service::currentDriverEntity).getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 辅助
|
||||
|
||||
private Driver driver(Long id, Long userId, String driverName, String mobile) {
|
||||
Driver driver = new Driver();
|
||||
driver.setId(id);
|
||||
driver.setUserId(userId);
|
||||
driver.setDriverName(driverName);
|
||||
driver.setMobile(mobile);
|
||||
return driver;
|
||||
}
|
||||
|
||||
/** 造一辆只关心有效期三个字段的车 */
|
||||
private TransportVehicle vehicle(LocalDate licenseStart, LocalDate licenseEnd, Integer licenseLongTerm) {
|
||||
TransportVehicle vehicle = new TransportVehicle();
|
||||
@@ -129,13 +176,9 @@ class DriverAppServiceImplTest {
|
||||
return vehicle;
|
||||
}
|
||||
|
||||
/** 按当前登录司机的手机号跑一次「我的车辆」,取第一张卡片 */
|
||||
/** 按当前登录司机跑一次「我的车辆」,取第一张卡片 */
|
||||
private DriverVehicleCardVO firstCard(TransportVehicle vehicle) {
|
||||
Driver driver = new Driver();
|
||||
driver.setId(9001L);
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDriverName("韦司机");
|
||||
driver.setMobile(MOBILE);
|
||||
Driver driver = driver(9001L, USER_ID, "韦司机", MOBILE);
|
||||
driver.setDrivingVehicle(vehicle.getPlateNo());
|
||||
|
||||
when(driverService.getOne(any(LambdaQueryWrapper.class))).thenReturn(driver);
|
||||
@@ -143,39 +186,9 @@ class DriverAppServiceImplTest {
|
||||
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
// 账号本身就是手机号,resolvePhone 走这条短路分支,不再回调用户中心
|
||||
authUtil.when(AuthUtil::getUserAccount).thenReturn(MOBILE);
|
||||
List<DriverVehicleCardVO> cards = service.myVehicles();
|
||||
assertEquals(1, cards.size(), "应当只返回绑定到本人的这一辆车");
|
||||
return cards.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按给定的登录态跑一次 currentByPhone。
|
||||
*
|
||||
* @param mobileHint 前端传入的 mobile,可为 null
|
||||
* @param account JWT 里的账号,可为 null
|
||||
* @param user 小程序用户,可为 null(表示走不到小程序用户中心)
|
||||
*/
|
||||
private Driver currentByPhone(String mobileHint, String account, MpUser user) {
|
||||
IMpUserClient mpUserClient = mock(IMpUserClient.class);
|
||||
if (user != null) {
|
||||
when(mpUserClient.userById(USER_ID)).thenReturn(R.data(user));
|
||||
}
|
||||
DriverAppServiceImpl scoped = new DriverAppServiceImpl(driverService, transportVehicleService, mpUserClient);
|
||||
|
||||
Driver driver = new Driver();
|
||||
driver.setId(9002L);
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDriverName("黄大富");
|
||||
driver.setMobile("175874212082");
|
||||
when(driverService.getOne(any(LambdaQueryWrapper.class))).thenReturn(driver);
|
||||
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
authUtil.when(AuthUtil::getUserAccount).thenReturn(account);
|
||||
return scoped.currentByPhone(mobileHint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
@@ -358,6 +358,56 @@ class DriverCertServiceImplTest {
|
||||
assertNull(service.getEffectivePersonCert());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("读生效值:有修订版时返回修订版内容(司机端不能拿旧值覆盖审核员的改动)")
|
||||
void getEffectivePersonCertPrefersRevision() {
|
||||
Long effectiveId = 777L;
|
||||
Driver driver = driver(PLATE);
|
||||
driver.setEffectiveCertRecordId(effectiveId);
|
||||
when(driverAppService.currentDriverEntity()).thenReturn(driver);
|
||||
when(certRecordService.getById(effectiveId)).thenReturn(pointedRecord(effectiveId,
|
||||
"司机原提交", "审核员修订"));
|
||||
|
||||
PersonCertSubmitRequest result = service.getEffectivePersonCert();
|
||||
|
||||
assertEquals("审核员修订", result.getRealName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("读生效值:车辆同理,修订版优先")
|
||||
void getEffectiveVehicleCertPrefersRevision() {
|
||||
when(driverAppService.currentDriverEntity()).thenReturn(driver(PLATE));
|
||||
when(transportVehicleService.getById(VEHICLE_ID)).thenReturn(vehicle(VEHICLE_ID, PLATE, 777L));
|
||||
when(certRecordService.getById(777L)).thenReturn(pointedVehicleRecord(777L,
|
||||
"司机原提交", "审核员修订"));
|
||||
|
||||
VehicleCertSubmitRequest result = service.getEffectiveVehicleCert(VEHICLE_ID);
|
||||
|
||||
assertEquals("审核员修订", result.getOwnerName());
|
||||
}
|
||||
|
||||
private TransportCertRecord pointedRecord(Long id, String submittedName, String revisedName) {
|
||||
PersonCertSubmitRequest submitted = new PersonCertSubmitRequest();
|
||||
submitted.setRealName(submittedName);
|
||||
PersonCertSubmitRequest revised = new PersonCertSubmitRequest();
|
||||
revised.setRealName(revisedName);
|
||||
TransportCertRecord record = record(id, TransportCertRecord.STATUS_APPROVED);
|
||||
record.setSnapshotJson(JSON.toJSONString(submitted));
|
||||
record.setAuditedSnapshotJson(JSON.toJSONString(revised));
|
||||
return record;
|
||||
}
|
||||
|
||||
private TransportCertRecord pointedVehicleRecord(Long id, String submittedName, String revisedName) {
|
||||
VehicleCertSubmitRequest submitted = new VehicleCertSubmitRequest();
|
||||
submitted.setOwnerName(submittedName);
|
||||
VehicleCertSubmitRequest revised = new VehicleCertSubmitRequest();
|
||||
revised.setOwnerName(revisedName);
|
||||
TransportCertRecord record = record(id, TransportCertRecord.STATUS_APPROVED);
|
||||
record.setSnapshotJson(JSON.toJSONString(submitted));
|
||||
record.setAuditedSnapshotJson(JSON.toJSONString(revised));
|
||||
return record;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("重新提交后,生效值仍是旧的(核心验收点:提交不影响已认证状态)")
|
||||
void resubmitKeepsEffectiveValue() {
|
||||
|
||||
-2
@@ -12,7 +12,6 @@ import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -95,7 +94,6 @@ class DriverNodePunchPhotoTest {
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
driverService,
|
||||
mock(IMpUserClient.class),
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
mock(CertificationGate.class),
|
||||
|
||||
-2
@@ -7,7 +7,6 @@ import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -92,7 +91,6 @@ class DriverPunchFinishFlagTest {
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
mock(IDriverService.class),
|
||||
mock(IMpUserClient.class),
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
mock(CertificationGate.class),
|
||||
|
||||
-2
@@ -10,7 +10,6 @@ import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -87,7 +86,6 @@ class DriverPunchNodeCardTest {
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
mock(IDriverService.class),
|
||||
mock(IMpUserClient.class),
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
mock(CertificationGate.class),
|
||||
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.system.feign.ISysClient;
|
||||
import org.springblade.transport.mapper.DriverMapper;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 手机号 -> 司机档案 ID 解析入口的测试。
|
||||
* <p>
|
||||
* 归属主键是档案 ID,手机号只是派单时的号码快照:只有租户内唯一命中才认,0 条与多条都不认。
|
||||
* 解析结果是"能不能写归属"的判断,不是业务校验,因此任何入参都只返回 null,不抛异常。
|
||||
*/
|
||||
class DriverResolveMobileTest {
|
||||
|
||||
private static final String TENANT_ID = "000000";
|
||||
private static final String MOBILE = "13800138000";
|
||||
|
||||
private DriverMapper driverMapper;
|
||||
private DriverServiceImpl service;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, Driver.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
driverMapper = mock(DriverMapper.class);
|
||||
service = new DriverServiceImpl(mock(IMpUserClient.class), mock(ISysClient.class));
|
||||
inject("baseMapper", driverMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("租户内唯一命中:返回档案 ID")
|
||||
void returnsProfileIdWhenUnique() {
|
||||
when(driverMapper.selectList(any())).thenReturn(List.of(profile(9001L)));
|
||||
|
||||
assertEquals(9001L, service.resolveDriverIdByMobile(TENANT_ID, MOBILE));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("租户内没有档案:返回 null")
|
||||
void returnsNullWhenAbsent() {
|
||||
when(driverMapper.selectList(any())).thenReturn(Collections.emptyList());
|
||||
|
||||
assertNull(service.resolveDriverIdByMobile(TENANT_ID, MOBILE));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("租户内多条档案(历史同号):返回 null,不猜")
|
||||
void returnsNullWhenMultiple() {
|
||||
List<Driver> profiles = new ArrayList<>();
|
||||
profiles.add(profile(9001L));
|
||||
profiles.add(profile(9002L));
|
||||
when(driverMapper.selectList(any())).thenReturn(profiles);
|
||||
|
||||
assertNull(service.resolveDriverIdByMobile(TENANT_ID, MOBILE));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("手机号为空或脱敏串:返回 null,且不查库")
|
||||
void returnsNullWithoutQueryForBlankOrMaskedMobile() {
|
||||
assertNull(service.resolveDriverIdByMobile(TENANT_ID, null));
|
||||
assertNull(service.resolveDriverIdByMobile(TENANT_ID, " "));
|
||||
assertNull(service.resolveDriverIdByMobile(TENANT_ID, "138****8000"));
|
||||
verify(driverMapper, never()).selectList(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("租户为空:取当前登录租户,仍不抛异常")
|
||||
void fallsBackToCurrentTenant() {
|
||||
when(driverMapper.selectList(any())).thenReturn(List.of(profile(9001L)));
|
||||
|
||||
Long driverId = null;
|
||||
try (org.mockito.MockedStatic<org.springblade.core.secure.utils.AuthUtil> authUtil
|
||||
= org.mockito.Mockito.mockStatic(org.springblade.core.secure.utils.AuthUtil.class)) {
|
||||
authUtil.when(org.springblade.core.secure.utils.AuthUtil::getTenantId).thenReturn(TENANT_ID);
|
||||
driverId = service.resolveDriverIdByMobile(null, MOBILE);
|
||||
}
|
||||
assertEquals(9001L, driverId);
|
||||
}
|
||||
|
||||
private Driver profile(Long id) {
|
||||
Driver profile = new Driver();
|
||||
profile.setId(id);
|
||||
profile.setIsDeleted(0);
|
||||
profile.setTenantId(TENANT_ID);
|
||||
profile.setMobile(MOBILE);
|
||||
return profile;
|
||||
}
|
||||
|
||||
private void inject(String field, Object value) throws Exception {
|
||||
Class<?> type = service.getClass();
|
||||
while (type != null) {
|
||||
try {
|
||||
Field declared = type.getDeclaredField(field);
|
||||
declared.setAccessible(true);
|
||||
declared.set(service, value);
|
||||
return;
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
type = type.getSuperclass();
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("未找到字段:" + field);
|
||||
}
|
||||
|
||||
}
|
||||
+11
@@ -105,6 +105,17 @@ class DriverSyncMobileTest {
|
||||
verify(driverMapper, never()).updateById(any(Driver.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("改号链路不持有运单/配载服务:换号只改档案,不回写单据上的手机号快照")
|
||||
void phoneSyncDoesNotDependOnWaybillServices() {
|
||||
for (Field field : DriverServiceImpl.class.getDeclaredFields()) {
|
||||
String typeName = field.getType().getSimpleName();
|
||||
assertFalse(typeName.contains("Waybill") || typeName.contains("LoadingManage"),
|
||||
"改号链路引入了 " + typeName + ":换号不得顺手回写运单/配载单的司机快照,"
|
||||
+ "否则司机档案与历史单据的归属会一起漂移");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("档案 mobile 与账号旧号不一致:不动档案,返回可读原因(不抛异常)")
|
||||
void skipsWhenProfileMobileDiffersFromOldPhone() {
|
||||
|
||||
+18
-6
@@ -14,7 +14,6 @@ import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.BladeUser;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -57,7 +56,10 @@ class DriverTripAcceptRejectTest {
|
||||
|
||||
private static final Long USER_ID = 7401L;
|
||||
private static final Long DRIVER_ID = 9001L;
|
||||
private static final String PHONE = "17587421208";
|
||||
/** 派单时落在运次上的号码快照。 */
|
||||
private static final String SNAPSHOT_PHONE = "17587421208";
|
||||
/** 司机改号后档案与登录账号上的号码。 */
|
||||
private static final String CURRENT_PHONE = "17687421208";
|
||||
private static final String TRIP_ID = "2101146192739459074";
|
||||
private static final Long PENDING_ID = 2101144166395699202L;
|
||||
private static final Long SECOND_ID = 2101145869819994113L;
|
||||
@@ -85,7 +87,6 @@ class DriverTripAcceptRejectTest {
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
driverService,
|
||||
mock(IMpUserClient.class),
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
certificationGate,
|
||||
@@ -121,7 +122,8 @@ class DriverTripAcceptRejectTest {
|
||||
"两张子运单与接单司机一起下发:" + capturedUpdateParams());
|
||||
assertTrue(capturedWhereSql().contains("(id IN ("), capturedWhereSql());
|
||||
assertEquals(TRIP_ID, capturedQuery().getTripId());
|
||||
assertEquals(PHONE, capturedQuery().getDriverPhone(), "归属仍按司机手机号收口");
|
||||
assertEquals(DRIVER_ID, capturedQuery().getDriverId(),
|
||||
"运次查询按司机档案 ID 收口:手机号换了也认得出本人");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -340,6 +342,16 @@ class DriverTripAcceptRejectTest {
|
||||
verify(waybillService, never()).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("没有司机档案的账号不能运次接单:不按手机号回退、不提供代报")
|
||||
void tripAcceptWithoutDriverProfileIsRefused() {
|
||||
when(driverService.getOne(any())).thenReturn(null);
|
||||
|
||||
assertEquals("未找到当前登录司机档案", assertThrows(ServiceException.class,
|
||||
() -> runAsDriver(() -> service.tripAccept(TRIP_ID))).getMessage());
|
||||
verify(waybillService, never()).update(any());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private <T> T runAsDriver(Supplier<T> action) {
|
||||
@@ -373,7 +385,7 @@ class DriverTripAcceptRejectTest {
|
||||
private void stubTrip(TripVO.TripWaybillVO[] leading, TripVO.TripWaybillVO... children) {
|
||||
TripVO trip = new TripVO();
|
||||
trip.setId(TRIP_ID);
|
||||
trip.setDriverPhone(PHONE);
|
||||
trip.setDriverPhone(SNAPSHOT_PHONE);
|
||||
List<TripVO.TripWaybillVO> all = new java.util.ArrayList<>(List.of(leading));
|
||||
all.addAll(List.of(children));
|
||||
trip.setWaybills(all);
|
||||
@@ -400,7 +412,7 @@ class DriverTripAcceptRejectTest {
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDriverName("黄宇富");
|
||||
driver.setDrivingVehicle("桂A32523");
|
||||
driver.setMobile(PHONE);
|
||||
driver.setMobile(CURRENT_PHONE);
|
||||
return driver;
|
||||
}
|
||||
|
||||
|
||||
+3
-8
@@ -10,7 +10,6 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -74,7 +73,6 @@ class DriverTripCardTest {
|
||||
|
||||
private IWaybillService waybillService;
|
||||
private IDriverService driverService;
|
||||
private IMpUserClient mpUserClient;
|
||||
private ITripQueryService tripQueryService;
|
||||
private WaybillEnroutePunchMapper enroutePunchMapper;
|
||||
private WaybillNodePunchMapper nodePunchMapper;
|
||||
@@ -94,14 +92,12 @@ class DriverTripCardTest {
|
||||
void setUp() {
|
||||
waybillService = mock(IWaybillService.class);
|
||||
driverService = mock(IDriverService.class);
|
||||
mpUserClient = mock(IMpUserClient.class);
|
||||
tripQueryService = mock(ITripQueryService.class);
|
||||
enroutePunchMapper = mock(WaybillEnroutePunchMapper.class);
|
||||
nodePunchMapper = mock(WaybillNodePunchMapper.class);
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
driverService,
|
||||
mpUserClient,
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
mock(CertificationGate.class),
|
||||
@@ -125,7 +121,7 @@ class DriverTripCardTest {
|
||||
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.tripPage(1, 10, null, null));
|
||||
|
||||
given = capturedQuery();
|
||||
assertEquals(PHONE, given.getDriverPhone());
|
||||
assertEquals(DRIVER_ID, given.getDriverId(), "运次列表按司机档案 ID 收口");
|
||||
assertEquals(1, given.getCurrent());
|
||||
assertEquals(10, given.getSize(), "一页 10 条即 10 个运次");
|
||||
assertNull(given.getStatus());
|
||||
@@ -334,10 +330,9 @@ class DriverTripCardTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("收口缺失时不放行:空页、零统计、当前任务为空,且不查运次")
|
||||
void missingPhoneIsRefused() {
|
||||
@DisplayName("收口缺失时不放行:没有司机档案就空页、零统计、当前任务为空,且不查运次")
|
||||
void missingDriverProfileIsRefused() {
|
||||
when(driverService.getOne(any())).thenReturn(null);
|
||||
when(mpUserClient.userById(any())).thenReturn(null);
|
||||
|
||||
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.tripPage(null, null, null, null));
|
||||
DriverWaybillPreviewVO preview = runAsDriver(() -> service.tripPendingPreview(null));
|
||||
|
||||
-2
@@ -11,7 +11,6 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -116,7 +115,6 @@ class DriverWaybillAutoCompleteTest {
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
driverService,
|
||||
mock(IMpUserClient.class),
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
mock(CertificationGate.class),
|
||||
|
||||
-2
@@ -7,7 +7,6 @@ import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -73,7 +72,6 @@ class DriverWaybillCardLocationTest {
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
mock(IDriverService.class),
|
||||
mock(IMpUserClient.class),
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
mock(CertificationGate.class),
|
||||
|
||||
-2
@@ -7,7 +7,6 @@ import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -55,7 +54,6 @@ class DriverWaybillNodePunchTest {
|
||||
service = new DriverWaybillServiceImpl(
|
||||
mock(IWaybillService.class),
|
||||
mock(IDriverService.class),
|
||||
mock(IMpUserClient.class),
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
mock(CertificationGate.class),
|
||||
|
||||
+74
-35
@@ -12,7 +12,6 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -27,26 +26,34 @@ import org.springblade.transport.support.CertificationGate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
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.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 司机对运单的归属校验(异常上报等按运单操作的前置条件)。
|
||||
* <p>
|
||||
* 口径:当前用户手机号匹配运单 {@code driverPhone}。
|
||||
* 口径:登录用户按 {@code user_id} 解析到司机档案,运单归属键是运单上的 {@code driver_id};
|
||||
* {@code driver_phone} 只是派单快照,不参与判定。
|
||||
* 管理端与调度员账号没有司机档案,会停在这里,这是刻意的:不提供代报。
|
||||
*/
|
||||
class DriverWaybillOwnershipTest {
|
||||
|
||||
private static final Long WAYBILL_ID = 5301L;
|
||||
private static final Long USER_ID = 7001L;
|
||||
private static final String PHONE = "13800138000";
|
||||
private static final Long DRIVER_ID = 9001L;
|
||||
private static final Long OTHER_DRIVER_ID = 9002L;
|
||||
/** 派单时落在运单上的号码快照。 */
|
||||
private static final String SNAPSHOT_PHONE = "13800138000";
|
||||
/** 司机改号后档案与登录账号上的号码。 */
|
||||
private static final String CURRENT_PHONE = "13900139000";
|
||||
|
||||
private IDriverService driverService;
|
||||
private IWaybillService waybillService;
|
||||
@@ -58,6 +65,7 @@ class DriverWaybillOwnershipTest {
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||
TableInfoHelper.initTableInfo(assistant, Driver.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@@ -67,7 +75,6 @@ class DriverWaybillOwnershipTest {
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
driverService,
|
||||
mock(IMpUserClient.class),
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
mock(CertificationGate.class),
|
||||
@@ -91,33 +98,26 @@ class DriverWaybillOwnershipTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("司机档案与登录态都没有手机号时被拒")
|
||||
void withoutPhoneIsRejected() {
|
||||
Driver driver = driver(null);
|
||||
when(driverService.getOne(any())).thenReturn(driver);
|
||||
@DisplayName("司机档案只按登录用户 ID 解析,不按手机号回退")
|
||||
void driverProfileResolvedByUserIdOnly() {
|
||||
when(driverService.getOne(any())).thenReturn(driver(CURRENT_PHONE));
|
||||
when(waybillService.getOne(any())).thenReturn(waybill());
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runOwnershipCheck(WAYBILL_ID));
|
||||
assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID));
|
||||
|
||||
assertEquals("当前用户手机号为空,无法查找运单", exception.getMessage());
|
||||
ArgumentCaptor<LambdaQueryWrapper<Driver>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(driverService, times(1)).getOne(captor.capture());
|
||||
LambdaQueryWrapper<Driver> wrapper = captor.getValue();
|
||||
assertTrue(wrapper.getTargetSql().contains("user_id"),
|
||||
"司机档案必须按登录用户 ID 解析:" + wrapper.getTargetSql());
|
||||
assertFalse(wrapper.getTargetSql().contains("mobile"),
|
||||
"司机档案不允许再按手机号回退:" + wrapper.getTargetSql());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运单司机手机号与当前用户不一致时被拒")
|
||||
void waybillOfAnotherPhoneIsRejected() {
|
||||
when(driverService.getOne(any())).thenReturn(driver(PHONE));
|
||||
when(waybillService.getOne(any())).thenReturn(null);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runOwnershipCheck(WAYBILL_ID));
|
||||
|
||||
assertEquals("运单不存在或无权操作", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("本人手机号运单放行,且查询确实按司机手机号收敛")
|
||||
void ownWaybillPasses() {
|
||||
when(driverService.getOne(any())).thenReturn(driver(PHONE));
|
||||
@DisplayName("司机改号后本人运单仍放行,归属只认 driver_id")
|
||||
void ownWaybillPassesAfterNumberChange() {
|
||||
when(driverService.getOne(any())).thenReturn(driver(CURRENT_PHONE));
|
||||
when(waybillService.getOne(any())).thenReturn(waybill());
|
||||
|
||||
assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID));
|
||||
@@ -125,17 +125,55 @@ class DriverWaybillOwnershipTest {
|
||||
ArgumentCaptor<LambdaQueryWrapper<Waybill>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(waybillService).getOne(captor.capture());
|
||||
LambdaQueryWrapper<Waybill> wrapper = captor.getValue();
|
||||
assertTrue(wrapper.getTargetSql().contains("driver_phone"),
|
||||
"归属查询必须落在运单司机手机号上:" + wrapper.getTargetSql());
|
||||
assertTrue(wrapper.getParamNameValuePairs().values().stream()
|
||||
.anyMatch(PHONE::equals),
|
||||
"归属查询必须带上当前用户手机号:" + wrapper.getParamNameValuePairs());
|
||||
// 护栏:归属查询必须落在运单 driver_id 上,不能再出现手机号比较,否则改号即失效。
|
||||
assertTrue(wrapper.getTargetSql().contains("driver_id"),
|
||||
"归属查询必须落在运单司机档案 ID 上:" + wrapper.getTargetSql());
|
||||
assertFalse(wrapper.getTargetSql().contains("driver_phone"),
|
||||
"归属查询不能再比较运单司机手机号快照:" + wrapper.getTargetSql());
|
||||
assertTrue(wrapper.getParamNameValuePairs().values().contains(DRIVER_ID),
|
||||
"归属查询必须带上当前司机档案 ID:" + wrapper.getParamNameValuePairs());
|
||||
assertFalse(wrapper.getParamNameValuePairs().values().contains(SNAPSHOT_PHONE),
|
||||
"快照号不参与归属判定:" + wrapper.getParamNameValuePairs());
|
||||
assertFalse(wrapper.getParamNameValuePairs().values().contains(CURRENT_PHONE),
|
||||
"档案手机号不参与归属判定:" + wrapper.getParamNameValuePairs());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("同号换人:同手机号但不同档案看不到对方的运单")
|
||||
void waybillOwnedByAnotherDriverWithSamePhoneIsRejected() {
|
||||
when(driverService.getOne(any())).thenReturn(driver(CURRENT_PHONE));
|
||||
// 库里存在该运单,但归属档案是另一个同号档案,按 driver_id 收口后查不到。
|
||||
when(waybillService.getOne(any())).thenReturn(null);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runOwnershipCheck(WAYBILL_ID));
|
||||
|
||||
assertEquals("运单不存在或无权操作", exception.getMessage());
|
||||
|
||||
ArgumentCaptor<LambdaQueryWrapper<Waybill>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(waybillService).getOne(captor.capture());
|
||||
LambdaQueryWrapper<Waybill> wrapper = captor.getValue();
|
||||
// 参数槽由 SQL 片段构建时填充:不先取一次片段,参数表是空的
|
||||
wrapper.getTargetSql();
|
||||
assertTrue(wrapper.getParamNameValuePairs().values().contains(DRIVER_ID),
|
||||
"收口条件必须是当前档案 ID:" + wrapper.getParamNameValuePairs());
|
||||
assertFalse(wrapper.getParamNameValuePairs().values().contains(OTHER_DRIVER_ID),
|
||||
"不能把别人的档案 ID 当查询条件:" + wrapper.getParamNameValuePairs());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("司机档案没有手机号也能按档案 ID 认领自己的运单")
|
||||
void profileWithoutPhoneStillOwnsByDriverId() {
|
||||
when(driverService.getOne(any())).thenReturn(driver(null));
|
||||
when(waybillService.getOne(any())).thenReturn(waybill());
|
||||
|
||||
assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运单ID为空时被拒")
|
||||
void withoutWaybillIdIsRejected() {
|
||||
when(driverService.getOne(any())).thenReturn(driver(PHONE));
|
||||
when(driverService.getOne(any())).thenReturn(driver(CURRENT_PHONE));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> runOwnershipCheck(null));
|
||||
@@ -148,14 +186,14 @@ class DriverWaybillOwnershipTest {
|
||||
private void runOwnershipCheck(Long waybillId) {
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
authUtil.when(AuthUtil::getUserAccount).thenReturn(PHONE);
|
||||
authUtil.when(AuthUtil::getUserAccount).thenReturn(CURRENT_PHONE);
|
||||
service.assertCurrentDriverOwnsWaybill(waybillId);
|
||||
}
|
||||
}
|
||||
|
||||
private Driver driver(String mobile) {
|
||||
Driver driver = new Driver();
|
||||
driver.setId(9001L);
|
||||
driver.setId(DRIVER_ID);
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDriverName("韦司机");
|
||||
driver.setMobile(mobile);
|
||||
@@ -166,7 +204,8 @@ class DriverWaybillOwnershipTest {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setWaybillNo("YD202610070001");
|
||||
waybill.setDriverPhone(PHONE);
|
||||
waybill.setDriverId(DRIVER_ID);
|
||||
waybill.setDriverPhone(SNAPSHOT_PHONE);
|
||||
waybill.setIsDeleted(0);
|
||||
return waybill;
|
||||
}
|
||||
|
||||
+31
-5
@@ -15,7 +15,6 @@ import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -34,6 +33,7 @@ import org.springblade.transport.support.CertificationGate;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -58,7 +58,10 @@ class DriverWaybillRejectDetachTest {
|
||||
private static final Long USER_ID = 7401L;
|
||||
private static final Long DRIVER_ID = 9001L;
|
||||
private static final String PLATE = "沪AE31312";
|
||||
private static final String PHONE = "13800138002";
|
||||
/** 派单时落在运单上的号码快照。 */
|
||||
private static final String SNAPSHOT_PHONE = "13800138002";
|
||||
/** 司机改号后档案与登录账号上的号码。 */
|
||||
private static final String CURRENT_PHONE = "13900139002";
|
||||
private static final Long REJECTED_ID = 7201L;
|
||||
|
||||
private IDriverService driverService;
|
||||
@@ -83,7 +86,6 @@ class DriverWaybillRejectDetachTest {
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
driverService,
|
||||
mock(IMpUserClient.class),
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
certificationGate,
|
||||
@@ -155,6 +157,29 @@ class DriverWaybillRejectDetachTest {
|
||||
assertEquals(2, page.getRecords().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("司机改号后运单仍可见,卡片仍展示派单时的号码快照")
|
||||
void numberChangeKeepsWaybillsVisible() {
|
||||
// 同一个档案:档案与登录账号已换成新号,运单快照还是派单时的旧号
|
||||
when(waybillService.list(ArgumentMatchers.<com.baomidou.mybatisplus.core.conditions.Wrapper<Waybill>>any()))
|
||||
.thenReturn(List.of(waybill(7202L, "YD-20261001-0002", "pending", "pending")));
|
||||
|
||||
IPage<DriverWaybillCardVO> page = runAsDriver(() -> service.page(1, 10, null, null));
|
||||
|
||||
assertEquals(1, page.getTotal(), "改号不影响本人运单的可见性");
|
||||
assertEquals(SNAPSHOT_PHONE, page.getRecords().get(0).getDriverPhone(),
|
||||
"卡片展示的仍是运单上的号码快照,不改成本人新号");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("重新派回后运单恢复可见:归属看新的 driver_id,不看历史拒单")
|
||||
void reassignedWaybillBecomesVisibleAgain() {
|
||||
when(waybillService.getOne(any()))
|
||||
.thenReturn(waybill(REJECTED_ID, "YD-20261010-0001", "pending", "pending"));
|
||||
|
||||
assertDoesNotThrow(() -> runAsDriver(() -> service.detail(REJECTED_ID)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已拒单运单的详情不可达:取单条件已把它挡住")
|
||||
void detailOfRejectedIsUnreachable() {
|
||||
@@ -218,7 +243,7 @@ class DriverWaybillRejectDetachTest {
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDriverName("黄大富");
|
||||
driver.setDrivingVehicle(PLATE);
|
||||
driver.setMobile(PHONE);
|
||||
driver.setMobile(CURRENT_PHONE);
|
||||
return driver;
|
||||
}
|
||||
|
||||
@@ -231,7 +256,8 @@ class DriverWaybillRejectDetachTest {
|
||||
waybill.setId(id);
|
||||
waybill.setWaybillNo(no);
|
||||
waybill.setVehicleNo(PLATE);
|
||||
waybill.setDriverPhone(PHONE);
|
||||
waybill.setDriverId(DRIVER_ID);
|
||||
waybill.setDriverPhone(SNAPSHOT_PHONE);
|
||||
waybill.setBusinessStatus(businessStatus);
|
||||
waybill.setDriverAcceptStatus(acceptStatus);
|
||||
waybill.setIsDeleted(0);
|
||||
|
||||
+40
-6
@@ -12,7 +12,6 @@ import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.system.feign.IMpUserClient;
|
||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -35,6 +34,7 @@ import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -51,8 +51,12 @@ class DriverWaybillTabStatusTest {
|
||||
|
||||
private static final Long WAYBILL_ID = 5901L;
|
||||
private static final Long USER_ID = 7001L;
|
||||
private static final Long DRIVER_ID = 9001L;
|
||||
private static final String PLATE = "桂AAW3256";
|
||||
private static final String PHONE = "13800138002";
|
||||
/** 派单时落在运单上的号码快照。 */
|
||||
private static final String SNAPSHOT_PHONE = "13800138002";
|
||||
/** 司机改号后档案与登录账号上的号码。 */
|
||||
private static final String CURRENT_PHONE = "13900139002";
|
||||
|
||||
private IWaybillService waybillService;
|
||||
private IDriverService driverService;
|
||||
@@ -81,7 +85,6 @@ class DriverWaybillTabStatusTest {
|
||||
service = new DriverWaybillServiceImpl(
|
||||
waybillService,
|
||||
driverService,
|
||||
mock(IMpUserClient.class),
|
||||
mock(IProcessConfigService.class),
|
||||
mock(ITransportVehicleService.class),
|
||||
mock(CertificationGate.class),
|
||||
@@ -141,6 +144,27 @@ class DriverWaybillTabStatusTest {
|
||||
assertEquals(1, counts.getAll());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("司机改号后三档计数不变,计数查询按 driver_id 收口")
|
||||
void tabCountsAfterNumberChangeScopedByDriverId() {
|
||||
DriverWaybillTabCountsVO counts = runAsDriver(service::tabCounts);
|
||||
|
||||
assertEquals(1, counts.getDoing(), "改号不影响本人运单的分档");
|
||||
assertEquals(1, counts.getAll());
|
||||
|
||||
LambdaQueryWrapper<Waybill> wrapper = firstQueriedWrapper();
|
||||
// 护栏:计数查询必须落在运单 driver_id 上;比对手机号的话,改号后司机的数字会清零
|
||||
String sql = wrapper.getTargetSql();
|
||||
assertTrue(sql.contains("driver_id"), "计数查询必须按司机档案 ID 收口:" + sql);
|
||||
assertFalse(sql.contains("driver_phone"), "计数查询不能再比较运单手机号快照:" + sql);
|
||||
assertTrue(wrapper.getParamNameValuePairs().values().contains(DRIVER_ID),
|
||||
"计数查询必须带上当前司机档案 ID:" + wrapper.getParamNameValuePairs());
|
||||
assertFalse(wrapper.getParamNameValuePairs().values().contains(SNAPSHOT_PHONE),
|
||||
"快照号不参与计数:" + wrapper.getParamNameValuePairs());
|
||||
assertFalse(wrapper.getParamNameValuePairs().values().contains(CURRENT_PHONE),
|
||||
"档案手机号不参与计数:" + wrapper.getParamNameValuePairs());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private <T> T runAsDriver(Supplier<T> action) {
|
||||
@@ -167,6 +191,15 @@ class DriverWaybillTabStatusTest {
|
||||
return statuses;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private LambdaQueryWrapper<Waybill> firstQueriedWrapper() {
|
||||
assertEquals(1, queried.size(), "一次列表查询");
|
||||
LambdaQueryWrapper<Waybill> wrapper = (LambdaQueryWrapper<Waybill>) queried.get(0);
|
||||
// 参数槽由 SQL 片段构建时填充:不先取一次片段,参数表是空的
|
||||
wrapper.getSqlSegment();
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Wrapper<Waybill> anyWrapper() {
|
||||
return org.mockito.ArgumentMatchers.<Wrapper<Waybill>>any();
|
||||
@@ -177,7 +210,8 @@ class DriverWaybillTabStatusTest {
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setWaybillNo("YD202610080002");
|
||||
waybill.setVehicleNo(PLATE);
|
||||
waybill.setDriverPhone(PHONE);
|
||||
waybill.setDriverId(DRIVER_ID);
|
||||
waybill.setDriverPhone(SNAPSHOT_PHONE);
|
||||
waybill.setBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM);
|
||||
waybill.setIsDeleted(0);
|
||||
return waybill;
|
||||
@@ -185,10 +219,10 @@ class DriverWaybillTabStatusTest {
|
||||
|
||||
private Driver driver() {
|
||||
Driver driver = new Driver();
|
||||
driver.setId(9001L);
|
||||
driver.setId(DRIVER_ID);
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDrivingVehicle(PLATE);
|
||||
driver.setMobile(PHONE);
|
||||
driver.setMobile(CURRENT_PHONE);
|
||||
return driver;
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -19,6 +19,7 @@ import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IMaintenanceRecordService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@@ -86,7 +87,8 @@ class ExceptionDisposalDetailPayloadTest {
|
||||
loadingManageMapper = mock(LoadingManageMapper.class);
|
||||
service = new ExceptionDisposalServiceImpl(
|
||||
followRecordMapper, waybillService, mock(IDriverWaybillService.class),
|
||||
mock(org.springblade.transport.support.MpUserDataScopeSupport.class), loadingManageMapper);
|
||||
mock(org.springblade.transport.support.MpUserDataScopeSupport.class), loadingManageMapper,
|
||||
mock(IMaintenanceRecordService.class));
|
||||
inject("baseMapper", disposalMapper);
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -20,6 +20,7 @@ import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IMaintenanceRecordService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@@ -73,7 +74,8 @@ class ExceptionDisposalListQueryTest {
|
||||
waybillService,
|
||||
mock(IDriverWaybillService.class),
|
||||
scopeSupport,
|
||||
loadingManageMapper);
|
||||
loadingManageMapper,
|
||||
mock(IMaintenanceRecordService.class));
|
||||
inject("baseMapper", disposalMapper);
|
||||
when(disposalMapper.selectPage(any(), any())).thenReturn(new Page<ExceptionDisposal>(1, 10));
|
||||
}
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposalFollowRecord;
|
||||
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IMaintenanceRecordService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@@ -69,7 +70,7 @@ class ExceptionDisposalRejectRevokeTest {
|
||||
service = new ExceptionDisposalServiceImpl(
|
||||
followRecordMapper, mock(IWaybillService.class), mock(IDriverWaybillService.class),
|
||||
mock(org.springblade.transport.support.MpUserDataScopeSupport.class),
|
||||
mock(LoadingManageMapper.class));
|
||||
mock(LoadingManageMapper.class), mock(IMaintenanceRecordService.class));
|
||||
inject("baseMapper", disposalMapper);
|
||||
when(disposalMapper.updateById(any(ExceptionDisposal.class))).thenReturn(1);
|
||||
when(followRecordMapper.insert(any(ExceptionDisposalFollowRecord.class))).thenReturn(1);
|
||||
|
||||
+2
-1
@@ -19,6 +19,7 @@ import org.springblade.transport.mapper.LoadingManageMapper;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IMaintenanceRecordService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@@ -74,7 +75,7 @@ class ExceptionDisposalReportGateTest {
|
||||
service = new ExceptionDisposalServiceImpl(
|
||||
mock(ExceptionDisposalFollowRecordMapper.class), waybillService, driverWaybillService,
|
||||
mock(org.springblade.transport.support.MpUserDataScopeSupport.class),
|
||||
mock(LoadingManageMapper.class));
|
||||
mock(LoadingManageMapper.class), mock(IMaintenanceRecordService.class));
|
||||
inject("baseMapper", disposalMapper);
|
||||
|
||||
when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill());
|
||||
|
||||
+3
-1
@@ -23,6 +23,7 @@ import org.springblade.transport.pojo.entity.ExceptionDisposalFollowRecord;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IMaintenanceRecordService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@@ -88,7 +89,8 @@ class ExceptionDisposalRouteAdjustTest {
|
||||
loadingManageMapper = mock(LoadingManageMapper.class);
|
||||
service = new ExceptionDisposalServiceImpl(
|
||||
followRecordMapper, waybillService, mock(IDriverWaybillService.class),
|
||||
mock(org.springblade.transport.support.MpUserDataScopeSupport.class), loadingManageMapper);
|
||||
mock(org.springblade.transport.support.MpUserDataScopeSupport.class), loadingManageMapper,
|
||||
mock(IMaintenanceRecordService.class));
|
||||
inject("baseMapper", disposalMapper);
|
||||
when(disposalMapper.updateById(any(ExceptionDisposal.class))).thenReturn(1);
|
||||
when(followRecordMapper.insert(any(ExceptionDisposalFollowRecord.class))).thenReturn(1);
|
||||
|
||||
+29
@@ -19,6 +19,7 @@ import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
@@ -81,6 +82,7 @@ class LoadingManageAdmissionTest {
|
||||
inject("baseMapper", loadingManageMapper);
|
||||
inject("waybillMapper", waybillMapper);
|
||||
inject("contractManageService", contractManageService);
|
||||
inject("driverService", mock(IDriverService.class));
|
||||
authUtil = mockStatic(AuthUtil.class);
|
||||
authUtil.when(AuthUtil::getDeptId).thenReturn(String.valueOf(DEPT_ID));
|
||||
}
|
||||
@@ -292,6 +294,33 @@ class LoadingManageAdmissionTest {
|
||||
assertEquals("所选承运商合同不存在、未审核通过或已失效", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("子运单的客户合同快照为空时不按甲方过滤,取不到甲方也不空指针")
|
||||
void missingCustomerContractDoesNotFilterByPartyA() {
|
||||
Waybill withSnapshot = waybill(1L, WAYBILL_NO_A, "road", "pending", null, PROJECT_A);
|
||||
withSnapshot.setContractId(OTHER_CONTRACT_ID);
|
||||
stubWaybills(withSnapshot, waybill(2L, WAYBILL_NO_B, "road", "pending", null, PROJECT_A));
|
||||
stubContract(CONTRACT_ID, PROJECT_A);
|
||||
|
||||
assertDoesNotThrow(() -> service.submit(submitPayload(List.of(1L, 2L))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("项目下没有可用承运商合同时不按乙方交集拦截:自运的多张子运单照常提交")
|
||||
void noAvailableCarrierContractSkipsPartyBCheck() {
|
||||
stubWaybills(
|
||||
waybill(1L, WAYBILL_NO_A, "road", "pending", null, PROJECT_A),
|
||||
waybill(2L, WAYBILL_NO_B, "road", "pending", null, PROJECT_A));
|
||||
when(contractManageService.list(anyContractWrapper())).thenReturn(List.of());
|
||||
LoadingManage payload = submitPayload(List.of(1L, 2L));
|
||||
payload.setCarrierType("自运");
|
||||
payload.setCarrierContractId(null);
|
||||
payload.setDriverName("张三");
|
||||
payload.setDriverPhone("13800008000");
|
||||
|
||||
assertDoesNotThrow(() -> service.submit(payload));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 放行
|
||||
|
||||
@Test
|
||||
|
||||
+271
-9
@@ -1,5 +1,7 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
@@ -15,16 +17,20 @@ import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.transport.mapper.LoadingManageMapper;
|
||||
import org.springblade.transport.mapper.WaybillMapper;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.ProcessConfig;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.support.LogCapture;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -32,9 +38,11 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -60,6 +68,12 @@ class LoadingManageChildStatusWriteTest {
|
||||
private static final String LOADING_NO = "PZ-20261018-001";
|
||||
private static final Long LOADING_ID = 1800000000000000001L;
|
||||
private static final String DRIVER_PHONE = "13800138000";
|
||||
private static final String TENANT_ID = "000000";
|
||||
private static final Long RESOLVED_DRIVER_ID = 9321L;
|
||||
private static final Long MANUAL_DRIVER_ID = 7001L;
|
||||
private static final Long OTHER_PROFILE_ID = 8888L;
|
||||
private static final String OTHER_DRIVER_PHONE = "13900139000";
|
||||
private static final Long SOURCE_LOADING_ID = 1800000000000000002L;
|
||||
|
||||
private static final Long NEED_ACCEPT_WAYBILL_ID = 3101L;
|
||||
private static final Long NO_ACCEPT_WAYBILL_ID = 3102L;
|
||||
@@ -80,6 +94,7 @@ class LoadingManageChildStatusWriteTest {
|
||||
"carrier_type",
|
||||
"carrier_contract_id",
|
||||
"carrier_name",
|
||||
"driver_id",
|
||||
"driver_name",
|
||||
"driver_phone",
|
||||
"vehicle_no",
|
||||
@@ -104,6 +119,8 @@ class LoadingManageChildStatusWriteTest {
|
||||
private IProcessConfigService processConfigService;
|
||||
private IContractManageService contractManageService;
|
||||
private IReceivablePayableDetailService receivablePayableDetailService;
|
||||
private IDriverService driverService;
|
||||
private LoadingManageMapper loadingManageMapper;
|
||||
private LoadingManageServiceImpl service;
|
||||
private MockedStatic<AuthUtil> authUtil;
|
||||
|
||||
@@ -115,6 +132,7 @@ class LoadingManageChildStatusWriteTest {
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||
TableInfoHelper.initTableInfo(assistant, ProcessConfig.class);
|
||||
TableInfoHelper.initTableInfo(assistant, LoadingManage.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@@ -123,14 +141,19 @@ class LoadingManageChildStatusWriteTest {
|
||||
processConfigService = mock(IProcessConfigService.class);
|
||||
contractManageService = mock(IContractManageService.class);
|
||||
receivablePayableDetailService = mock(IReceivablePayableDetailService.class);
|
||||
driverService = mock(IDriverService.class);
|
||||
loadingManageMapper = mock(LoadingManageMapper.class);
|
||||
service = spy(new LoadingManageServiceImpl());
|
||||
inject(service, "waybillMapper", waybillMapper);
|
||||
inject(service, "processConfigService", processConfigService);
|
||||
inject(service, "contractManageService", contractManageService);
|
||||
inject(service, "receivablePayableDetailService", receivablePayableDetailService);
|
||||
inject(service, "driverService", driverService);
|
||||
inject(service, "baseMapper", loadingManageMapper);
|
||||
doReturnTrue(service);
|
||||
authUtil = mockStatic(AuthUtil.class);
|
||||
authUtil.when(AuthUtil::getDeptId).thenReturn(DEPT_ID);
|
||||
authUtil.when(AuthUtil::getTenantId).thenReturn(TENANT_ID);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -309,6 +332,179 @@ class LoadingManageChildStatusWriteTest {
|
||||
verify(processConfigService, never()).list(ArgumentMatchers.<Wrapper<ProcessConfig>>any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("提交配载单:按手机号解析到档案 ID 时,配载单与子单一起记住归属")
|
||||
void submitWritesResolvedDriverId() {
|
||||
stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
|
||||
stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
|
||||
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
|
||||
|
||||
assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID)));
|
||||
|
||||
assertEquals(RESOLVED_DRIVER_ID, capturedLoading().getDriverId());
|
||||
assertEquals(RESOLVED_DRIVER_ID, statusSyncUpdates().get(0).setValue("driver_id"),
|
||||
"子单必须与配载单写同一个归属,否则司机端按归属查不到这张票");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("解析不到档案(承运商司机):提交照常成功,归属留空")
|
||||
void submitAllowsUnresolvedDriverId() {
|
||||
stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
|
||||
stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
|
||||
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(null);
|
||||
|
||||
assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID)));
|
||||
|
||||
assertNull(capturedLoading().getDriverId());
|
||||
assertNull(statusSyncUpdates().get(0).setValue("driver_id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("进行中编辑保留的子单:任务字段更新也带上司机归属,改派后不留旧值")
|
||||
void runningEditCarriesDriverIdToKeptWaybills() {
|
||||
LoadingManage record = runningRecord();
|
||||
stubLoadingRecord(record);
|
||||
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
|
||||
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
|
||||
|
||||
assertTrue(service.submit(editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID)));
|
||||
|
||||
CapturedUpdate taskUpdate = waybillUpdates().stream()
|
||||
.filter(update -> update.columns().contains("driver_name"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
assertEquals(List.of(NEED_ACCEPT_WAYBILL_ID), taskUpdate.whereIds());
|
||||
assertEquals(RESOLVED_DRIVER_ID, taskUpdate.setValue("driver_id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("复制配载单:沿用源单的司机归属,不按手机号重解析覆盖")
|
||||
void copyKeepsSourceDriverId() {
|
||||
LoadingManage source = copySource();
|
||||
source.setDriverId(555L);
|
||||
stubCopyDependencies(source);
|
||||
|
||||
service.copy(SOURCE_LOADING_ID);
|
||||
|
||||
assertEquals(555L, capturedSaved().getDriverId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("复制配载单:源单没有归属时,用手机号解析出的档案 ID")
|
||||
void copyFallsBackToResolvedDriverId() {
|
||||
LoadingManage source = copySource();
|
||||
stubCopyDependencies(source);
|
||||
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(RESOLVED_DRIVER_ID);
|
||||
|
||||
service.copy(SOURCE_LOADING_ID);
|
||||
|
||||
assertEquals(RESOLVED_DRIVER_ID, capturedSaved().getDriverId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已归属的单子号码没变:无关字段的保存不重解析,归属原样保留")
|
||||
void unrelatedEditKeepsStoredDriverId() {
|
||||
LoadingManage record = runningRecord();
|
||||
record.setDriverId(RESOLVED_DRIVER_ID);
|
||||
stubLoadingRecord(record);
|
||||
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
|
||||
// 这一次谁都解析不到:只要实现还按手机号重解析,归属就会被清空
|
||||
when(driverService.resolveDriverIdByMobile(any(), any())).thenReturn(null);
|
||||
|
||||
assertTrue(service.submit(editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID)));
|
||||
|
||||
assertEquals(RESOLVED_DRIVER_ID, capturedLoading().getDriverId());
|
||||
assertEquals(RESOLVED_DRIVER_ID, statusSyncUpdates().get(0).setValue("driver_id"));
|
||||
verify(driverService, never()).resolveDriverIdByMobile(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("人工补过归属的单子:号码恰好命中别人的档案,保存也不改归属")
|
||||
void unrelatedEditKeepsManualOwnership() {
|
||||
LoadingManage record = runningRecord();
|
||||
record.setDriverId(MANUAL_DRIVER_ID);
|
||||
stubLoadingRecord(record);
|
||||
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
|
||||
when(driverService.resolveDriverIdByMobile(any(), any())).thenReturn(OTHER_PROFILE_ID);
|
||||
|
||||
assertTrue(service.submit(editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID)));
|
||||
|
||||
assertEquals(MANUAL_DRIVER_ID, capturedLoading().getDriverId(),
|
||||
"人工归属不能被一次无关保存改到同号的另一个档案上");
|
||||
assertEquals(MANUAL_DRIVER_ID, statusSyncUpdates().get(0).setValue("driver_id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("-1 哨兵的归属:号码没变的保存原样保留,不抹成 NULL")
|
||||
void sentinelDriverIdSurvivesUnrelatedEdit() {
|
||||
LoadingManage record = runningRecord();
|
||||
record.setDriverId(-1L);
|
||||
stubLoadingRecord(record);
|
||||
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
|
||||
when(driverService.resolveDriverIdByMobile(any(), any())).thenReturn(null);
|
||||
|
||||
assertTrue(service.submit(editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID)));
|
||||
|
||||
assertEquals(-1L, capturedLoading().getDriverId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("换司机:号码变了就按新号码重新解析,归属跟着换")
|
||||
void changingPhoneResolvesNewDriverId() {
|
||||
LoadingManage record = pendingRecord();
|
||||
record.setDriverId(RESOLVED_DRIVER_ID);
|
||||
stubLoadingRecord(record);
|
||||
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
|
||||
stubConfigs(config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON));
|
||||
when(driverService.resolveDriverIdByMobile(any(), eq(OTHER_DRIVER_PHONE))).thenReturn(OTHER_PROFILE_ID);
|
||||
LoadingManage form = editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID);
|
||||
form.setDriverPhone(OTHER_DRIVER_PHONE);
|
||||
|
||||
assertTrue(service.submit(form));
|
||||
|
||||
assertEquals(OTHER_PROFILE_ID, capturedLoading().getDriverId());
|
||||
assertEquals(OTHER_PROFILE_ID, statusSyncUpdates().get(0).setValue("driver_id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("号码改成一个解析不到的号:归属落空,子单同步落空")
|
||||
void changingPhoneToUnresolvedNumberClearsDriverId() {
|
||||
LoadingManage record = pendingRecord();
|
||||
record.setDriverId(RESOLVED_DRIVER_ID);
|
||||
stubLoadingRecord(record);
|
||||
stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID));
|
||||
stubConfigs(config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON));
|
||||
when(driverService.resolveDriverIdByMobile(any(), eq(OTHER_DRIVER_PHONE))).thenReturn(null);
|
||||
LoadingManage form = editForm(LOADING_ID, NEED_ACCEPT_WAYBILL_ID);
|
||||
form.setDriverPhone(OTHER_DRIVER_PHONE);
|
||||
|
||||
assertTrue(service.submit(form));
|
||||
|
||||
assertNull(capturedLoading().getDriverId());
|
||||
assertNull(statusSyncUpdates().get(0).setValue("driver_id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("解析不到档案:留一条脱敏 WARN,保存照常成功")
|
||||
void unresolvedPhoneLogsMaskedWarning() {
|
||||
stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID));
|
||||
stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON));
|
||||
when(driverService.resolveDriverIdByMobile(any(), eq(DRIVER_PHONE))).thenReturn(null);
|
||||
try (LogCapture logs = LogCapture.open(LoadingManageServiceImpl.class)) {
|
||||
assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID)));
|
||||
|
||||
String warning = logs.events().stream()
|
||||
.filter(event -> event.getLevel() == Level.WARN)
|
||||
.map(ILoggingEvent::getFormattedMessage)
|
||||
.filter(message -> message.contains("司机档案"))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("解析失败必须留 WARN,实际日志:" + logs.messages()));
|
||||
assertTrue(warning.contains("138****8000"), "WARN 里的号码必须脱敏,实际:" + warning);
|
||||
assertFalse(warning.contains(DRIVER_PHONE), "WARN 里不能出现完整号码,实际:" + warning);
|
||||
assertTrue(warning.contains(LOADING_NO), "WARN 要带业务标识(配载单号),实际:" + warning);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private LoadingManage loadingForm(Long... waybillIds) {
|
||||
@@ -375,10 +571,54 @@ class LoadingManageChildStatusWriteTest {
|
||||
return waybill;
|
||||
}
|
||||
|
||||
private String capturedLoadingStatus() {
|
||||
private LoadingManage runningRecord() {
|
||||
LoadingManage record = loadingRecord("running", NEED_ACCEPT_WAYBILL_ID);
|
||||
record.setVehicleNo("桂A12345");
|
||||
record.setDriverName("张三");
|
||||
record.setDriverPhone(DRIVER_PHONE);
|
||||
return record;
|
||||
}
|
||||
|
||||
private LoadingManage pendingRecord() {
|
||||
LoadingManage record = loadingRecord("pending", NEED_ACCEPT_WAYBILL_ID);
|
||||
record.setVehicleNo("桂A12345");
|
||||
record.setDriverName("张三");
|
||||
record.setDriverPhone(DRIVER_PHONE);
|
||||
return record;
|
||||
}
|
||||
|
||||
private LoadingManage copySource() {
|
||||
LoadingManage source = loadingRecord("pending", NEED_ACCEPT_WAYBILL_ID);
|
||||
source.setLoadingSubNos("YD-" + NEED_ACCEPT_WAYBILL_ID);
|
||||
source.setTransportType("公路运输");
|
||||
source.setVehicleNo("桂A12345");
|
||||
source.setDriverName("张三");
|
||||
source.setDriverPhone(DRIVER_PHONE);
|
||||
source.setDepartureAddress("南宁");
|
||||
source.setArrivalAddress("广州");
|
||||
return source;
|
||||
}
|
||||
|
||||
private void stubCopyDependencies(LoadingManage source) {
|
||||
source.setId(SOURCE_LOADING_ID);
|
||||
doReturn(source).when(service).getById(SOURCE_LOADING_ID);
|
||||
doReturn(null).when(service).detail(any());
|
||||
}
|
||||
|
||||
private LoadingManage capturedSaved() {
|
||||
ArgumentCaptor<LoadingManage> captor = ArgumentCaptor.forClass(LoadingManage.class);
|
||||
verify(service, atLeastOnce()).save(captor.capture());
|
||||
return captor.getValue();
|
||||
}
|
||||
|
||||
private LoadingManage capturedLoading() {
|
||||
ArgumentCaptor<LoadingManage> captor = ArgumentCaptor.forClass(LoadingManage.class);
|
||||
verify(service, atLeastOnce()).saveOrUpdate(captor.capture());
|
||||
return captor.getValue().getBusinessStatus();
|
||||
return captor.getValue();
|
||||
}
|
||||
|
||||
private String capturedLoadingStatus() {
|
||||
return capturedLoading().getBusinessStatus();
|
||||
}
|
||||
|
||||
private void stubWaybills(Waybill... waybills) {
|
||||
@@ -438,18 +678,28 @@ class LoadingManageChildStatusWriteTest {
|
||||
}
|
||||
|
||||
private static void inject(LoadingManageServiceImpl target, String fieldName, Object value) {
|
||||
try {
|
||||
Field field = LoadingManageServiceImpl.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
throw new IllegalStateException("注入测试替身失败:" + fieldName, exception);
|
||||
Class<?> type = LoadingManageServiceImpl.class;
|
||||
while (type != null) {
|
||||
try {
|
||||
Field field = type.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
return;
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
type = type.getSuperclass();
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
throw new IllegalStateException("注入测试替身失败:" + fieldName, exception);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("注入测试替身失败:" + fieldName);
|
||||
}
|
||||
|
||||
/** 一条被捕获的批量更新:SET 的列 → 值、WHERE 的运单 id */
|
||||
private record CapturedUpdate(String sqlSet, String whereSql, Map<String, Object> params, List<String> columns) {
|
||||
|
||||
private static final java.util.regex.Pattern PARAM_REF =
|
||||
java.util.regex.Pattern.compile("#\\{ew\\.paramNameValuePairs\\.([^}]+)}");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static CapturedUpdate of(Wrapper<Waybill> wrapper) {
|
||||
LambdaUpdateWrapper<Waybill> update = (LambdaUpdateWrapper<Waybill>) wrapper;
|
||||
@@ -478,8 +728,20 @@ class LoadingManageChildStatusWriteTest {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 只认 WHERE 段用到的参数:SET 段的 driver_id 也是 Long,混进来会当成运单 id */
|
||||
List<Long> whereIds() {
|
||||
return params.values().stream().filter(Long.class::isInstance).map(Long.class::cast).toList();
|
||||
Set<String> setParamKeys = new java.util.HashSet<>();
|
||||
Matcher matcher = PARAM_REF.matcher(sqlSet);
|
||||
while (matcher.find()) {
|
||||
setParamKeys.add(matcher.group(1));
|
||||
}
|
||||
List<Long> ids = new java.util.ArrayList<>();
|
||||
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
||||
if (entry.getValue() instanceof Long longValue && !setParamKeys.contains(entry.getKey())) {
|
||||
ids.add(longValue);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.system.cache.UserDisplayCache;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.AdminHomeVO;
|
||||
import org.springblade.transport.pojo.vo.TripTabCountsVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IExceptionDisposalService;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IRiskDisposalService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.MpUserDataScopeSupport;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 调度端首页「异常处置」快捷入口的红点:只数待处理(pending)的异常。
|
||||
* <p>
|
||||
* 统计卡的「在途异常」仍按未了结(pending + processing)算,两处口径不同:红点是待办提醒,
|
||||
* 处置中的异常已经在跟进,再算进红点会让它一直清不掉。
|
||||
* <p>
|
||||
* 这里走 {@code home()}:红点是聚合接口里的字段,只测计数方法证明不了它接在哪一处。
|
||||
*/
|
||||
class ManageAdminHomeBadgeTest {
|
||||
|
||||
private static final String DEPT_ID = "1123598813738675201";
|
||||
private static final Long USER_ID = 1900000000000000001L;
|
||||
private static final long PENDING_COUNT = 3L;
|
||||
private static final long INCOMPLETE_COUNT = 7L;
|
||||
|
||||
private ManageWaybillServiceImpl service;
|
||||
private IExceptionDisposalService exceptionService;
|
||||
private MockedStatic<AuthUtil> authUtil;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||
TableInfoHelper.initTableInfo(assistant, ExceptionDisposal.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
IWaybillService waybillService = mock(IWaybillService.class);
|
||||
exceptionService = mock(IExceptionDisposalService.class);
|
||||
ITripQueryService tripQueryService = mock(ITripQueryService.class);
|
||||
when(tripQueryService.tabCounts(ArgumentMatchers.any())).thenReturn(new TripTabCountsVO());
|
||||
when(exceptionService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
|
||||
.thenReturn(new ArrayList<>());
|
||||
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(new ArrayList<>());
|
||||
// 待重新派单的分页查询回它收到的 Page(总数从入参上读,回 null 会空指针)
|
||||
when(waybillService.page(ArgumentMatchers.any(), ArgumentMatchers.any()))
|
||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||
// 处置状态的计数被查两次:带 processing 的是「未了结」,只带 pending 的是「待处理」
|
||||
when(exceptionService.count(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
|
||||
.thenAnswer(invocation -> coversProcessing(invocation.getArgument(0)) ? INCOMPLETE_COUNT : PENDING_COUNT);
|
||||
service = new ManageWaybillServiceImpl(
|
||||
waybillService,
|
||||
mock(IDriverService.class),
|
||||
exceptionService,
|
||||
mock(IRiskDisposalService.class),
|
||||
mock(IDriverWaybillService.class),
|
||||
tripQueryService,
|
||||
mock(ILoadingManageService.class),
|
||||
mock(org.springblade.transport.service.ITransportVehicleService.class),
|
||||
mock(MpUserDataScopeSupport.class));
|
||||
authUtil = mockStatic(AuthUtil.class);
|
||||
authUtil.when(AuthUtil::getUserRole).thenReturn("yewu");
|
||||
authUtil.when(AuthUtil::getDeptId).thenReturn(DEPT_ID);
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
authUtil.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("红点只数待处理,统计卡的在途异常仍是未了结数")
|
||||
void exceptionBadgeCountsPendingOnly() {
|
||||
AdminHomeVO home;
|
||||
try (MockedStatic<UserDisplayCache> ignored = mockStatic(UserDisplayCache.class)) {
|
||||
home = service.home();
|
||||
}
|
||||
|
||||
assertEquals(PENDING_COUNT, home.getBadges().getException(), "处置中的异常不再计入红点");
|
||||
assertEquals(INCOMPLETE_COUNT, home.getStats().getException(), "统计卡口径不变:pending + processing");
|
||||
|
||||
ArgumentCaptor<Wrapper<ExceptionDisposal>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(exceptionService, times(2)).count(captor.capture());
|
||||
List<String> segments = captor.getAllValues().stream()
|
||||
.map(ManageAdminHomeBadgeTest::sqlSegment)
|
||||
.collect(Collectors.toList());
|
||||
assertTrue(segments.stream().anyMatch(segment -> segment.contains("disposal_status IN")),
|
||||
"统计卡仍要数未了结:处置状态取 pending 与 processing 两档:" + segments);
|
||||
assertTrue(segments.stream().anyMatch(segment -> segment.contains("disposal_status = ")),
|
||||
"红点只认待处理:处置状态是等值条件,不再带 processing:" + segments);
|
||||
}
|
||||
|
||||
/** 计数条件里是否含处置中:带 IN 的才是「pending + processing」那次查询 */
|
||||
private static boolean coversProcessing(Wrapper<ExceptionDisposal> wrapper) {
|
||||
return sqlSegment(wrapper).contains("disposal_status IN");
|
||||
}
|
||||
|
||||
private static String sqlSegment(Wrapper<ExceptionDisposal> wrapper) {
|
||||
return ((AbstractWrapper<?, ?, ?>) wrapper).getSqlSegment();
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user